diff --git a/MIGRATION.md b/MIGRATION.md index 2141d4dde078..34c0ca87c685 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -2,6 +2,7 @@ - [From version 8.0 to 8.1.0](#from-version-80-to-810) - [Subtitle block and `parameters.componentSubtitle`](#subtitle-block-and-parameterscomponentsubtitle) + - [Title block](#title-block) - [From version 7.x to 8.0.0](#from-version-7x-to-800) - [Portable stories](#portable-stories) - [Project annotations are now merged instead of overwritten in composeStory](#project-annotations-are-now-merged-instead-of-overwritten-in-composestory) @@ -413,6 +414,12 @@ The `Subtitle` block now accepts an `of` prop, which can be a reference to a CSF `parameters.componentSubtitle` has been deprecated to be consistent with other parameters related to autodocs, instead use `parameters.docs.subtitle`. +##### Title block + +The `Title` block now accepts an `of` prop, which can be a reference to a CSF file or a default export (meta). + +It still accepts being passed `children`. + ## From version 7.x to 8.0.0 ### Portable stories diff --git a/code/ui/blocks/src/blocks/Title.stories.tsx b/code/ui/blocks/src/blocks/Title.stories.tsx new file mode 100644 index 000000000000..a75b6ef72d98 --- /dev/null +++ b/code/ui/blocks/src/blocks/Title.stories.tsx @@ -0,0 +1,55 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Title } from './Title'; +import * as DefaultButtonStories from '../examples/Button.stories'; + +const meta: Meta = { + component: Title, + title: 'Blocks/Title', + parameters: { + controls: { + include: [], + hideNoControlsWarning: true, + }, + // workaround for https://github.com/storybookjs/storybook/issues/20505 + docs: { source: { type: 'code' } }, + attached: false, + docsStyles: true, + }, +}; +export default meta; + +type Story = StoryObj; + +export const OfCSFFile: Story = { + args: { + of: DefaultButtonStories, + }, + parameters: { relativeCsfPaths: ['../examples/Button.stories'] }, +}; + +export const OfMeta: Story = { + args: { + of: DefaultButtonStories, + }, + parameters: { relativeCsfPaths: ['../examples/Button.stories'] }, +}; + +export const OfStringMetaAttached: Story = { + name: 'Of attached "meta"', + args: { + of: 'meta', + }, + parameters: { relativeCsfPaths: ['../examples/Button.stories'], attached: true }, +}; + +export const Children: Story = { + args: { + children: 'Title as children', + }, + parameters: { relativeCsfPaths: ['../examples/Button.stories'], attached: false }, +}; + +export const DefaultAttached: Story = { + args: {}, + parameters: { relativeCsfPaths: ['../examples/Button.stories'], attached: true }, +}; diff --git a/code/ui/blocks/src/blocks/Title.tsx b/code/ui/blocks/src/blocks/Title.tsx index 1f52fb2cc179..55b85ebad717 100644 --- a/code/ui/blocks/src/blocks/Title.tsx +++ b/code/ui/blocks/src/blocks/Title.tsx @@ -1,10 +1,20 @@ import type { ComponentTitle } from '@storybook/types'; import type { FunctionComponent, ReactNode } from 'react'; -import React, { useContext } from 'react'; +import React from 'react'; import { Title as PureTitle } from '../components'; -import { DocsContext } from './DocsContext'; +import type { Of } from './useOf'; +import { useOf } from './useOf'; interface TitleProps { + /** + * Specify where to get the title from. Must be a CSF file's default export. + * If not specified, the title will be read from children, or extracted from the meta of the attached CSF file. + */ + of?: Of; + + /** + * Specify content to display as the title. + */ children?: ReactNode; } @@ -12,12 +22,27 @@ const STORY_KIND_PATH_SEPARATOR = /\s*\/\s*/; export const extractTitle = (title: ComponentTitle) => { const groups = title.trim().split(STORY_KIND_PATH_SEPARATOR); - return (groups && groups[groups.length - 1]) || title; + return groups?.[groups?.length - 1] || title; }; -export const Title: FunctionComponent = ({ children }) => { - const context = useContext(DocsContext); - const content = children || extractTitle(context.storyById().title); +export const Title: FunctionComponent = (props) => { + const { children, of } = props; + + if ('of' in props && of === undefined) { + throw new Error('Unexpected `of={undefined}`, did you mistype a CSF file reference?'); + } + + let preparedMeta; + try { + preparedMeta = useOf(of || 'meta', ['meta']).preparedMeta; + } catch (error) { + if (children && !error.message.includes('did you forget to use ?')) { + // ignore error about unattached CSF since we can still render children + throw error; + } + } + + const content = children || extractTitle(preparedMeta.title); return content ? {content} : null; }; diff --git a/docs/api/doc-block-title.md b/docs/api/doc-block-title.md index 886d19075386..0427e763a390 100644 --- a/docs/api/doc-block-title.md +++ b/docs/api/doc-block-title.md @@ -31,3 +31,9 @@ import { Title } from '@storybook/blocks'; Type: `JSX.Element | string` Provides the content. Falls back to value of `title` in an [attached](./doc-block-meta.md#attached-vs-unattached) CSF file (or value derived from [autotitle](../configure/sidebar-and-urls.md#csf-30-auto-titles)), trimmed to the last segment. For example, if the title value is `'path/to/components/Button'`, the default content is `'Button'`. + +### `of` + +Type: CSF file exports + +Specifies which meta's title is displayed.