Skip to content

Commit 10386ac

Browse files
authored
Merge pull request storybookjs#34496 from NYCU-Chung/fix/docs-blocks-custom-mdx
Addon Docs: Fix Primary and Controls blocks not rendering in custom MDX pages
2 parents d25d870 + 3f04a59 commit 10386ac

13 files changed

Lines changed: 106 additions & 17 deletions

File tree

code/addons/docs/src/blocks/blocks/external/ExternalDocsContext.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ export class ExternalDocsContext<TRenderer extends Renderer> extends DocsContext
1616
renderStoryToElement as DocsContextProps<TRenderer>['renderStoryToElement'],
1717
[]
1818
);
19+
20+
// External docs are always an author-supplied page (`<ExternalDocs>` passes its children as the
21+
// docs page), so `<Primary />` / `<Controls />` should respect the author's story selection
22+
// rather than filtering to `autodocs`-tagged stories.
23+
this.filterByAutodocs = false;
1924
}
2025

2126
referenceMeta = (metaExports: ModuleExports, attach: boolean) => {

code/addons/docs/src/blocks/blocks/usePrimaryStory.test.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,21 @@ const stories: Record<string, Partial<PreparedStory>> = {
1919
story4: { name: 'Story Four', tags: [] },
2020
};
2121

22-
const createMockContext = (storyList: PreparedStory[]) => ({
22+
const createMockContext = (
23+
storyList: PreparedStory[],
24+
overrides: Partial<DocsContextProps> = {}
25+
) => ({
2326
componentStories: vi.fn(() => storyList),
27+
filterByAutodocs: true,
28+
...overrides,
2429
});
2530

2631
const Wrapper: FC<PropsWithChildren<{ context: Partial<DocsContextProps> }>> = ({
2732
children,
2833
context,
2934
}) => <DocsContext.Provider value={context as DocsContextProps}>{children}</DocsContext.Provider>;
3035

31-
describe('usePrimaryStory', () => {
36+
describe('usePrimaryStory — autodocs page (filterByAutodocs: true)', () => {
3237
it('ignores !autodocs stories', () => {
3338
const mockContext = createMockContext([
3439
stories.story1,
@@ -49,7 +54,7 @@ describe('usePrimaryStory', () => {
4954
expect(result.current?.name).toBe('Story Two');
5055
});
5156

52-
it('returns undefined if no story has "autodocs" tag', () => {
57+
it('returns undefined when no story has the autodocs tag', () => {
5358
const mockContext = createMockContext([stories.story1, stories.story4] as PreparedStory[]);
5459
const { result } = renderHook(() => usePrimaryStory(), {
5560
wrapper: ({ children }) => <Wrapper context={mockContext}>{children}</Wrapper>,
@@ -65,3 +70,16 @@ describe('usePrimaryStory', () => {
6570
expect(result.current).toBeUndefined();
6671
});
6772
});
73+
74+
describe('usePrimaryStory — MDX / custom page (filterByAutodocs: false)', () => {
75+
it('returns the first story regardless of autodocs tag', () => {
76+
const mockContext = createMockContext(
77+
[stories.story1, stories.story2, stories.story3] as PreparedStory[],
78+
{ filterByAutodocs: false }
79+
);
80+
const { result } = renderHook(() => usePrimaryStory(), {
81+
wrapper: ({ children }) => <Wrapper context={mockContext}>{children}</Wrapper>,
82+
});
83+
expect(result.current?.name).toBe('Story One');
84+
});
85+
});

code/addons/docs/src/blocks/blocks/usePrimaryStory.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ import type { PreparedStory } from 'storybook/internal/types';
66
import { DocsContext } from './DocsContext';
77

88
/**
9-
* A hook to get the primary story for the current component's doc page. It defines the primary
10-
* story as the first story that includes the 'autodocs' tag
9+
* Returns the primary story for the current docs page. Autodocs pages pick the first story tagged
10+
* `autodocs`; MDX pages pick the first story regardless of tag (driven by
11+
* `DocsContext.filterByAutodocs`, set by the docs render based on the entry type).
1112
*/
1213
export const usePrimaryStory = (): PreparedStory | undefined => {
1314
const context = useContext(DocsContext);
1415
const stories = context.componentStories();
16+
if (context.filterByAutodocs === false) {
17+
return stories[0];
18+
}
1519
return stories.find((story) => story.tags.includes(Tag.AUTODOCS));
1620
};

code/core/src/core-server/utils/StoryIndexGenerator.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { resolveImport, supportedExtensions } from '../../common/index.ts';
3333
import { userOrAutoTitleFromSpecifier } from '../../preview-api/modules/store/autoTitle.ts';
3434
import { sortStoriesV7 } from '../../preview-api/modules/store/sortStories.ts';
3535
import { Tag } from '../../shared/constants/tags.ts';
36+
import { isMdxEntry } from '../../shared/utils/story-index-filters.ts';
3637
import { IndexingError, MultipleIndexingError } from './IndexingError.ts';
3738
import { autoName } from './autoName.ts';
3839
import { analyzeMdx } from './analyze-mdx.ts';
@@ -65,11 +66,6 @@ export type StoryIndexGeneratorOptions = {
6566
build?: StorybookConfigRaw['build'];
6667
};
6768

68-
/** Was this docs entry generated by a .mdx file? (see discussion below) */
69-
export function isMdxEntry({ tags }: DocsIndexEntry) {
70-
return tags?.includes(Tag.UNATTACHED_MDX) || tags?.includes(Tag.ATTACHED_MDX);
71-
}
72-
7369
const makeAbsolute = (otherImport: Path, normalizedPath: Path, workingDir: Path) =>
7470
otherImport.startsWith('.')
7571
? slash(resolve(workingDir, normalizeStoryPath(join(dirname(normalizedPath), otherImport))))

code/core/src/core-server/utils/summarizeIndex.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { isExampleStoryId } from 'storybook/internal/telemetry';
22
import type { IndexEntry, StoryIndex } from 'storybook/internal/types';
33

44
import { Tag } from '../../shared/constants/tags.ts';
5-
import { isMdxEntry } from './StoryIndexGenerator.ts';
5+
import { isMdxEntry } from '../../shared/utils/story-index-filters.ts';
66

77
const PAGE_REGEX = /(page|screen)/i;
88

code/core/src/preview-api/modules/preview-web/PreviewWithSelection.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@ import {
2222
MdxFileWithNoCsfReferencesError,
2323
NoStoryMatchError,
2424
} from 'storybook/internal/preview-errors';
25-
import type { DocsIndexEntry, StoryIndex } from 'storybook/internal/types';
25+
import type { StoryIndex } from 'storybook/internal/types';
2626
import type { Args, Globals, Renderer, StoryId, ViewMode } from 'storybook/internal/types';
2727
import type { ModuleImportFn, ProjectAnnotations } from 'storybook/internal/types';
2828

2929
import invariant from 'tiny-invariant';
3030

3131
import { Tag } from '../../../shared/constants/tags.ts';
32+
import { isMdxEntry } from '../../../shared/utils/story-index-filters.ts';
3233
import type { StorySpecifier } from '../store/StoryIndexStore.ts';
3334
import type { MaybePromise } from './Preview.tsx';
3435
import { Preview } from './Preview.tsx';
@@ -46,11 +47,6 @@ function focusInInput(event: Event) {
4647
return /input|textarea/i.test(target.tagName) || target.getAttribute('contenteditable') !== null;
4748
}
4849

49-
/** Was this docs entry generated by a .mdx file? (see discussion below) */
50-
export function isMdxEntry({ tags }: DocsIndexEntry) {
51-
return tags?.includes(Tag.UNATTACHED_MDX) || tags?.includes(Tag.ATTACHED_MDX);
52-
}
53-
5450
type PossibleRender<TRenderer extends Renderer> =
5551
| StoryRender<TRenderer>
5652
| CsfDocsRender<TRenderer>

code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ export class DocsContext<TRenderer extends Renderer> implements DocsContextProps
3232

3333
private primaryStory?: PreparedStory<TRenderer>;
3434

35+
// Set by the docs render (autodocs vs MDX) so `usePrimaryStory` knows whether to filter the
36+
// CSF file's stories to `autodocs`-tagged ones. See `DocsContextProps.filterByAutodocs`.
37+
public filterByAutodocs?: boolean;
38+
3539
constructor(
3640
public channel: Channel,
3741
protected store: StoryStore<TRenderer>,

code/core/src/preview-api/modules/preview-web/render/CsfDocsRender.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,29 @@ it('attached immediately', async () => {
7777

7878
expect(context.storyById()).toEqual(story);
7979
});
80+
81+
it('sets filterByAutodocs to true for autodocs pages', async () => {
82+
const { story, csfFile, moduleExports } = csfFileParts();
83+
84+
const store = {
85+
loadEntry: () => ({
86+
entryExports: moduleExports,
87+
csfFiles: [],
88+
}),
89+
processCSFFileWithCache: () => csfFile,
90+
componentStoriesFromCSFFile: () => [story],
91+
storyFromCSFFile: () => story,
92+
} as unknown as StoryStore<Renderer>;
93+
94+
const render = new CsfDocsRender(
95+
new Channel({}),
96+
store,
97+
entry,
98+
{} as RenderContextCallbacks<Renderer>
99+
);
100+
await render.prepare();
101+
102+
const context = render.docsContext(vi.fn());
103+
104+
expect(context.filterByAutodocs).toBe(true);
105+
});

code/core/src/preview-api/modules/preview-web/render/CsfDocsRender.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { CSFFile, PreparedStory } from 'storybook/internal/types';
55
import type { IndexEntry } from 'storybook/internal/types';
66
import type { RenderContextCallbacks } from 'storybook/internal/types';
77

8+
import { isMdxEntry } from '../../../../shared/utils/story-index-filters.ts';
89
import type { StoryStore } from '../../../store.ts';
910
import { DocsContext } from '../docs-context/DocsContext.ts';
1011
import type { DocsContextProps } from '../docs-context/DocsContextProps.ts';
@@ -110,6 +111,12 @@ export class CsfDocsRender<TRenderer extends Renderer> implements Render<TRender
110111
// - When you create two CSF files that both reference the same title, they are combined into
111112
// a single CSF docs entry with a `storiesImport` defined.
112113
this.csfFiles.forEach((csfFile) => docsContext.attachCSFFile(csfFile));
114+
115+
// Autodocs pages filter the CSF file's stories down to `autodocs`-tagged ones when picking
116+
// the `<Primary />` story; a custom `docs.page` template on an autodocs entry does not change
117+
// that (the entry is still not an MDX entry).
118+
docsContext.filterByAutodocs = !isMdxEntry(this.entry);
119+
113120
return docsContext;
114121
}
115122

code/core/src/preview-api/modules/preview-web/render/MdxDocsRender.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,20 @@ describe('attaching', () => {
113113

114114
expect(context.storyById()).toEqual(story);
115115
});
116+
117+
it('sets filterByAutodocs to false for MDX pages', async () => {
118+
const render = new MdxDocsRender(
119+
new Channel({}),
120+
store,
121+
attachedEntry,
122+
{} as RenderContextCallbacks<Renderer>
123+
);
124+
await render.prepare();
125+
126+
const context = render.docsContext(vi.fn());
127+
128+
expect(context.filterByAutodocs).toBe(false);
129+
});
116130
});
117131

118132
describe('docs parameters', () => {

0 commit comments

Comments
 (0)