Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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
* `<Primary>` / `<Stories>` 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 `<HeadlessSourcePanel>`. 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 `<Anchor>`, and replace
* Storybook's single-blob Source block with the tabbed `<HeadlessSourcePanel>`
* (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';

Expand Down Expand Up @@ -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 = () => (
<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>
);

/**
* Prefixes the preview disclaimer, then renders the primary story inside an
* `<Anchor>` 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 (
<div className="sb-unstyled headless-docs-page">
{/*
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).
*/}
<Title />
<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>
);
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
box-shadow: none !important;
border-radius: 0 !important;
right: auto !important;
margin-top: 0 !important;
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,6 @@ const AdditionalApiDocs: React.FC<{ children: React.ReactElement | React.ReactEl
</div>
);
};

const RenderArgsTable = ({
story,
hideArgsTable,
Expand All @@ -269,6 +268,7 @@ const RenderArgsTable = ({
return hideArgsTable ? null : (
<>
<div className={styles.additionalInfoWrapper}>
<ArgTypes of={component} />
{hasArgAsProp && (
<AdditionalApiDocs>
<p>
Expand Down Expand Up @@ -304,7 +304,6 @@ const RenderArgsTable = ({
</AdditionalApiDocs>
)}
</div>
<ArgTypes of={component} />
</>
);
};
Expand All @@ -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
Expand Down Expand Up @@ -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>
);
}
Expand Down Expand Up @@ -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}>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
3 changes: 2 additions & 1 deletion packages/react-components/react-storybook-addon/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Loading