Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[docs-infra] Simplify API sections typing #43128

Merged
merged 20 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 58 additions & 30 deletions docs/src/modules/components/ApiPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { ComponentApiContent, PropsTranslations } from '@mui-internal/api-docs-builder';
import exactProp from '@mui/utils/exactProp';
import Typography from '@mui/material/Typography';
import Alert from '@mui/material/Alert';
import { TableOfContentsEntry } from '@mui/internal-markdown';
import { Ad, AdGuest } from '@mui/docs/Ad';
import VerifiedRoundedIcon from '@mui/icons-material/VerifiedRounded';
import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
Expand All @@ -13,18 +15,29 @@ import { BrandingProvider } from '@mui/docs/branding';
import { SectionTitle, SectionTitleProps } from '@mui/docs/SectionTitle';
import { MarkdownElement } from '@mui/docs/MarkdownElement';
import AppLayoutDocs from 'docs/src/modules/components/AppLayoutDocs';
import PropertiesSection, {
getPropsToC,
} from 'docs/src/modules/components/ApiPage/sections/PropertiesSection';
import ClassesSection, {
getClassesToC,
} from 'docs/src/modules/components/ApiPage/sections/ClassesSection';
import PropertiesSection from 'docs/src/modules/components/ApiPage/sections/PropertiesSection';
import ClassesSection from 'docs/src/modules/components/ApiPage/sections/ClassesSection';
import SlotsSection from 'docs/src/modules/components/ApiPage/sections/SlotsSection';
import {
ApiDisplayOptions,
DEFAULT_API_LAYOUT_STORAGE_KEYS,
} from 'docs/src/modules/components/ApiPage/sections/ToggleDisplayOption';
import { TableOfContentsEntry } from '@mui/internal-markdown';
import {
getPropertiesToC,
getPropsApiDefinitions,
} from 'docs/src/modules/components/ApiPage/definitions/properties';
import {
getClassApiDefinitions,
getClassesToC,
} from 'docs/src/modules/components/ApiPage/definitions/classes';
import { getSlotsApiDefinitions } from './ApiPage/definitions/slots';

// TODO Move this type definition to the AppLayoutDocs file when moved to TS
export interface TableOfContentsParams {
children: (TableOfContentsParams | TableOfContentsEntry)[];
hash: string;
text: string;
}

