diff --git a/CHANGELOG.md b/CHANGELOG.md index ef1c0ad4edbf..d93c3b996486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 8.0.9 + +- Addon-docs: Fix MDX compilation when using `@vitejs/plugin-react-swc` with plugins - [#26837](https://github.com/storybookjs/storybook/pull/26837), thanks @JReinhold! +- CSF: Fix typings for control and other properties of argTypes - [#26824](https://github.com/storybookjs/storybook/pull/26824), thanks @kasperpeulen! +- Controls: Fix crashing when docgen extraction partially fails - [#26862](https://github.com/storybookjs/storybook/pull/26862), thanks @yannbf! +- Doc Tools: Signature Type Error Handling - [#26774](https://github.com/storybookjs/storybook/pull/26774), thanks @ethriel3695! +- Next.js: Move sharp into optional deps - [#26787](https://github.com/storybookjs/storybook/pull/26787), thanks @shuta13! +- Nextjs: Support next 14.2 useParams functionality - [#26874](https://github.com/storybookjs/storybook/pull/26874), thanks @yannbf! +- Test: Remove chai as dependency of @storybook/test - [#26852](https://github.com/storybookjs/storybook/pull/26852), thanks @kasperpeulen! +- UI: Fix sidebar search hanging when selecting a story in touch mode - [#26807](https://github.com/storybookjs/storybook/pull/26807), thanks @JReinhold! + ## 8.0.8 - Automigration: Fix name of VTA addon - [#26816](https://github.com/storybookjs/storybook/pull/26816), thanks @valentinpalkovic! diff --git a/MIGRATION.md b/MIGRATION.md index eecaf94950a2..34c0ca87c685 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,5 +1,8 @@

Migration

+- [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) @@ -90,17 +93,17 @@ - [Tab addons cannot manually route, Tool addons can filter their visibility via tabId](#tab-addons-cannot-manually-route-tool-addons-can-filter-their-visibility-via-tabid) - [Removed `config` preset](#removed-config-preset-1) - [From version 7.5.0 to 7.6.0](#from-version-750-to-760) - - [CommonJS with Vite is deprecated](#commonjs-with-vite-is-deprecated) - - [Using implicit actions during rendering is deprecated](#using-implicit-actions-during-rendering-is-deprecated) - - [typescript.skipBabel deprecated](#typescriptskipbabel-deprecated) - - [Primary doc block accepts of prop](#primary-doc-block-accepts-of-prop) - - [Addons no longer need a peer dependency on React](#addons-no-longer-need-a-peer-dependency-on-react) + - [CommonJS with Vite is deprecated](#commonjs-with-vite-is-deprecated) + - [Using implicit actions during rendering is deprecated](#using-implicit-actions-during-rendering-is-deprecated) + - [typescript.skipBabel deprecated](#typescriptskipbabel-deprecated) + - [Primary doc block accepts of prop](#primary-doc-block-accepts-of-prop) + - [Addons no longer need a peer dependency on React](#addons-no-longer-need-a-peer-dependency-on-react) - [From version 7.4.0 to 7.5.0](#from-version-740-to-750) - - [`storyStoreV6` and `storiesOf` is deprecated](#storystorev6-and-storiesof-is-deprecated) - - [`storyIndexers` is replaced with `experimental_indexers`](#storyindexers-is-replaced-with-experimental_indexers) + - [`storyStoreV6` and `storiesOf` is deprecated](#storystorev6-and-storiesof-is-deprecated) + - [`storyIndexers` is replaced with `experimental_indexers`](#storyindexers-is-replaced-with-experimental_indexers) - [From version 7.0.0 to 7.2.0](#from-version-700-to-720) - - [Addon API is more type-strict](#addon-api-is-more-type-strict) - - [Addon-controls hideNoControlsWarning parameter is deprecated](#addon-controls-hidenocontrolswarning-parameter-is-deprecated) + - [Addon API is more type-strict](#addon-api-is-more-type-strict) + - [Addon-controls hideNoControlsWarning parameter is deprecated](#addon-controls-hidenocontrolswarning-parameter-is-deprecated) - [From version 6.5.x to 7.0.0](#from-version-65x-to-700) - [7.0 breaking changes](#70-breaking-changes) - [Dropped support for Node 15 and below](#dropped-support-for-node-15-and-below) @@ -126,7 +129,7 @@ - [Deploying build artifacts](#deploying-build-artifacts) - [Dropped support for file URLs](#dropped-support-for-file-urls) - [Serving with nginx](#serving-with-nginx) - - [Ignore story files from node_modules](#ignore-story-files-from-node_modules) + - [Ignore story files from node\_modules](#ignore-story-files-from-node_modules) - [7.0 Core changes](#70-core-changes) - [7.0 feature flags removed](#70-feature-flags-removed) - [Story context is prepared before for supporting fine grained updates](#story-context-is-prepared-before-for-supporting-fine-grained-updates) @@ -140,7 +143,7 @@ - [Addon-interactions: Interactions debugger is now default](#addon-interactions-interactions-debugger-is-now-default) - [7.0 Vite changes](#70-vite-changes) - [Vite builder uses Vite config automatically](#vite-builder-uses-vite-config-automatically) - - [Vite cache moved to node_modules/.cache/.vite-storybook](#vite-cache-moved-to-node_modulescachevite-storybook) + - [Vite cache moved to node\_modules/.cache/.vite-storybook](#vite-cache-moved-to-node_modulescachevite-storybook) - [7.0 Webpack changes](#70-webpack-changes) - [Webpack4 support discontinued](#webpack4-support-discontinued) - [Babel mode v7 exclusively](#babel-mode-v7-exclusively) @@ -190,7 +193,7 @@ - [Dropped addon-docs manual babel configuration](#dropped-addon-docs-manual-babel-configuration) - [Dropped addon-docs manual configuration](#dropped-addon-docs-manual-configuration) - [Autoplay in docs](#autoplay-in-docs) - - [Removed STORYBOOK_REACT_CLASSES global](#removed-storybook_react_classes-global) + - [Removed STORYBOOK\_REACT\_CLASSES global](#removed-storybook_react_classes-global) - [7.0 Deprecations and default changes](#70-deprecations-and-default-changes) - [storyStoreV7 enabled by default](#storystorev7-enabled-by-default) - [`Story` type deprecated](#story-type-deprecated) @@ -403,6 +406,20 @@ - [Packages renaming](#packages-renaming) - [Deprecated embedded addons](#deprecated-embedded-addons) +## From version 8.0 to 8.1.0 + +### Subtitle block and `parameters.componentSubtitle` + +The `Subtitle` block now accepts an `of` prop, which can be a reference to a CSF file or a default export (meta). + +`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/addons/links/template/stories/decorator.stories.ts b/code/addons/links/template/stories/decorator.stories.ts index 218833a75800..53a05f380e74 100644 --- a/code/addons/links/template/stories/decorator.stories.ts +++ b/code/addons/links/template/stories/decorator.stories.ts @@ -9,29 +9,44 @@ export default { decorators: [withLinks], }; -export const Basic = { +export const Target = { args: { content: `
- go to other + This is just a story to target with the links
`, }, + parameters: { + chromatic: { disable: true }, + }, }; -export const Other = { + +export const KindAndStory = { args: { content: `
- go to third + go to story only
`, }, }; -export const Third = { + +export const StoryOnly = { + args: { + content: ` +
+ go to target +
+ `, + }, +}; + +export const KindOnly = { args: { content: `
- go to basic + go to target
`, }, diff --git a/code/addons/links/template/stories/hrefto.stories.ts b/code/addons/links/template/stories/hrefto.stories.ts new file mode 100644 index 000000000000..d58844b305bd --- /dev/null +++ b/code/addons/links/template/stories/hrefto.stories.ts @@ -0,0 +1,22 @@ +import { hrefTo } from '@storybook/addon-links'; + +export default { + component: globalThis.Components.Html, + title: 'hrefTo', + parameters: { + chromatic: { disable: true }, + }, + args: { + content: '
Waiting for hrefTo to resolve...
', + }, +}; + +export const Default = { + play: async () => { + const href = await hrefTo('addons-links-hrefto', 'target'); + const content = document.querySelector('#content'); + if (content) { + content.textContent = href; + } + }, +}; diff --git a/code/addons/links/template/stories/linkto.stories.ts b/code/addons/links/template/stories/linkto.stories.ts index bdc752c2ae17..502509a8d5aa 100644 --- a/code/addons/links/template/stories/linkto.stories.ts +++ b/code/addons/links/template/stories/linkto.stories.ts @@ -3,6 +3,7 @@ import { linkTo } from '@storybook/addon-links'; export default { component: globalThis.Components.Button, + title: 'linkTo', args: { label: 'Click Me!', }, @@ -11,34 +12,69 @@ export default { }, }; -export const ID = { +export const Target = { args: { - onClick: linkTo('addons-links-parameters--basic'), + label: 'This is just a story to target with the links', + }, + parameters: { + chromatic: { disable: true }, }, }; -export const Title = { + +export const Id = { args: { - onClick: linkTo('addons-links-parameters'), + onClick: linkTo('addons-links-linkto--target'), + label: 'addons-links-linkto--target', }, }; -export const Basic = { + +export const TitleOnly = { args: { - onClick: linkTo('addons-links-parameters', 'basic'), + onClick: linkTo('addons/links/linkTo'), + label: 'addons/links/linkTo', }, }; -export const Other = { + +export const NormalizedTitleOnly = { args: { - onClick: linkTo('addons-links-parameters', 'basic'), + onClick: linkTo('addons-links-linkto'), + label: 'addons-links-linkto', }, }; -export const Third = { + +export const TitleAndName = { args: { - onClick: linkTo('addons-links-parameters', 'other'), + onClick: linkTo('addons/links/linkTo', 'Target'), + label: 'addons/links/linkTo, Target', + }, +}; + +export const NormalizedTitleAndName = { + args: { + onClick: linkTo('addons-links-linkto', 'target'), + label: 'addons-links-linkto, target', }, }; export const Callback = { args: { - onClick: linkTo('addons-links-parameters', (event: Event) => 'basic'), + onClick: linkTo( + (event: Event) => 'addons-links-linkto', + (event: Event) => 'target' + ), + }, +}; + +export const ToMDXDocs = { + args: { + onClick: linkTo('Configure Your Project'), + label: 'Configure Your Project', + }, +}; + +export const ToAutodocs = { + args: { + onClick: linkTo('Example Button', 'Docs'), + label: 'Example Button, Docs', }, }; diff --git a/code/addons/links/template/stories/scroll.stories.ts b/code/addons/links/template/stories/scroll.stories.ts deleted file mode 100644 index a7d6a3937763..000000000000 --- a/code/addons/links/template/stories/scroll.stories.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { global as globalThis } from '@storybook/global'; -import { withLinks } from '@storybook/addon-links'; - -export default { - component: globalThis.Components.Html, - parameters: { - chromatic: { disable: true }, - }, - decorators: [withLinks], -}; - -export const Basic = { - args: { - content: ` -
-
- go to basic -
- `, - }, -}; -export const Other = { - args: { - content: ` -
-
- to to basic -
- `, - }, -}; -export const Third = { - args: { - content: ` -
-
- go to other -
- `, - }, -}; diff --git a/code/e2e-tests/addon-docs.spec.ts b/code/e2e-tests/addon-docs.spec.ts index db7b7b7d5e05..2713892fd042 100644 --- a/code/e2e-tests/addon-docs.spec.ts +++ b/code/e2e-tests/addon-docs.spec.ts @@ -67,7 +67,7 @@ test.describe('addon-docs', () => { await new Promise(resolve => resolve('Play function')); } }`; - await expect(sourceCode.textContent()).resolves.toContain(expectedSource); + await expect(sourceCode).toHaveText(expectedSource); }); test('should render errors', async ({ page }) => { diff --git a/code/e2e-tests/framework-nextjs.spec.ts b/code/e2e-tests/framework-nextjs.spec.ts index 61233dd5ac25..c1a15c2632ef 100644 --- a/code/e2e-tests/framework-nextjs.spec.ts +++ b/code/e2e-tests/framework-nextjs.spec.ts @@ -10,7 +10,7 @@ test.describe('Next.js', () => { // TODO: improve these E2E tests given that we have more version of Next.js to test // and this only tests nextjs/default-js test.skip( - !templateName?.includes('nextjs/default-js'), + !templateName?.includes('nextjs/default-ts'), 'Only run this test for the Frameworks that support next/navigation' ); @@ -66,7 +66,7 @@ test.describe('Next.js', () => { sbPage = new SbPage(page); await sbPage.navigateToStory( - 'stories/frameworks/nextjs-nextjs-default-js/Navigation', + 'stories/frameworks/nextjs-nextjs-default-ts/Navigation', 'default' ); root = sbPage.previewRoot(); @@ -100,7 +100,7 @@ test.describe('Next.js', () => { test.beforeEach(async ({ page }) => { sbPage = new SbPage(page); - await sbPage.navigateToStory('stories/frameworks/nextjs-nextjs-default-js/Router', 'default'); + await sbPage.navigateToStory('stories/frameworks/nextjs-nextjs-default-ts/Router', 'default'); root = sbPage.previewRoot(); }); diff --git a/code/e2e-tests/framework-svelte.spec.ts b/code/e2e-tests/framework-svelte.spec.ts index 7d2efe7db6f3..ba42745c6172 100644 --- a/code/e2e-tests/framework-svelte.spec.ts +++ b/code/e2e-tests/framework-svelte.spec.ts @@ -41,7 +41,7 @@ test.describe('Svelte', () => { await showCodeButton.click(); const sourceCode = root.locator('pre.prismjs'); const expectedSource = ''; - await expect(sourceCode.textContent()).resolves.toContain(expectedSource); + await expect(sourceCode).toHaveText(expectedSource); }); test('Decorators runs only once', async ({ page }) => { diff --git a/code/ui/blocks/src/blocks/Subtitle.stories.tsx b/code/ui/blocks/src/blocks/Subtitle.stories.tsx new file mode 100644 index 000000000000..4fe4a2ef6a19 --- /dev/null +++ b/code/ui/blocks/src/blocks/Subtitle.stories.tsx @@ -0,0 +1,104 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { Subtitle } from './Subtitle'; +import * as DefaultButtonStories from '../examples/Button.stories'; +import * as ButtonStoriesWithMetaSubtitleAsBoth from '../examples/ButtonWithMetaSubtitleAsBoth.stories'; +import * as ButtonStoriesWithMetaSubtitleAsComponentSubtitle from '../examples/ButtonWithMetaSubtitleAsComponentSubtitle.stories'; +import * as ButtonStoriesWithMetaSubtitleAsDocsSubtitle from '../examples/ButtonWithMetaSubtitleAsDocsSubtitle.stories'; + +const meta: Meta = { + component: Subtitle, + 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 OfCSFFileAsBoth: Story = { + args: { + of: ButtonStoriesWithMetaSubtitleAsBoth, + }, + parameters: { + relativeCsfPaths: ['../examples/ButtonWithMetaSubtitleAsBoth.stories'], + }, +}; +export const OfCSFFileAsComponentSubtitle: Story = { + name: 'Of CSF File As parameters.componentSubtitle', + args: { + of: ButtonStoriesWithMetaSubtitleAsComponentSubtitle, + }, + parameters: { + relativeCsfPaths: ['../examples/ButtonWithMetaSubtitleAsComponentSubtitle.stories'], + }, +}; +export const OfCSFFileAsDocsSubtitle: Story = { + name: 'Of CSF File As parameters.docs.subtitle', + args: { + of: ButtonStoriesWithMetaSubtitleAsDocsSubtitle, + }, + parameters: { + relativeCsfPaths: ['../examples/ButtonWithMetaSubtitleAsDocsSubtitle.stories'], + }, +}; +export const OfMetaAsBoth: Story = { + args: { + of: ButtonStoriesWithMetaSubtitleAsBoth.default, + }, + parameters: { + relativeCsfPaths: ['../examples/ButtonWithMetaSubtitleAsBoth.stories'], + }, +}; +export const OfMetaAsComponentSubtitle: Story = { + name: 'Of Meta As parameters.componentSubtitle', + args: { + of: ButtonStoriesWithMetaSubtitleAsComponentSubtitle.default, + }, + parameters: { + relativeCsfPaths: ['../examples/ButtonWithMetaSubtitleAsComponentSubtitle.stories'], + }, +}; +export const OfMetaAsDocsSubtitle: Story = { + name: 'Of Meta As parameters.docs.subtitle', + args: { + of: ButtonStoriesWithMetaSubtitleAsDocsSubtitle.default, + }, + parameters: { + relativeCsfPaths: ['../examples/ButtonWithMetaSubtitleAsDocsSubtitle.stories'], + }, +}; +export const DefaultAttached: Story = { + parameters: { relativeCsfPaths: ['../examples/Button.stories'], attached: true }, +}; +export const OfUndefinedAttached: Story = { + args: { + // @ts-expect-error this is supposed to be undefined + // eslint-disable-next-line import/namespace + of: DefaultButtonStories.NotDefined, + }, + parameters: { + chromatic: { disableSnapshot: true }, + relativeCsfPaths: ['../examples/Button.stories'], + attached: true, + }, + decorators: [(s) => (window?.navigator.userAgent.match(/StorybookTestRunner/) ?
: s())], +}; +export const OfStringMetaAttached: Story = { + name: 'Of "meta" Attached', + args: { + of: 'meta', + }, + parameters: { relativeCsfPaths: ['../examples/Button.stories'], attached: true }, +}; +export const Children: Story = { + parameters: { relativeCsfPaths: ['../examples/Button.stories'], attached: true }, + render: () => This subtitle is a string passed as a children, +}; diff --git a/code/ui/blocks/src/blocks/Subtitle.tsx b/code/ui/blocks/src/blocks/Subtitle.tsx index 143543cb27fb..9b7556e9c7c6 100644 --- a/code/ui/blocks/src/blocks/Subtitle.tsx +++ b/code/ui/blocks/src/blocks/Subtitle.tsx @@ -1,15 +1,40 @@ import type { FunctionComponent, ReactNode } from 'react'; -import React, { useContext } from 'react'; +import React from 'react'; +import { deprecate } from '@storybook/client-logger'; + import { Subtitle as PureSubtitle } from '../components'; -import { DocsContext } from './DocsContext'; +import type { Of } from './useOf'; +import { useOf } from './useOf'; interface SubtitleProps { children?: ReactNode; + /** + * Specify where to get the subtitle from. + * If not specified, the subtitle will be extracted from the meta of the attached CSF file. + */ + of?: Of; } -export const Subtitle: FunctionComponent = ({ children }) => { - const docsContext = useContext(DocsContext); - const content = children || docsContext.storyById().parameters?.componentSubtitle; +const DEPRECATION_MIGRATION_LINK = + 'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#subtitle-block-and-parameterscomponentsubtitle'; + +export const Subtitle: FunctionComponent = (props) => { + const { of, children } = props; + + if ('of' in props && of === undefined) { + throw new Error('Unexpected `of={undefined}`, did you mistype a CSF file reference?'); + } + + const { preparedMeta } = useOf(of || 'meta', ['meta']); + const { componentSubtitle, docs } = preparedMeta.parameters || {}; + + if (componentSubtitle) { + deprecate( + `Using 'parameters.componentSubtitle' property to subtitle stories is deprecated. See ${DEPRECATION_MIGRATION_LINK}` + ); + } + + const content = children || docs?.subtitle || componentSubtitle; return content ? ( {content} 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/code/ui/blocks/src/examples/Button.stories.tsx b/code/ui/blocks/src/examples/Button.stories.tsx index e5fc5b2e3457..a49f88f5d8f8 100644 --- a/code/ui/blocks/src/examples/Button.stories.tsx +++ b/code/ui/blocks/src/examples/Button.stories.tsx @@ -17,6 +17,9 @@ const meta = { notes: 'These are notes for the Button stories', info: 'This is info for the Button stories', jsx: { useBooleanShorthandSyntax: false }, + docs: { + subtitle: 'This is the subtitle for the Button stories', + }, }, } satisfies Meta; diff --git a/code/ui/blocks/src/examples/ButtonWithMetaSubtitleAsBoth.stories.tsx b/code/ui/blocks/src/examples/ButtonWithMetaSubtitleAsBoth.stories.tsx new file mode 100644 index 000000000000..5b4235c07c57 --- /dev/null +++ b/code/ui/blocks/src/examples/ButtonWithMetaSubtitleAsBoth.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from './Button'; + +const meta = { + title: 'examples/Button with Meta Subtitle in Both', + component: Button, + argTypes: { + backgroundColor: { control: 'color' }, + }, + parameters: { + // Stop *this* story from being stacked in Chromatic + theme: 'default', + // this is to test the deprecated features of the Subtitle block + componentSubtitle: 'This subtitle is set in parameters.componentSubtitle', + docs: { + subtitle: 'This subtitle is set in parameters.docs.subtitle', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const WithMetaSubtitleAsBoth: Story = { + args: { + primary: true, + label: 'Button', + }, +}; diff --git a/code/ui/blocks/src/examples/ButtonWithMetaSubtitleAsComponentSubtitle.stories.tsx b/code/ui/blocks/src/examples/ButtonWithMetaSubtitleAsComponentSubtitle.stories.tsx new file mode 100644 index 000000000000..57a106340421 --- /dev/null +++ b/code/ui/blocks/src/examples/ButtonWithMetaSubtitleAsComponentSubtitle.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from './Button'; + +const meta = { + title: 'examples/Button with Meta Subtitle in componentSubtitle', + component: Button, + argTypes: { + backgroundColor: { control: 'color' }, + }, + parameters: { + // Stop *this* story from being stacked in Chromatic + theme: 'default', + // this is to test the deprecated features of the Subtitle block + componentSubtitle: 'This subtitle is set in parameters.componentSubtitle', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const WithMetaSubtitleInComponentSubtitle: Story = { + args: { + primary: true, + label: 'Button', + }, +}; diff --git a/code/ui/blocks/src/examples/ButtonWithMetaSubtitleAsDocsSubtitle.stories.tsx b/code/ui/blocks/src/examples/ButtonWithMetaSubtitleAsDocsSubtitle.stories.tsx new file mode 100644 index 000000000000..3df3110baf6c --- /dev/null +++ b/code/ui/blocks/src/examples/ButtonWithMetaSubtitleAsDocsSubtitle.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from './Button'; + +const meta = { + title: 'examples/Button with Meta Subtitle in docs.subtitle', + component: Button, + argTypes: { + backgroundColor: { control: 'color' }, + }, + parameters: { + // Stop *this* story from being stacked in Chromatic + theme: 'default', + docs: { + subtitle: 'This subtitle is set in parameters.docs.subtitle', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const WithMetaSubtitleInDocsSubtitle: Story = { + args: { + primary: true, + label: 'Button', + }, +}; diff --git a/code/ui/blocks/src/examples/EmptyExample.tsx b/code/ui/blocks/src/examples/EmptyExample.tsx index d9ad80b7a120..a1b48922f303 100644 --- a/code/ui/blocks/src/examples/EmptyExample.tsx +++ b/code/ui/blocks/src/examples/EmptyExample.tsx @@ -2,7 +2,7 @@ import React from 'react'; export const EmptyExample = ({}) => (
- This component is not intended to render anything, it simply serves a something to hang + This component is not intended to render anything, it simply serves as something to hang parameters off
); diff --git a/code/ui/components/src/components/tabs/tabs.stories.tsx b/code/ui/components/src/components/tabs/tabs.stories.tsx index a3c40fd8a9d9..658b994b1e2e 100644 --- a/code/ui/components/src/components/tabs/tabs.stories.tsx +++ b/code/ui/components/src/components/tabs/tabs.stories.tsx @@ -18,6 +18,11 @@ interface FibonacciMap { [key: string]: number; } +function Counter() { + const [count, setCount] = React.useState(0); + return ; +} + function fibonacci(num: number, memo?: FibonacciMap): number { if (!memo) { memo = {}; @@ -376,3 +381,22 @@ export const StatelessWithCustomEmpty = { /> ), } satisfies StoryObj; + +export const StatefulWithStatefulPanel = { + render: (args) => { + const [update, setUpdate] = React.useState(0); + return ( +
+ + +
+ +
+
+ +
+
+
+ ); + }, +} satisfies Story; diff --git a/code/ui/components/src/components/tabs/tabs.tsx b/code/ui/components/src/components/tabs/tabs.tsx index 5b0cbb2b5612..3d90fb3f9e58 100644 --- a/code/ui/components/src/components/tabs/tabs.tsx +++ b/code/ui/components/src/components/tabs/tabs.tsx @@ -145,18 +145,13 @@ export const Tabs: FC = memo( emptyState, showToolsWhenEmpty, }) => { - const idList = childrenToList(children) - .map((i) => i.id) - .join(','); - const list = useMemo( () => childrenToList(children).map((i, index) => ({ ...i, active: selected ? i.id === selected : index === 0, })), - // eslint-disable-next-line react-hooks/exhaustive-deps -- we're using idList as a replacement for children - [selected, idList] + [children, selected] ); const { visibleList, tabBarRef, tabRefs, AddonTab } = useList(list); diff --git a/docs/api/doc-block-subtitle.md b/docs/api/doc-block-subtitle.md index 1104184c9992..3478a0c2fe8a 100644 --- a/docs/api/doc-block-subtitle.md +++ b/docs/api/doc-block-subtitle.md @@ -30,6 +30,12 @@ import { Subtitle } from '@storybook/blocks'; Type: `JSX.Element | string` -Default: `parameters.componentSubtitle` +Default: `parameters.docs.subtitle` Provides the content. + +### `of` + +Type: CSF file exports + +Specifies which meta's subtitle is displayed. 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. diff --git a/scripts/tasks/sandbox-parts.ts b/scripts/tasks/sandbox-parts.ts index cd7dc643a185..b4e3b535d79d 100644 --- a/scripts/tasks/sandbox-parts.ts +++ b/scripts/tasks/sandbox-parts.ts @@ -533,6 +533,32 @@ export const extendMain: Task['run'] = async ({ template, sandboxDir }, { disabl Object.entries(configToAdd).forEach(([field, value]) => mainConfig.setFieldValue([field], value)); + const previewHeadCode = ` + (head) => \` + \${head} + ${templateConfig.previewHead || ''} + + \``; + mainConfig.setFieldNode(['previewHead'], babelParse(previewHeadCode).program.body[0].expression); + // Simulate Storybook Lite if (disableDocs) { const addons = mainConfig.getFieldValue(['addons']);