Skip to content
Open
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
12 changes: 8 additions & 4 deletions code/addons/docs/src/blocks/blocks/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -39,12 +40,15 @@ function extractComponentArgTypes(

export const Controls: FC<ControlsProps> = (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);

Expand Down
22 changes: 22 additions & 0 deletions code/addons/docs/src/blocks/blocks/Primary.stories.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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'],
},
};
23 changes: 4 additions & 19 deletions code/addons/docs/src/blocks/blocks/Primary.tsx
Original file line number Diff line number Diff line change
@@ -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<PrimaryProps> = (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 ? (
<DocsStory of={primaryStory.moduleExport} expanded={false} __primary withToolbar />
Expand Down
66 changes: 66 additions & 0 deletions code/addons/docs/src/blocks/blocks/usePrimaryStory.test.tsx
Original file line number Diff line number Diff line change
@@ -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<string, Partial<PreparedStory>> = {
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<PropsWithChildren<{ context: Partial<DocsContextProps> }>> = ({
children,
context,
}) => <DocsContext.Provider value={context as DocsContextProps}>{children}</DocsContext.Provider>;

describe('usePrimaryStory', () => {
it('ignores !autodocs stories', () => {
const mockContext = createMockContext([
stories.story1,
stories.story2,
stories.story3,
] as PreparedStory[]);
const { result } = renderHook(() => usePrimaryStory(), {
wrapper: ({ children }) => <Wrapper context={mockContext}>{children}</Wrapper>,
});
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 }) => <Wrapper context={mockContext}>{children}</Wrapper>,
});
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 }) => <Wrapper context={mockContext}>{children}</Wrapper>,
});
expect(result.current).toBeUndefined();
});

it('returns undefined for empty story list', () => {
const mockContext = createMockContext([]);
const { result } = renderHook(() => usePrimaryStory(), {
wrapper: ({ children }) => <Wrapper context={mockContext}>{children}</Wrapper>,
});
expect(result.current).toBeUndefined();
});
});
15 changes: 15 additions & 0 deletions code/addons/docs/src/blocks/blocks/usePrimaryStory.ts
Original file line number Diff line number Diff line change
@@ -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'));
};
Comment on lines +11 to +15
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 11, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

LGTM! The hook logic correctly addresses the bug.

The implementation correctly selects the first story with the 'autodocs' tag, which excludes stories marked with '!autodocs'. This should fix the issue where stories tagged with '!autodocs' were appearing as the primary preview when positioned first.

Verify the fix resolves the reported issue by testing against the reproduction case:


🏁 Script executed:

#!/bin/bash
# Description: Search for test files or stories that might cover the !autodocs tag behavior

# Look for existing tests related to autodocs tags
rg -n --type=ts --type=tsx -C3 '\!autodocs|autodocs.*tag'

# Find stories or test files that might need verification
fd -e story -e test.ts -e test.tsx | head -20

Length of output: 1067


Add unit tests for '!autodocs' exclusion
No existing tests cover scenarios where a story is tagged !autodocs. Add tests (e.g., code/addons/docs/src/blocks/blocks/usePrimaryStory.test.ts) to verify that:

  • Stories with the 'autodocs' tag are selected.
  • Stories with the '!autodocs' tag are excluded.

Copy link
Member

Choose a reason for hiding this comment

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

@ia319 hooks usually have their own test file. You should be able to mock the DocsContext and build tests that show

  • The hook ignores !autodocs stories
  • It takes the first story with autodocs

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof EmptyExample>;
export default meta;

Expand Down