diff --git a/code/addons/docs/src/blocks/blocks/Controls.tsx b/code/addons/docs/src/blocks/blocks/Controls.tsx index da5036b969a0..5f7e9eee69fa 100644 --- a/code/addons/docs/src/blocks/blocks/Controls.tsx +++ b/code/addons/docs/src/blocks/blocks/Controls.tsx @@ -14,6 +14,7 @@ import { ArgsTableError, ArgsTable as PureArgsTable, TabbedArgsTable } from '../ import { DocsContext } from './DocsContext'; import { useArgs } from './useArgs'; import { useGlobals } from './useGlobals'; +import { usePrimaryStory } from './usePrimaryStory'; import { getComponentName } from './utils'; type ControlsParameters = { @@ -39,12 +40,15 @@ function extractComponentArgTypes( export const Controls: FC = (props) => { const { of } = props; - if ('of' in props && of === undefined) { - throw new Error('Unexpected `of={undefined}`, did you mistype a CSF file reference?'); + const context = useContext(DocsContext); + const primaryStory = usePrimaryStory(); + + const story = of ? context.resolveOf(of, ['story']).story : primaryStory; + + if (!story) { + return null; } - const context = useContext(DocsContext); - const { story } = context.resolveOf(of || 'story', ['story']); const { parameters, argTypes, component, subcomponents } = story; const controlsParameters = parameters.docs?.controls || ({} as ControlsParameters); diff --git a/code/addons/docs/src/blocks/blocks/Primary.stories.tsx b/code/addons/docs/src/blocks/blocks/Primary.stories.tsx index 39b9123ea726..66b1108984a9 100644 --- a/code/addons/docs/src/blocks/blocks/Primary.stories.tsx +++ b/code/addons/docs/src/blocks/blocks/Primary.stories.tsx @@ -1,6 +1,8 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import * as DefaultButtonStories from '../examples/Button.stories'; +import * as ButtonNoAutodocsStories from '../examples/ButtonNoAutodocs.stories'; +import * as ButtonSomeAutodocsStories from '../examples/ButtonSomeAutodocs.stories'; import * as StoriesParametersStories from '../examples/StoriesParameters.stories'; import { Primary } from './Primary'; @@ -59,3 +61,23 @@ export const WithoutToolbarOfStringMetaAttached: Story = { }, parameters: { relativeCsfPaths: ['../examples/StoriesParameters.stories'] }, }; + +export const NoAutodocsExample: Story = { + name: 'Button (No Autodocs)', + args: { + of: ButtonNoAutodocsStories, + }, + parameters: { + relativeCsfPaths: ['../examples/ButtonNoAutodocs.stories'], + }, +}; + +export const SomeAutodocsExample: Story = { + name: 'Button (Some Autodocs)', + args: { + of: ButtonSomeAutodocsStories, + }, + parameters: { + relativeCsfPaths: ['../examples/ButtonSomeAutodocs.stories'], + }, +}; diff --git a/code/addons/docs/src/blocks/blocks/Primary.tsx b/code/addons/docs/src/blocks/blocks/Primary.tsx index 588e0428131b..4c22ad1eccd1 100644 --- a/code/addons/docs/src/blocks/blocks/Primary.tsx +++ b/code/addons/docs/src/blocks/blocks/Primary.tsx @@ -1,26 +1,11 @@ import type { FC } from 'react'; -import React, { useContext } from 'react'; +import React from 'react'; -import { DocsContext } from './DocsContext'; import { DocsStory } from './DocsStory'; -import type { Of } from './useOf'; -import { useOf } from './useOf'; +import { usePrimaryStory } from './usePrimaryStory'; -interface PrimaryProps { - /** Specify where to get the primary story from. */ - of?: Of; -} - -export const Primary: FC = (props) => { - const { of } = props; - if ('of' in props && of === undefined) { - throw new Error('Unexpected `of={undefined}`, did you mistype a CSF file reference?'); - } - - const { csfFile } = useOf(of || 'meta', ['meta']); - const context = useContext(DocsContext); - - const primaryStory = context.componentStoriesFromCSFFile(csfFile)[0]; +export const Primary: FC = () => { + const primaryStory = usePrimaryStory(); return primaryStory ? ( diff --git a/code/addons/docs/src/blocks/blocks/usePrimaryStory.test.tsx b/code/addons/docs/src/blocks/blocks/usePrimaryStory.test.tsx new file mode 100644 index 000000000000..aa92719b802a --- /dev/null +++ b/code/addons/docs/src/blocks/blocks/usePrimaryStory.test.tsx @@ -0,0 +1,66 @@ +// @vitest-environment happy-dom +import { renderHook } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import React from 'react'; +import type { FC, PropsWithChildren } from 'react'; + +import type { PreparedStory } from 'storybook/internal/types'; + +import type { DocsContextProps } from './DocsContext'; +import { DocsContext } from './DocsContext'; +import { usePrimaryStory } from './usePrimaryStory'; + +const stories: Record> = { + story1: { name: 'Story One', tags: ['!autodocs'] }, + story2: { name: 'Story Two', tags: ['autodocs'] }, + story3: { name: 'Story Three', tags: ['autodocs'] }, + story4: { name: 'Story Four', tags: [] }, +}; + +const createMockContext = (storyList: PreparedStory[]) => ({ + componentStories: vi.fn(() => storyList), +}); + +const Wrapper: FC }>> = ({ + children, + context, +}) => {children}; + +describe('usePrimaryStory', () => { + it('ignores !autodocs stories', () => { + const mockContext = createMockContext([ + stories.story1, + stories.story2, + stories.story3, + ] as PreparedStory[]); + const { result } = renderHook(() => usePrimaryStory(), { + wrapper: ({ children }) => {children}, + }); + expect(result.current?.name).toBe('Story Two'); + }); + + it('selects the first autodocs story', () => { + const mockContext = createMockContext([stories.story2, stories.story3] as PreparedStory[]); + const { result } = renderHook(() => usePrimaryStory(), { + wrapper: ({ children }) => {children}, + }); + expect(result.current?.name).toBe('Story Two'); + }); + + it('returns undefined if no story has "autodocs" tag', () => { + const mockContext = createMockContext([stories.story1, stories.story4] as PreparedStory[]); + const { result } = renderHook(() => usePrimaryStory(), { + wrapper: ({ children }) => {children}, + }); + expect(result.current).toBeUndefined(); + }); + + it('returns undefined for empty story list', () => { + const mockContext = createMockContext([]); + const { result } = renderHook(() => usePrimaryStory(), { + wrapper: ({ children }) => {children}, + }); + expect(result.current).toBeUndefined(); + }); +}); diff --git a/code/addons/docs/src/blocks/blocks/usePrimaryStory.ts b/code/addons/docs/src/blocks/blocks/usePrimaryStory.ts new file mode 100644 index 000000000000..fd177287023e --- /dev/null +++ b/code/addons/docs/src/blocks/blocks/usePrimaryStory.ts @@ -0,0 +1,15 @@ +import { useContext } from 'react'; + +import type { PreparedStory } from 'storybook/internal/types'; + +import { DocsContext } from './DocsContext'; + +/** + * A hook to get the primary story for the current component's doc page. It defines the primary + * story as the first story that includes the 'autodocs' tag + */ +export const usePrimaryStory = (): PreparedStory | undefined => { + const context = useContext(DocsContext); + const stories = context.componentStories(); + return stories.find((story) => story.tags.includes('autodocs')); +}; diff --git a/code/addons/docs/src/blocks/examples/StoriesParameters.stories.tsx b/code/addons/docs/src/blocks/examples/StoriesParameters.stories.tsx index 74b397ee4b69..b986320df76d 100644 --- a/code/addons/docs/src/blocks/examples/StoriesParameters.stories.tsx +++ b/code/addons/docs/src/blocks/examples/StoriesParameters.stories.tsx @@ -5,6 +5,7 @@ import { EmptyExample } from './EmptyExample'; const meta = { title: 'examples/Stories for the Stories and Primary Block', component: EmptyExample, + tags: ['autodocs'], } satisfies Meta; export default meta;