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
24 changes: 24 additions & 0 deletions code/addons/docs/src/blocks/blocks/Stories.stories.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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'],
Expand Down
49 changes: 44 additions & 5 deletions code/addons/docs/src/blocks/blocks/Stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => ({
Expand All @@ -31,10 +40,33 @@ const StyledHeading: typeof Heading = styled(Heading)(({ theme }) => ({
},
}));

const StoriesImpl: FC<StoriesProps> = ({ title = 'Stories', includePrimary = true }) => {
const { componentStories, projectAnnotations, getStoryContext } = useContext(DocsContext);
const StoriesImpl: FC<StoriesProps> = (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)));
Expand All @@ -53,7 +85,7 @@ const StoriesImpl: FC<StoriesProps> = ({ title = 'Stories', includePrimary = tru
stories = stories.filter((story) => story.tags?.includes(Tag.AUTODOCS) && !story.usesMount);
}

if (!includePrimary) {
if (!showPrimaryStory) {
stories = stories.slice(1);
}

Expand All @@ -65,7 +97,14 @@ const StoriesImpl: FC<StoriesProps> = ({ title = 'Stories', includePrimary = tru
{typeof title === 'string' ? <StyledHeading>{title}</StyledHeading> : title}
{stories.map(
(story) =>
story && <DocsStory key={story.id} of={story.moduleExport} expanded __forceInitialArgs />
story && (
<DocsStory
key={story.id}
of={story.moduleExport}
expanded
__forceInitialArgs={forceInitialArgs}
/>
)
)}
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<typeof EmptyExample>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Primary: Story = {};
export const Secondary: Story = {};
export const Tertiary: Story = {};
21 changes: 20 additions & 1 deletion code/addons/docs/src/types.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -31,6 +32,17 @@ type StoryBlockParameters = {
of: ModuleExport;
};

type StoriesBlockParameters = {
/** Filter which stories are rendered by the Stories block */
filter?: (story: PreparedStory<Renderer>, context: StoryContext<Renderer, Args>) => 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;
Expand Down Expand Up @@ -221,6 +233,13 @@ export interface DocsParameters {
*/
story?: Partial<StoryBlockParameters>;

/**
* Stories configuration
*
* @see https://storybook.js.org/docs/api/doc-blocks/doc-block-stories
*/
stories?: StoriesBlockParameters;

/**
* The subtitle displayed when shown in docs page
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -310,7 +311,7 @@
});

it('throws for attached CSF file', () => {
expect(() => context.resolveOf('meta')).toThrow('No CSF file attached');
expect(() => context.resolveOf('meta')).toThrow(NoMetaAttachedError);

Check failure on line 314 in code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.test.ts

View workflow job for this annotation

GitHub Actions / Core Unit Tests, ubuntu-latest

[core] src/preview-api/modules/preview-web/docs-context/DocsContext.test.ts > resolveOf > unattached > throws for attached CSF file

AssertionError: expected error to be instance of NoMetaAttachedError - Expected: [Function NoMetaAttachedError] + Received: SB_BLOCKS_0002 (NoMetaAttachedError) { "message": "No CSF file attached to this docs file, did you forget to use <Meta of={} />?", "data": {}, "fromStorybook": true, "isHandledError": false, "subErrors": [], "category": "BLOCKS", "documentation": false, "code": 2, "_name": "NoMetaAttachedError", } ❯ src/preview-api/modules/preview-web/docs-context/DocsContext.test.ts:314:47
});

it('throws for attached component', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -131,9 +132,7 @@ export class DocsContext<TRenderer extends Renderer> implements DocsContextProps
}

if (this.attachedCSFFiles.size === 0) {
throw new Error(
`No CSF file attached to this docs file, did you forget to use <Meta of={} />?`
);
throw new NoMetaAttachedError();
}

const firstAttachedCSFFile = Array.from(this.attachedCSFFiles)[0];
Expand Down
11 changes: 11 additions & 0 deletions code/core/src/preview-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Meta of={} />?',
});
}
}

export class UnsupportedViewportDimensionError extends StorybookError {
constructor(public data: { dimension: string; value: string }) {
super({
Expand Down
Loading