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

feat(storybook): implement argTypeEnhancers for improved Slot API docs rendering #33838

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
30 changes: 30 additions & 0 deletions apps/public-docsite-v9/.storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,33 @@ export const parameters = {
},
},
};

const slotRegex = /as\?:\s*"([^"]+)"/;

/**
*
* @type {import('@storybook/types').ArgTypesEnhancer}
*/
const withSlotEnhancer = context => {
Copy link
Contributor Author

@Hotell Hotell Feb 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this works as expected with default DocsPage as it updates context properly which is then obtained and used for rendering the view.

https://github.com/storybookjs/storybook/blob/release-7-6/code/ui/blocks/src/blocks/ArgsTable.tsx#L220

because we override DocsPage by our custom implementation, this is no longer working unfortunately, as the context won't propagate as expected within default SB ArgsTable component.

Task:

  • determine if this can be set properly so SB ArgsTable would render enhanced data - if doable we don't have to execute the override inline within the custom DocsPage implementation

const updatedArgTypes = { ...context.argTypes };

Object.entries(updatedArgTypes).forEach(([key, argType]) => {
// @ts-expect-error - storybook doesn't ship proper types
const value = argType?.type?.value;

// we are interested only on raw strings ( which is case of non Storybook supported types)
if (value && typeof value === 'string') {
const match = value.match(slotRegex);
Copy link
Contributor Author

@Hotell Hotell Feb 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to tweak the overrides if the Slot type is complex ( eg prop is using Slot<typeof SomeOtherComponent>:

if( value.includes('WithSlotShorthandValue')){
  const match = value.match(slotRegex);
  if(match) {
    // we know that the origin prop was defined via `Slot<'html elemen'>`
   // update value to `Slot<\"${match[1]}\">`
  } else {
  // we know that the origin prop is using non trivial type structure like `Slot<typeof PresenceBadgeProps>` -> we just fallback to `Slot` because we are unable to infer the correct value
// update to `Slot`
  }
}

if (match) {
updatedArgTypes[key].table.type.summary = `Slot<\"${match[1]}\">`;
}
}
});

return updatedArgTypes;
};

/**
* @type {import('@storybook/types').ArgTypesEnhancer[]}
*/
export const argTypesEnhancers = [withSlotEnhancer];
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
Stories,
type DocsContextProps,
} from '@storybook/addon-docs';
import type { PreparedStory, Renderer } from '@storybook/types';
import type { PreparedStory, Renderer, StrictArgTypes, StoryContextForEnhancers } from '@storybook/types';
import type { SBEnumType } from '@storybook/csf';
import { makeStyles, shorthands, tokens, Link, Text } from '@fluentui/react-components';
import { InfoFilled } from '@fluentui/react-icons';
Expand Down Expand Up @@ -133,11 +133,41 @@ const getNativeElementsList = (elements: SBEnumType['value']): JSX.Element => {
);
};

const RenderArgsTable = ({ hideArgsTable, primaryStory }: { primaryStory: PrimaryStory; hideArgsTable: boolean }) => {
const slotRegex = /as\?:\s*"([^"]+)"/;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems this is not catching all Slot apis as expected for example (Avatar Component / https://react.fluentui.dev/?path=/docs/components-avatar--docs):

image

Todo:

  • make sure we are catching all actual Slot definitions

function withSlotEnhancer(story: PreparedStory) {
const docGenProps = story.component?.__docgenInfo?.props;

if (!docGenProps) {
return story;
}

Object.entries(docGenProps).forEach(([key, argType]) => {
const value: string = argType?.type?.name;
const match = value.match(slotRegex);

if (Array.isArray(match)) {
story.component.__docgenInfo.props[key].type.name = `Slot<\"${match[1]}\">`;
}
});

return story;
}

const RenderArgsTable = ({
hideArgsTable,
primaryStory,
argsTypes,
}: {
primaryStory: PrimaryStory;
hideArgsTable: boolean;
argsTypes: StrictArgTypes;
}) => {
const styles = useStyles();

return hideArgsTable ? null : (
<>
<ArgsTable of={primaryStory.component} />

{primaryStory.argTypes.as && primaryStory.argTypes.as?.type?.name === 'enum' && (
<div className={styles.nativeProps}>
<InfoFilled className={styles.nativePropsIcon} />
Expand Down Expand Up @@ -179,6 +209,7 @@ const RenderPrimaryStory = ({
export const FluentDocsPage = () => {
const context = React.useContext(DocsContext);
const stories = context.componentStories();

const primaryStory = stories[0];
const primaryStoryContext = context.getStoryContext(primaryStory);

Expand All @@ -204,6 +235,8 @@ export const FluentDocsPage = () => {
// })),
// );

const TweakedComponent = withSlotEnhancer(primaryStory);

return (
<div className="sb-unstyled">
<Title />
Expand All @@ -219,7 +252,11 @@ export const FluentDocsPage = () => {
{videos && <VideoPreviews videos={videos} />}
</div>
<RenderPrimaryStory primaryStory={primaryStory} skipPrimaryStory={skipPrimaryStory} />
<RenderArgsTable primaryStory={primaryStory} hideArgsTable={hideArgsTable} />
<RenderArgsTable
primaryStory={primaryStory}
hideArgsTable={hideArgsTable}
argsTypes={primaryStoryContext.argTypes}
/>
<Stories />
</div>
<div className={styles.toc}>
Expand Down
Loading