diff --git a/code/addons/docs/src/blocks/blocks/Stories.stories.tsx b/code/addons/docs/src/blocks/blocks/Stories.stories.tsx index 0164ad82e7d8..d30dd8a3fc57 100644 --- a/code/addons/docs/src/blocks/blocks/Stories.stories.tsx +++ b/code/addons/docs/src/blocks/blocks/Stories.stories.tsx @@ -1,5 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; +import * as DefaultButtonStories from '../examples/Button.stories'; +import * as StoriesBlockParametersStories from '../examples/StoriesBlockParameters.stories'; import { Stories } from './Stories'; const meta = { @@ -28,6 +30,28 @@ export const WithoutPrimary: Story = { relativeCsfPaths: ['../examples/Button.stories'], }, }; +export const WithoutPrimaryStory: Story = { + args: { includePrimaryStory: false }, + parameters: { + relativeCsfPaths: ['../examples/Button.stories'], + }, +}; +export const OfCSFFile: Story = { + args: { + of: DefaultButtonStories, + }, + parameters: { + relativeCsfPaths: ['../examples/Button.stories'], + }, +}; +export const WithDocsStoriesParameters: Story = { + args: { + of: StoriesBlockParametersStories, + }, + parameters: { + relativeCsfPaths: ['../examples/StoriesBlockParameters.stories'], + }, +}; export const DifferentToolbars: Story = { parameters: { relativeCsfPaths: ['../examples/StoriesParameters.stories'], diff --git a/code/addons/docs/src/blocks/blocks/Stories.tsx b/code/addons/docs/src/blocks/blocks/Stories.tsx index 555163f539e6..87364d7d4f92 100644 --- a/code/addons/docs/src/blocks/blocks/Stories.tsx +++ b/code/addons/docs/src/blocks/blocks/Stories.tsx @@ -2,17 +2,26 @@ import type { FC, ReactElement } from 'react'; import React, { useContext } from 'react'; import { Tag } from 'storybook/internal/preview-api'; +import { InvalidBlockOfPropError, NoMetaAttachedError } from 'storybook/internal/preview-errors'; +import type { ResolvedModuleExportFromType } from 'storybook/internal/types'; import { styled } from 'storybook/theming'; import { DocsContext } from './DocsContext'; import { DocsStory } from './DocsStory'; import { Heading } from './Heading'; +import type { Of } from './useOf.ts'; +import { useOf } from './useOf.ts'; import { withMdxComponentOverride } from './with-mdx-component-override'; interface StoriesProps { + /** Specify which CSF file's stories are displayed. */ + of?: Of; title?: ReactElement | string; + /** @deprecated Use `includePrimaryStory` instead. */ includePrimary?: boolean; + includePrimaryStory?: boolean; + forceInitialArgs?: boolean; } const StyledHeading: typeof Heading = styled(Heading)(({ theme }) => ({ @@ -31,10 +40,33 @@ const StyledHeading: typeof Heading = styled(Heading)(({ theme }) => ({ }, })); -const StoriesImpl: FC = ({ title = 'Stories', includePrimary = true }) => { - const { componentStories, projectAnnotations, getStoryContext } = useContext(DocsContext); +const StoriesImpl: FC = (props) => { + const { of, includePrimary, includePrimaryStory } = props; + const context = useContext(DocsContext); + const { componentStories, componentStoriesFromCSFFile, projectAnnotations, getStoryContext } = + context; - let stories = componentStories(); + if ('of' in props && of === undefined) { + throw new InvalidBlockOfPropError(); + } + + let resolvedOf: ResolvedModuleExportFromType<'meta'> | undefined; + try { + resolvedOf = useOf(of || 'meta', ['meta']); + } catch (error: unknown) { + if (of || !(error instanceof NoMetaAttachedError)) { + throw error; + } + } + + const docsStoriesParameters = resolvedOf?.preparedMeta.parameters.docs?.stories || {}; + const title = props.title ?? docsStoriesParameters.title ?? 'Stories'; + const showPrimaryStory = + includePrimaryStory ?? includePrimary ?? docsStoriesParameters.includePrimaryStory ?? true; + const forceInitialArgs = props.forceInitialArgs ?? docsStoriesParameters.forceInitialArgs ?? true; + + let stories = + of && resolvedOf ? componentStoriesFromCSFFile(resolvedOf.csfFile) : componentStories(); const { stories: { filter } = { filter: undefined } } = projectAnnotations.parameters?.docs || {}; if (filter) { stories = stories.filter((story) => filter(story, getStoryContext(story))); @@ -53,7 +85,7 @@ const StoriesImpl: FC = ({ title = 'Stories', includePrimary = tru stories = stories.filter((story) => story.tags?.includes(Tag.AUTODOCS) && !story.usesMount); } - if (!includePrimary) { + if (!showPrimaryStory) { stories = stories.slice(1); } @@ -65,7 +97,14 @@ const StoriesImpl: FC = ({ title = 'Stories', includePrimary = tru {typeof title === 'string' ? {title} : title} {stories.map( (story) => - story && + story && ( + + ) )} ); diff --git a/code/addons/docs/src/blocks/examples/StoriesBlockParameters.stories.tsx b/code/addons/docs/src/blocks/examples/StoriesBlockParameters.stories.tsx new file mode 100644 index 000000000000..fa2a07b6068d --- /dev/null +++ b/code/addons/docs/src/blocks/examples/StoriesBlockParameters.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { EmptyExample } from './EmptyExample.tsx'; + +const meta = { + title: 'examples/Stories block parameters', + component: EmptyExample, + tags: ['autodocs'], + parameters: { + docs: { + stories: { + title: 'Configured stories', + includePrimaryStory: false, + forceInitialArgs: false, + }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Primary: Story = {}; +export const Secondary: Story = {}; +export const Tertiary: Story = {}; diff --git a/code/addons/docs/src/types.ts b/code/addons/docs/src/types.ts index 71768a3e34b4..d1bedff924d8 100644 --- a/code/addons/docs/src/types.ts +++ b/code/addons/docs/src/types.ts @@ -1,6 +1,7 @@ import type { ComponentType } from 'react'; -import type { ModuleExport, ModuleExports } from 'storybook/internal/types'; +import type { Args, Renderer, StoryContext } from 'storybook/internal/csf'; +import type { ModuleExport, ModuleExports, PreparedStory } from 'storybook/internal/types'; import type { ThemeVars } from 'storybook/theming'; @@ -31,6 +32,17 @@ type StoryBlockParameters = { of: ModuleExport; }; +type StoriesBlockParameters = { + /** Filter which stories are rendered by the Stories block */ + filter?: (story: PreparedStory, context: StoryContext) => boolean; + /** When rendering stories, whether the first story should be included */ + includePrimaryStory?: boolean; + /** Whether to force initial args when rendering stories */ + forceInitialArgs?: boolean; + /** The title displayed above the stories list */ + title?: string; +}; + type ControlsBlockParameters = { /** Exclude specific properties from the Controls panel */ exclude?: string[] | RegExp; @@ -221,6 +233,13 @@ export interface DocsParameters { */ story?: Partial; + /** + * Stories configuration + * + * @see https://storybook.js.org/docs/api/doc-blocks/doc-block-stories + */ + stories?: StoriesBlockParameters; + /** * The subtitle displayed when shown in docs page * diff --git a/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.test.ts b/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.test.ts index b26c3d165d1a..b702c36386d2 100644 --- a/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.test.ts +++ b/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; import { Channel } from 'storybook/internal/channels'; +import { NoMetaAttachedError } from 'storybook/internal/preview-errors'; import type { CSFFile, Renderer } from 'storybook/internal/types'; import type { StoryStore } from '../../store/index.ts'; @@ -310,7 +311,7 @@ describe('resolveOf', () => { }); it('throws for attached CSF file', () => { - expect(() => context.resolveOf('meta')).toThrow('No CSF file attached'); + expect(() => context.resolveOf('meta')).toThrow(NoMetaAttachedError); }); it('throws for attached component', () => { diff --git a/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.ts b/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.ts index 803b9aca0fdd..4c2baf4b2158 100644 --- a/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.ts +++ b/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.ts @@ -14,6 +14,7 @@ import type { import { dedent } from 'ts-dedent'; +import { NoMetaAttachedError } from '../../../../preview-errors.ts'; import { type StoryStore } from '../../store/index.ts'; import type { DocsContextProps } from './DocsContextProps.ts'; @@ -131,9 +132,7 @@ export class DocsContext implements DocsContextProps } if (this.attachedCSFFiles.size === 0) { - throw new Error( - `No CSF file attached to this docs file, did you forget to use ?` - ); + throw new NoMetaAttachedError(); } const firstAttachedCSFFile = Array.from(this.attachedCSFFiles)[0]; diff --git a/code/core/src/preview-errors.ts b/code/core/src/preview-errors.ts index 48b485b2cce8..a7c4eb8be3d7 100644 --- a/code/core/src/preview-errors.ts +++ b/code/core/src/preview-errors.ts @@ -373,6 +373,17 @@ export class InvalidBlockOfPropError extends StorybookError { } } +export class NoMetaAttachedError extends StorybookError { + constructor() { + super({ + name: 'NoMetaAttachedError', + category: Category.BLOCKS, + code: 2, + message: 'No CSF file attached to this docs file, did you forget to use ?', + }); + } +} + export class UnsupportedViewportDimensionError extends StorybookError { constructor(public data: { dimension: string; value: string }) { super({