diff --git a/apps/public-docsite-v9/src/DocsComponents/FluentDocsPage.stories.tsx b/apps/public-docsite-v9/src/DocsComponents/FluentDocsPage.stories.tsx index 7313f5f22f428d..14694ba1e1cc15 100644 --- a/apps/public-docsite-v9/src/DocsComponents/FluentDocsPage.stories.tsx +++ b/apps/public-docsite-v9/src/DocsComponents/FluentDocsPage.stories.tsx @@ -10,8 +10,7 @@ import { Stories, type DocsContextProps, } from '@storybook/addon-docs'; -import type { PreparedStory, Renderer } from '@storybook/types'; -import type { SBEnumType } from '@storybook/csf'; +import type { PreparedStory, Renderer, SBEnumType } from '@storybook/types'; import { makeStyles, shorthands, tokens, Link, Text } from '@fluentui/react-components'; import { InfoFilled } from '@fluentui/react-icons'; import { DIR_ID, THEME_ID, themes } from '@fluentui/react-storybook-addon'; @@ -52,24 +51,32 @@ const useStyles = makeStyles({ display: 'grid', gridTemplateColumns: '1fr min-content', }, - nativeProps: { + additionalInfo: { display: 'flex', - gap: tokens.spacingHorizontalM, - + flexDirection: 'column', + gap: tokens.spacingVerticalM, border: `1px solid ${tokens.colorNeutralStroke1}`, borderRadius: tokens.borderRadiusMedium, padding: tokens.spacingHorizontalM, margin: `0 ${tokens.spacingHorizontalM}`, }, - nativePropsIcon: { + additionalInfoIcon: { alignSelf: 'center', color: tokens.colorBrandForeground1, fontSize: '24px', + marginRight: tokens.spacingHorizontalM, + }, + additionalInfoMessage: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: tokens.spacingVerticalXS, }, - nativePropsMessage: { + infoIcon: { display: 'flex', flexDirection: 'column', gap: tokens.spacingVerticalXS, + flex: 1, }, }); @@ -120,7 +127,7 @@ const VideoPreviews: React.FC<{ }; const getNativeElementsList = (elements: SBEnumType['value']): JSX.Element => { - const elementsArr = elements.map((el, idx) => [ + const elementsArr = elements?.map((el, idx) => [ {`<${el}>`}, idx !== elements.length - 1 ? ', ' : ' ', ]); @@ -133,30 +140,110 @@ const getNativeElementsList = (elements: SBEnumType['value']): JSX.Element => { ); }; -const RenderArgsTable = ({ hideArgsTable, primaryStory }: { primaryStory: PrimaryStory; hideArgsTable: boolean }) => { +const slotRegex = /as\?:\s*"([^"]+)"/; +/** + * NOTE: this function mutates original story argTypes including all story subcomponents if they are present + */ +function withSlotEnhancer(story: PreparedStory) { + const hasArgAsProp = story.argTypes.as?.type?.name === 'enum'; + const argAsProp = hasArgAsProp ? (story.argTypes.as.type as SBEnumType).value : null; + let hasArgAsSlot = false; + + type InternalComponentApi = { + __docgenInfo: { + props?: Record; + }; + [k: string]: unknown; + }; + + const transformPropsWithSlotShorthand = (props: Record) => { + Object.entries(props).forEach(([key, argType]) => { + const value: string = argType?.type?.name; + if (value.includes('WithSlotShorthandValue')) { + hasArgAsSlot = true; + const match = value.match(slotRegex); + if (match) { + props[key].type.name = `Slot<\"${match[1]}\">`; + } else { + props[key].type.name = `Slot`; + } + } + }); + }; + + const transformComponent = (component: InternalComponentApi) => { + const docGenProps = component?.__docgenInfo?.props; + if (docGenProps) { + transformPropsWithSlotShorthand(docGenProps); + } + }; + + const component = story.component as InternalComponentApi; + transformComponent(component); + + if (story.subcomponents) { + Object.values(story.subcomponents).forEach((subcomponent: InternalComponentApi) => { + transformComponent(subcomponent); + }); + } + + return { component, hasArgAsSlot, hasArgAsProp, argAsProp }; +} + +const AdditionalApiDocs: React.FC<{ children: React.ReactElement | React.ReactElement[] }> = ({ children }) => { const styles = useStyles(); + return ( +
+
+ +
{children}
+
+
+ ); +}; +const RenderArgsTable = ({ hideArgsTable, story }: { story: PrimaryStory; hideArgsTable: boolean }) => { + const { component, hasArgAsProp, hasArgAsSlot, argAsProp } = withSlotEnhancer(story); + return hideArgsTable ? null : ( <> - - {primaryStory.argTypes.as && primaryStory.argTypes.as?.type?.name === 'enum' && ( -
- -
+ {hasArgAsProp && ( + +

Native props are supported 🙌 +
- All HTML attributes native to the {getNativeElementsList(primaryStory.argTypes.as.type.value)}, including - all aria-* and data-* attributes, can be applied as native props on this - component. + All HTML attributes native to the + {getNativeElementsList(argAsProp!)}, including all aria-* and data-* attributes, + can be applied as native props on this component. -

-
+

+ )} + {hasArgAsSlot && ( + +

+ + Customizing components with slots 🙌 + +
+ + Slots in Fluent UI React components are designed to be modified or replaced, providing a flexible approach + to customizing components. Each slot is exposed as a top-level prop and can be filled with primitive + values, JSX/TSX, props objects, or render functions. This allows for more dynamic and reusable component + structures, similar to slots in other frameworks.{' '} + + Customizing components with slots{' '} + + +

+
+ )} + ); }; - const RenderPrimaryStory = ({ primaryStory, skipPrimaryStory, @@ -179,6 +266,7 @@ const RenderPrimaryStory = ({ export const FluentDocsPage = () => { const context = React.useContext(DocsContext); const stories = context.componentStories(); + const primaryStory = stories[0]; const primaryStoryContext = context.getStoryContext(primaryStory); @@ -219,7 +307,7 @@ export const FluentDocsPage = () => { {videos && } - +