type ApiHeaderKeys =
| 'demos'
Expand Down Expand Up @@ -150,6 +163,23 @@ export default function ApiPage(props: ApiPageProps) {
// Prefer linking the .tsx or .d.ts for the "Edit this page" link.
const apiSourceLocation = filename.replace('.js', '.d.ts');

// Merge data and translation
const propertiesDef = getPropsApiDefinitions({
componentName: pageContent.name,
properties: componentProps,
propertiesDescriptions: propDescriptions,
});
const classesDef = getClassApiDefinitions({
componentClasses,
componentName: pageContent.name,
classDescriptions,
});
const slotsDef = getSlotsApiDefinitions({
componentSlots,
componentName: pageContent.name,
slotDescriptions,
});

function createTocEntry(sectionName: ApiHeaderKeys) {
return {
text: getTranslatedHeader(t, sectionName),
Expand All @@ -165,23 +195,13 @@ export default function ApiPage(props: ApiPageProps) {
};
}

const toc = [
const toc: TableOfContentsParams[] = [
createTocEntry('demos'),
createTocEntry('import'),
...componentDescriptionToc,
getPropsToC({
t,
componentName: pageContent.name,
componentProps,
inheritance,
themeDefaultProps: pageContent.themeDefaultProps,
}),
componentSlots?.length > 0 && createTocEntry('slots'),
...getClassesToC({
t,
componentName: pageContent.name,
componentClasses,
}),
getPropertiesToC({ properties: propertiesDef, hash: 'props', t }),
...(componentSlots?.length > 0 ? [createTocEntry('slots')] : []),
...getClassesToC({ classes: classesDef, t }),
].filter(Boolean);

// The `ref` is forwarded to the root element.
Expand Down Expand Up @@ -272,9 +292,7 @@ export default function ApiPage(props: ApiPageProps) {
</React.Fragment>
) : null}
<PropertiesSection
properties={componentProps}
propertiesDescriptions={propDescriptions}
componentName={pageContent.name}
properties={propertiesDef}
spreadHint={spreadHint}
defaultLayout={defaultLayout}
layoutStorageKey={layoutStorageKey.props}
Expand Down Expand Up @@ -327,9 +345,7 @@ export default function ApiPage(props: ApiPageProps) {
)}
(
<SlotsSection
componentSlots={componentSlots}
slotDescriptions={slotDescriptions}
componentName={pageContent.name}
slots={slotsDef}
spreadHint={
slotGuideLink &&
t('api-docs.slotDescription').replace(/{{slotGuideLink}}/, slotGuideLink)
Expand All @@ -339,9 +355,7 @@ export default function ApiPage(props: ApiPageProps) {
/>
)
<ClassesSection
componentClasses={componentClasses}
componentName={pageContent.name}
classDescriptions={classDescriptions}
classes={classesDef}
spreadHint={t('api-docs.classesDescription')}
styleOverridesLink={styleOverridesLink}
defaultLayout={defaultLayout}
Expand All @@ -357,3 +371,17 @@ export default function ApiPage(props: ApiPageProps) {
</AppLayoutDocs>
);
}

if (process.env.NODE_ENV !== 'production') {
ApiPage.propTypes = exactProp({
defaultLayout: PropTypes.oneOf(['collapsed', 'expanded', 'table']),
descriptions: PropTypes.object.isRequired,
disableAd: PropTypes.bool,
layoutStorageKey: PropTypes.shape({
classes: PropTypes.string,
props: PropTypes.string,
slots: PropTypes.string,
}),
pageContent: PropTypes.object.isRequired,
});
}
82 changes: 82 additions & 0 deletions docs/src/modules/components/ApiPage/definitions/classes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { PropsTranslations, ComponentApiContent } from '@mui-internal/api-docs-builder';
import { Translate } from '@mui/docs/i18n';
import kebabCase from 'lodash/kebabCase';
import type { TableOfContentsParams } from 'docs/src/modules/components/ApiPage';

export interface ClassDefinition {
className: string;
key: string;
hash: string;
description?: string;
isGlobal?: boolean;
isDeprecated?: boolean;
deprecationInfo?: string;
}

export type GetCssToCParams = {
classes: ClassDefinition[];
t: Translate;
hash?: string;
};

export const getClassesToC = ({ classes, t, hash }: GetCssToCParams): TableOfContentsParams[] =>
!classes || classes.length === 0
? []
: [
{
text: t('api-docs.classes'),
hash: hash ?? 'classes',
children: [
...classes.map(({ key, hash: classeHash }) => ({
text: key,
hash: classeHash,
children: [],
})),
],
},
];

export interface GetClassApiDefinitionsParams {
componentClasses: ComponentApiContent['classes'];
classDescriptions: PropsTranslations['classDescriptions'];
componentName: string;
}

const errorMessage = (componentName: string, className: string, slotName: string): string =>
`${className} description from component ${componentName} should include ${slotName} since its definition includes "{{${slotName}}}"`;

export function getClassApiDefinitions(params: GetClassApiDefinitionsParams): ClassDefinition[] {
const { componentClasses, classDescriptions, componentName } = params;

return componentClasses.map((classDefinition) => {
const {
conditions,
nodeName,
deprecationInfo,
description: translatedDescription,
} = classDescriptions[classDefinition.key] ?? {}; // Not all classes have a description.

let description = translatedDescription ?? classDefinition.description;

if (description.includes('{{conditions}}')) {
if (!conditions) {
throw Error(errorMessage(componentName, classDefinition.className, 'conditions'));
}
description = description.replace(/{{conditions}}/, conditions);
}

if (description.includes('{{nodeName}}')) {
if (!nodeName) {
throw Error(errorMessage(componentName, classDefinition.className, 'nodeName'));
}
description = description.replace(/{{nodeName}}/, nodeName);
}

return {
...classDefinition,
description,
deprecationInfo,
hash: `${kebabCase(componentName)}-classes-${classDefinition.className}`,
};
});
}
Loading