From 094b22fc80668355bdbc663ccd8676929bb02046 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Thu, 7 May 2026 14:42:37 +0200 Subject: [PATCH] feat(react-storybook-addon): add render-prop slots to FluentDocsPage (#36115) Co-authored-by: Claude Opus 4.7 (1M context) --- ...-5627432c-5a25-42db-84fd-3b856653bba2.json | 7 + .../stories/.storybook/HeadlessDocsPage.tsx | 149 +++++++++--------- .../stories/.storybook/headless-docs-page.css | 1 + .../etc/react-storybook-addon.api.md | 12 ++ .../src/docs/FluentDocsPage.tsx | 60 +++++-- .../react-storybook-addon/src/docs/index.ts | 1 + .../react-storybook-addon/src/index.ts | 3 +- 7 files changed, 137 insertions(+), 96 deletions(-) create mode 100644 change/@fluentui-react-storybook-addon-5627432c-5a25-42db-84fd-3b856653bba2.json diff --git a/change/@fluentui-react-storybook-addon-5627432c-5a25-42db-84fd-3b856653bba2.json b/change/@fluentui-react-storybook-addon-5627432c-5a25-42db-84fd-3b856653bba2.json new file mode 100644 index 0000000000000..df1a7c0e2fe36 --- /dev/null +++ b/change/@fluentui-react-storybook-addon-5627432c-5a25-42db-84fd-3b856653bba2.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: Allow consumers to replace primary-story / args-table / stories sections of FluentDocsPage", + "packageName": "@fluentui/react-storybook-addon", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-headless-components-preview/stories/.storybook/HeadlessDocsPage.tsx b/packages/react-components/react-headless-components-preview/stories/.storybook/HeadlessDocsPage.tsx index a7535d328dbc0..71bf09a99eb87 100644 --- a/packages/react-components/react-headless-components-preview/stories/.storybook/HeadlessDocsPage.tsx +++ b/packages/react-components/react-headless-components-preview/stories/.storybook/HeadlessDocsPage.tsx @@ -1,27 +1,16 @@ /** - * `HeadlessDocsPage` — replaces Storybook's autodocs page so we can render a - * **tabbed** "Show code" panel under each story (TSX + each CSS Module the - * story uses). The deployed Fluent docs page (`FluentDocsPage`) hard-wires - * `` / `` blocks whose Source can't be made multi-language, - * so we re-implement the same layout (Title / Subtitle / Description / - * primary canvas + source / ArgTypes / Stories heading / each story canvas + - * source) and swap the source block for our own ``. The order - * mirrors `packages/react-components/react-storybook-addon/src/docs/FluentDocsPage.tsx` - * so the page matches what's deployed at storybooks.fluentui.dev/headless. - * + * `HeadlessDocsPage` — thin wrapper around `FluentDocsPage` that swaps in + * headless-specific renderers for the primary story and the secondary + * stories list. The shared page handles the docs chrome (Title / Subtitle / + * Description / ArgTypes via the slot enhancer); the renderers here add the + * "preview" disclaimer, wrap each canvas in ``, and replace + * Storybook's single-blob Source block with the tabbed `` + * (TSX + each CSS Module the story uses). */ import * as React from 'react'; -import { - Anchor, - ArgTypes, - Canvas, - Description, - DocsContext, - HeaderMdx, - Subtitle, - Title, -} from '@storybook/addon-docs/blocks'; +import { Anchor, Canvas, Description, HeaderMdx } from '@storybook/addon-docs/blocks'; +import { FluentDocsPage, type FluentDocsPageProps } from '@fluentui/react-storybook-addon'; import { HeadlessSourcePanel } from './HeadlessSourcePanel'; @@ -70,67 +59,71 @@ const disclaimerNoteStyle: React.CSSProperties = { color: '#3c3c3c', }; -export const HeadlessDocsPage: React.FC = () => { - const docsContext = React.useContext(DocsContext); - const stories = docsContext.componentStories(); - - const primaryStory = stories[0]; - const remainingStories = stories.slice(1); +const Disclaimer: React.FC = () => ( + +); +/** + * Prefixes the preview disclaimer, then renders the primary story inside an + * `` so `HeadlessSourcePanel` can portal its tabbed code panel into + * the same canvas card as the story. + * + * The `@fluentui/react-storybook-addon-export-to-sandbox` decorator looks for + * `.docblock-code-toggle` inside `.docs-story` — Canvas's default + * `sourceState: 'hidden'` keeps that toggle in the canvas footer next to the + * "Open in Stackblitz" button (see `HeadlessSourcePanel` for how the toggle + * drives the tabbed panel). + */ +const HeadlessRenderPrimaryStory: FluentDocsPageProps['renderPrimaryStory'] = ({ primaryStory, skipPrimaryStory }) => { + if (skipPrimaryStory) { + return null; + } return ( -
- {/* - The `@fluentui/react-storybook-addon-export-to-sandbox` decorator looks - for `.docblock-code-toggle` inside `.docs-story` of each story to anchor - its "Open in Stackblitz" button. We keep Canvas's default sourceState - ('hidden') so the native "Show code" toggle is rendered there too — - the Stackblitz button sits next to it inside the canvas footer (see - `HeadlessSourcePanel` for how its clicks drive our tabbed panel). - */} - - <Subtitle /> - <Description /> - <aside style={disclaimerStyle} role="note"> - <div> - <strong>Heads up:</strong> headless components ship without default styles. The CSS shown in these stories is - provided purely as a demonstration of one possible look. - </div> - <div style={disclaimerNoteStyle}> - <strong>Preview:</strong> these controls are in preview and their APIs are subject to change. - </div> - </aside> + <div> + <Disclaimer /> + <hr style={dividerStyle} /> + <HeaderMdx as="h3" id={nameToHash(primaryStory.name)}> + {primaryStory.name} + </HeaderMdx> + <Anchor storyId={primaryStory.id}> + <Canvas of={primaryStory.moduleExport} /> + <HeadlessSourcePanel of={primaryStory.moduleExport} /> + </Anchor> + </div> + ); +}; - {primaryStory && ( - <> - <hr style={dividerStyle} /> - <HeaderMdx as="h3" id={nameToHash(primaryStory.name)}> - {primaryStory.name} +const HeadlessRenderStories: FluentDocsPageProps['renderStories'] = ({ stories }) => { + if (stories.length === 0) { + return <></>; + } + return ( + <> + <h2 style={storiesHeadingStyle}>Stories</h2> + {stories.map(story => ( + <Anchor key={story.id} storyId={story.id}> + <HeaderMdx as="h3" id={nameToHash(story.name)}> + {story.name} </HeaderMdx> - <Anchor storyId={primaryStory.id}> - <Canvas of={primaryStory.moduleExport} /> - <HeadlessSourcePanel of={primaryStory.moduleExport} /> - </Anchor> - </> - )} - - {/* Component-level props table (mirrors what FluentDocsPage renders). */} - <ArgTypes /> - - {remainingStories.length > 0 && ( - <> - <h2 style={storiesHeadingStyle}>Stories</h2> - {remainingStories.map(story => ( - <Anchor key={story.id} storyId={story.id}> - <HeaderMdx as="h3" id={nameToHash(story.name)}> - {story.name} - </HeaderMdx> - <Description of={story.moduleExport} /> - <Canvas of={story.moduleExport} /> - <HeadlessSourcePanel of={story.moduleExport} /> - </Anchor> - ))} - </> - )} - </div> + <Description of={story.moduleExport} /> + <Canvas of={story.moduleExport} /> + <HeadlessSourcePanel of={story.moduleExport} /> + </Anchor> + ))} + </> ); }; + +export const HeadlessDocsPage: React.FC = () => ( + <div className="headless-docs-page"> + <FluentDocsPage renderPrimaryStory={HeadlessRenderPrimaryStory} renderStories={HeadlessRenderStories} /> + </div> +); diff --git a/packages/react-components/react-headless-components-preview/stories/.storybook/headless-docs-page.css b/packages/react-components/react-headless-components-preview/stories/.storybook/headless-docs-page.css index 3e65ab063698b..39c3039bf5913 100644 --- a/packages/react-components/react-headless-components-preview/stories/.storybook/headless-docs-page.css +++ b/packages/react-components/react-headless-components-preview/stories/.storybook/headless-docs-page.css @@ -26,6 +26,7 @@ box-shadow: none !important; border-radius: 0 !important; right: auto !important; + margin-top: 0 !important; } /* diff --git a/packages/react-components/react-storybook-addon/etc/react-storybook-addon.api.md b/packages/react-components/react-storybook-addon/etc/react-storybook-addon.api.md index a878249615a68..977601745fed0 100644 --- a/packages/react-components/react-storybook-addon/etc/react-storybook-addon.api.md +++ b/packages/react-components/react-storybook-addon/etc/react-storybook-addon.api.md @@ -7,7 +7,9 @@ import type { Args } from '@storybook/react-webpack5'; import type { JSXElement } from '@fluentui/react-utilities'; import type { Parameters as Parameters_2 } from '@storybook/react-webpack5'; +import type { PreparedStory } from 'storybook/internal/types'; import * as React_2 from 'react'; +import type { Renderer } from 'storybook/internal/types'; import type { StoryContext } from '@storybook/react-webpack5'; // @public (undocumented) @@ -16,6 +18,16 @@ export const DIR_ID: "storybook_fluentui-react-addon_dir"; // @public export const FluentCanvas: (props: React_2.ComponentProps<"div">) => JSXElement; +// @public (undocumented) +export const FluentDocsPage: ({ renderPrimaryStory, renderArgsTable, renderStories, }?: FluentDocsPageProps) => JSXElement; + +// @public (undocumented) +export type FluentDocsPageProps = { + renderPrimaryStory?: typeof RenderPrimaryStory; + renderArgsTable?: typeof RenderArgsTable; + renderStories?: typeof RenderStories; +}; + // @public export interface FluentGlobals extends Args { // (undocumented) diff --git a/packages/react-components/react-storybook-addon/src/docs/FluentDocsPage.tsx b/packages/react-components/react-storybook-addon/src/docs/FluentDocsPage.tsx index 6ab3e7b5951cf..57728961cff7f 100644 --- a/packages/react-components/react-storybook-addon/src/docs/FluentDocsPage.tsx +++ b/packages/react-components/react-storybook-addon/src/docs/FluentDocsPage.tsx @@ -248,7 +248,6 @@ const AdditionalApiDocs: React.FC<{ children: React.ReactElement | React.ReactEl </div> ); }; - const RenderArgsTable = ({ story, hideArgsTable, @@ -269,6 +268,7 @@ const RenderArgsTable = ({ return hideArgsTable ? null : ( <> <div className={styles.additionalInfoWrapper}> + <ArgTypes of={component} /> {hasArgAsProp && ( <AdditionalApiDocs> <p> @@ -304,7 +304,6 @@ const RenderArgsTable = ({ </AdditionalApiDocs> )} </div> - <ArgTypes of={component} /> </> ); }; @@ -328,7 +327,37 @@ const RenderPrimaryStory = ({ ); }; -export const FluentDocsPage = (): JSXElement => { +const RenderStories = ({ skipPrimaryStory }: { stories: PrimaryStory[]; skipPrimaryStory?: boolean }) => ( + <Stories includePrimary={!skipPrimaryStory} /> +); + +export type FluentDocsPageProps = { + /** + * Render the primary-story section (divider + h3 anchor heading + canvas). + * Defaults to the addon's built-in `RenderPrimaryStory`. Pass a custom + * component to swap the canvas/source rendering (e.g. wrap in `<Anchor>`, + * use a custom source panel) while keeping the rest of the docs page chrome. + */ + renderPrimaryStory?: typeof RenderPrimaryStory; + /** + * Render the props table. Defaults to the addon's built-in `RenderArgsTable`, + * which runs `withSlotEnhancer` and adds the slot/native-props info cards. + */ + renderArgsTable?: typeof RenderArgsTable; + /** + * Render the secondary-stories section. Defaults to Storybook's `<Stories />` + * block. When overridden, the page passes the non-primary stories so the + * consumer can iterate (e.g. to add per-story anchors and a custom source + * panel). The default ignores `stories` and lets `<Stories />` self-iterate. + */ + renderStories?: typeof RenderStories; +}; + +export const FluentDocsPage = ({ + renderPrimaryStory = RenderPrimaryStory, + renderArgsTable = RenderArgsTable, + renderStories = RenderStories, +}: FluentDocsPageProps = {}): JSXElement => { const context = React.useContext(DocsContext); // Get the fluent docs page configuration from context @@ -357,9 +386,9 @@ export const FluentDocsPage = (): JSXElement => { <Title /> <Subtitle /> <Description /> - <RenderPrimaryStory primaryStory={primaryStory} skipPrimaryStory={skipPrimaryStory} /> - <RenderArgsTable story={primaryStory} hideArgsTable={hideArgsTable} /> - <Stories /> + {renderPrimaryStory({ primaryStory: primaryStory, skipPrimaryStory })} + {renderArgsTable({ story: primaryStory, hideArgsTable })} + {renderStories({ stories: stories.slice(1), skipPrimaryStory })} </div> ); } @@ -402,17 +431,14 @@ export const FluentDocsPage = (): JSXElement => { <Description /> {videos && <VideoPreviews videos={videos} />} </div> - <RenderPrimaryStory - primaryStory={primaryStory as unknown as PrimaryStory} - skipPrimaryStory={skipPrimaryStory} - /> - <RenderArgsTable - story={primaryStory as unknown as PrimaryStory} - hideArgsTable={hideArgsTable} - showSlotsApi={argTable.slotsApi} - showNativePropsApi={argTable.nativePropsApi} - /> - <Stories /> + {renderPrimaryStory({ primaryStory, skipPrimaryStory })} + {renderArgsTable({ + story: primaryStory, + hideArgsTable, + showSlotsApi: argTable.slotsApi, + showNativePropsApi: argTable.nativePropsApi, + })} + {renderStories({ stories: stories.slice(1), skipPrimaryStory })} </div> {showTableOfContents && ( <div className={styles.toc}> diff --git a/packages/react-components/react-storybook-addon/src/docs/index.ts b/packages/react-components/react-storybook-addon/src/docs/index.ts index 3036e3e2c1570..b4bfab6470bd8 100644 --- a/packages/react-components/react-storybook-addon/src/docs/index.ts +++ b/packages/react-components/react-storybook-addon/src/docs/index.ts @@ -1,4 +1,5 @@ export { FluentCanvas } from './FluentCanvas'; export { FluentDocsContainer } from './FluentDocsContainer'; +export type { FluentDocsPageProps } from './FluentDocsPage'; export { FluentDocsPage } from './FluentDocsPage'; export { FluentStory } from './FluentStory'; diff --git a/packages/react-components/react-storybook-addon/src/index.ts b/packages/react-components/react-storybook-addon/src/index.ts index b1c71cf2823b0..d15dce947e768 100644 --- a/packages/react-components/react-storybook-addon/src/index.ts +++ b/packages/react-components/react-storybook-addon/src/index.ts @@ -3,4 +3,5 @@ export type { ThemeIds } from './theme'; export { themes } from './theme'; export { DIR_ID, THEME_ID } from './constants'; export { parameters } from './hooks'; -export { FluentCanvas, FluentStory } from './docs'; +export { FluentCanvas, FluentDocsPage, FluentStory } from './docs'; +export type { FluentDocsPageProps } from './docs';