- [From version 7.x to 8.0.0](#from-version-7x-to-800)
- - [Type change in `composeStories` API](#type-change-in-composestories-api)
+ - [Portable stories](#portable-stories)
+ - [Type change in `composeStories` API](#type-change-in-composestories-api)
+ - [The context in the play function is now optional](#the-context-in-the-play-function-is-now-optional)
- [Tab addons are now routed to a query parameter](#tab-addons-are-now-routed-to-a-query-parameter)
- [Default keyboard shortcuts changed](#default-keyboard-shortcuts-changed)
- [Manager addons are now rendered with React 18](#manager-addons-are-now-rendered-with-react-18)
@@ -86,17 +88,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)
@@ -122,7 +124,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)
@@ -136,7 +138,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)
@@ -186,7 +188,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)
@@ -401,7 +403,9 @@
## From version 7.x to 8.0.0
-### Type change in `composeStories` API
+### Portable stories
+
+#### Type change in `composeStories` API
There is a TypeScript type change in the `play` function returned from `composeStories` or `composeStory` in `@storybook/react` or `@storybook/vue3`, where before it was always defined, now it is potentially undefined. This means that you might have to make a small change in your code, such as:
@@ -418,6 +422,49 @@ await Primary.play!(...) // if you want a runtime error when the play function d
There are plans to make the type of the play function be inferred based on your imported story's play function in a near future, so the types will be 100% accurate.
+#### The context in the play function is now optional
+
+When reusing a story that has a play function, you don't have to pass the context anymore, not even the `canvasElement`. The context is built-in and if you don't pass overrides, it will still be present. It is still possible to pass overrides to the context, if you'd like.
+
+```tsx
+const { Primary } = composeStories(stories);
+test("load and render", async () => {
+ const { container } = render();
+ // before:
+ await Primary.play({ canvasElement: container, ...ArgsOrWhateverElse });
+
+ // after:
+ await Primary.play();
+});
+```
+
+In order for this to be possible, the portable stories API now adds a wrapper to your stories with a unique id based on your story id, such as:
+
+```html
+
+
+
+```
+
+This means that if you take DOM snapshots of your stories, they will be affected and you will have to update them.
+
+The id calculation is based on different heuristics based on your Meta title and Story name. When using `composeStories`, the id can be inferred automatically. However, when using `composeStory` and your story does not explicitly have a `storyName` property, the story name can't be inferred automatically. As a result, its name will be "Unnamed Story", resulting in a wrapper id like `"#storybook-story-button--unnamed-story"`. If the id matters to you and you want to fix it, you have to specify the `exportsName` property like so:
+
+```ts
+test("snapshots the story with custom id", () => {
+ const Primary = composeStory(
+ stories.Primary,
+ stories.default,
+ undefined,
+ // If you do not want the `unnamed-story` id, you have to pass the name of the story as a parameter
+ "Primary"
+ );
+
+ const { baseElement } = render();
+ expect(baseElement).toMatchSnapshot();
+});
+```
+
### Tab addons are now routed to a query parameter
The URL of a tab used to be: `http://localhost:6006/?path=/my-addon-tab/my-story`.
@@ -556,7 +603,6 @@ This means https://github.com/IanVS/vite-plugin-turbosnap is no longer necessary
Now that both Vite and Webpack support the `preview-stats.json` file, the flag has been renamed. The old flag will continue to work.
-
### Implicit actions can not be used during rendering (for example in the play function)
In Storybook 7, we inferred if the component accepts any action props,
diff --git a/code/lib/preview-api/src/index.ts b/code/lib/preview-api/src/index.ts
index e47cdaa0a0dd..63d45114dc23 100644
--- a/code/lib/preview-api/src/index.ts
+++ b/code/lib/preview-api/src/index.ts
@@ -56,6 +56,7 @@ export {
filterArgTypes,
sanitizeStoryContextUpdate,
setProjectAnnotations,
+ getPortableStoryWrapperId,
inferControls,
userOrAutoTitleFromSpecifier,
userOrAutoTitle,
diff --git a/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts b/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts
index af775f27360b..54e82553757f 100644
--- a/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts
+++ b/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts
@@ -1,10 +1,18 @@
// @vitest-environment node
import { describe, expect, vi, it } from 'vitest';
-import { composeStory, composeStories } from './portable-stories';
+import type {
+ ComponentAnnotations as Meta,
+ StoryAnnotationsOrFn as Story,
+ Store_CSFExports,
+} from '@storybook/types';
+
+import { composeStory, composeStories, setProjectAnnotations } from './portable-stories';
+
+type StoriesModule = Store_CSFExports & Record;
// Most integration tests for this functionality are located under renderers/react
describe('composeStory', () => {
- const meta = {
+ const meta: Meta = {
title: 'Button',
parameters: {
firstAddon: true,
@@ -15,13 +23,26 @@ describe('composeStory', () => {
},
};
- it('should return story with composed args and parameters', () => {
- const Story = () => {};
- Story.args = { primary: true };
- Story.parameters = {
+ it('should return story with composed annotations from story, meta and project', () => {
+ const decoratorFromProjectAnnotations = vi.fn((StoryFn) => StoryFn());
+ const decoratorFromStoryAnnotations = vi.fn((StoryFn) => StoryFn());
+ setProjectAnnotations([
+ {
+ parameters: { injected: true },
+ globalTypes: {
+ locale: { defaultValue: 'en' },
+ },
+ decorators: [decoratorFromProjectAnnotations],
+ },
+ ]);
+
+ const Story: Story = {
+ render: () => {},
+ args: { primary: true },
parameters: {
secondAddon: true,
},
+ decorators: [decoratorFromStoryAnnotations],
};
const composedStory = composeStory(Story, meta);
@@ -29,28 +50,22 @@ describe('composeStory', () => {
expect(composedStory.parameters).toEqual(
expect.objectContaining({ ...Story.parameters, ...meta.parameters })
);
+
+ composedStory();
+
+ expect(decoratorFromProjectAnnotations).toHaveBeenCalled();
+ expect(decoratorFromStoryAnnotations).toHaveBeenCalled();
});
it('should compose with a play function', async () => {
const spy = vi.fn();
- const Story = () => {};
+ const Story: Story = () => {};
Story.args = {
primary: true,
};
Story.play = async (context: any) => {
spy(context);
};
-
- const composedStory = composeStory(Story, meta);
- await composedStory.play!({ canvasElement: null });
- expect(spy).toHaveBeenCalledWith(
- expect.objectContaining({
- args: {
- ...Story.args,
- ...meta.args,
- },
- })
- );
});
it('should throw an error if Story is undefined', () => {
@@ -62,7 +77,7 @@ describe('composeStory', () => {
describe('Id of the story', () => {
it('is exposed correctly when composeStories is used', () => {
- const module = {
+ const module: StoriesModule = {
default: {
title: 'Example/Button',
},
@@ -72,7 +87,7 @@ describe('composeStory', () => {
expect(Primary.id).toBe('example-button--csf-3-primary');
});
it('is exposed correctly when composeStory is used and exportsName is passed', () => {
- const module = {
+ const module: StoriesModule = {
default: {
title: 'Example/Button',
},
@@ -83,7 +98,7 @@ describe('composeStory', () => {
});
it("is not unique when composeStory is used and exportsName isn't passed", () => {
const Primary = composeStory({ render: () => {} }, {});
- expect(Primary.id).toContain('unknown');
+ expect(Primary.id).toContain('composedstory--unnamed-story');
});
});
});
@@ -93,7 +108,7 @@ describe('composeStories', () => {
const defaultAnnotations = { render: () => '' };
it('should call composeStoryFn with stories', () => {
const composeStorySpy = vi.fn((v) => v);
- const module = {
+ const module: StoriesModule = {
default: {
title: 'Button',
},
@@ -118,7 +133,7 @@ describe('composeStories', () => {
it('should not call composeStoryFn for non-story exports', () => {
const composeStorySpy = vi.fn((v) => v);
- const module = {
+ const module: StoriesModule = {
default: {
title: 'Button',
excludeStories: /Data/,
@@ -131,7 +146,7 @@ describe('composeStories', () => {
describe('non-story exports', () => {
it('should filter non-story exports with excludeStories', () => {
- const StoryModuleWithNonStoryExports = {
+ const StoryModuleWithNonStoryExports: StoriesModule = {
default: {
title: 'Some/Component',
excludeStories: /.*Data/,
@@ -149,7 +164,7 @@ describe('composeStories', () => {
});
it('should filter non-story exports with includeStories', () => {
- const StoryModuleWithNonStoryExports = {
+ const StoryModuleWithNonStoryExports: StoriesModule = {
default: {
title: 'Some/Component',
includeStories: /.*Story/,
diff --git a/code/lib/preview-api/src/modules/store/csf/portable-stories.ts b/code/lib/preview-api/src/modules/store/csf/portable-stories.ts
index 91e4cdc365e1..6f24b2933cdf 100644
--- a/code/lib/preview-api/src/modules/store/csf/portable-stories.ts
+++ b/code/lib/preview-api/src/modules/store/csf/portable-stories.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/naming-convention */
import { isExportStory } from '@storybook/csf';
import type {
Renderer,
@@ -12,7 +13,7 @@ import type {
Parameters,
ComposedStoryFn,
StrictArgTypes,
- ComposedStoryPlayContext,
+ PlayFunctionContext,
} from '@storybook/types';
import { HooksContext } from '../../../addons';
@@ -22,9 +23,14 @@ import { normalizeStory } from './normalizeStory';
import { normalizeComponentAnnotations } from './normalizeComponentAnnotations';
import { getValuesFromArgTypes } from './getValuesFromArgTypes';
import { normalizeProjectAnnotations } from './normalizeProjectAnnotations';
+import { normalizeArrays } from './normalizeArrays';
let GLOBAL_STORYBOOK_PROJECT_ANNOTATIONS = composeConfigs([]);
+export function getPortableStoryWrapperId(storyId: string) {
+ return `storybook-story-${storyId}`;
+}
+
export function setProjectAnnotations(
projectAnnotations: ProjectAnnotations | ProjectAnnotations[]
) {
@@ -32,6 +38,46 @@ export function setProjectAnnotations(
GLOBAL_STORYBOOK_PROJECT_ANNOTATIONS = composeConfigs(annotations);
}
+// TODO: we don't have any utility that merges two sets of annotations (e.g. two project annotations)
+// this should be elsewhere, probably reused
+function mergeAnnotations(
+ firstAnnotations: ProjectAnnotations,
+ secondAnnotations: ProjectAnnotations
+) {
+ return {
+ ...firstAnnotations,
+ ...secondAnnotations,
+ args: {
+ ...firstAnnotations.args,
+ ...secondAnnotations.args,
+ },
+ argTypes: {
+ ...firstAnnotations.argTypes,
+ ...secondAnnotations.argTypes,
+ },
+ parameters: {
+ ...firstAnnotations.parameters,
+ ...secondAnnotations.parameters,
+ },
+ decorators: [
+ ...normalizeArrays(firstAnnotations.decorators),
+ ...normalizeArrays(secondAnnotations.decorators),
+ ],
+ loaders: [
+ ...normalizeArrays(firstAnnotations.loaders),
+ ...normalizeArrays(secondAnnotations.loaders),
+ ],
+ argsEnhancers: [
+ ...normalizeArrays(firstAnnotations.argsEnhancers),
+ ...normalizeArrays(secondAnnotations.argsEnhancers),
+ ],
+ argTypesEnhancers: [
+ ...normalizeArrays(firstAnnotations.argTypesEnhancers),
+ ...normalizeArrays(secondAnnotations.argTypesEnhancers),
+ ],
+ };
+}
+
export function composeStory(
storyAnnotations: LegacyStoryAnnotationsOrFn,
componentAnnotations: ComponentAnnotations,
@@ -43,6 +89,12 @@ export function composeStory;
+ }
+
// @TODO: Support auto title
componentAnnotations.title = componentAnnotations.title ?? 'ComposedStory';
@@ -54,7 +106,7 @@ export function composeStory(
storyName,
@@ -62,10 +114,9 @@ export function composeStory({
- ...projectAnnotations,
- ...defaultConfig,
- });
+ const normalizedProjectAnnotations = normalizeProjectAnnotations(
+ mergeAnnotations(projectAnnotations, defaultConfig)
+ );
const story = prepareStory(
normalizedStory,
@@ -77,7 +128,10 @@ export function composeStory = {
hooks: new HooksContext(),
- globals: defaultGlobals,
+ globals: {
+ ...defaultGlobals,
+ ...projectAnnotations.globals,
+ },
args: { ...story.initialArgs },
viewMode: 'story',
loaded: {},
@@ -86,28 +140,35 @@ export function composeStory>) =>
+ story.playFunction!({
+ ...context,
+ ...extraContext,
+ // if canvasElement is not provided, we default to the root element, which comes from a decorator
+ // the decorator has to be implemented in the defaultAnnotations of each integrator of portable stories
+ canvasElement:
+ extraContext?.canvasElement ??
+ globalThis.document?.getElementById(getPortableStoryWrapperId(context.id)),
+ })
+ : undefined;
+
const composedStory: ComposedStoryFn> = Object.assign(
- (extraArgs?: Partial) => {
- const finalContext: StoryContext = {
- ...context,
- args: { ...context.initialArgs, ...extraArgs },
+ function storyFn(extraArgs?: Partial) {
+ context.args = {
+ ...context.initialArgs,
+ ...extraArgs,
};
- return story.unboundStoryFn(prepareContext(finalContext));
+ return story.unboundStoryFn(prepareContext(context));
},
{
+ id: story.id,
storyName,
args: story.initialArgs as Partial,
parameters: story.parameters as Parameters,
argTypes: story.argTypes as StrictArgTypes,
- id: story.id,
- play: story.playFunction
- ? ((async (extraContext: ComposedStoryPlayContext) =>
- story.playFunction!({
- ...context,
- ...extraContext,
- })) as unknown as ComposedStoryPlayFn>)
- : undefined,
+ play: playFunction as ComposedStoryPlayFn | undefined,
}
);
@@ -119,7 +180,6 @@ export function composeStories(
globalConfig: ProjectAnnotations,
composeStoryFn: ComposeStoryFn
) {
- // eslint-disable-next-line @typescript-eslint/naming-convention
const { default: meta, __esModule, __namedExportsOrder, ...stories } = storiesImport;
const composedStories = Object.entries(stories).reduce((storiesMap, [exportsName, story]) => {
if (!isExportStory(exportsName, meta)) {
diff --git a/code/lib/types/src/modules/composedStory.ts b/code/lib/types/src/modules/composedStory.ts
index 5ce61bc678e8..b7051a699b7f 100644
--- a/code/lib/types/src/modules/composedStory.ts
+++ b/code/lib/types/src/modules/composedStory.ts
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */
-import type { Renderer, StoryId, StrictArgTypes } from '@storybook/csf';
+import type { PlayFunction, Renderer, StoryId, StrictArgTypes } from '@storybook/csf';
import type {
AnnotatedStoryFn,
@@ -9,7 +9,6 @@ import type {
Parameters,
StoryAnnotations,
StoryAnnotationsOrFn,
- StoryContext,
} from './csf';
import type { ProjectAnnotations } from './story';
@@ -22,23 +21,6 @@ export type Store_CSFExports) // or PrimaryButton()
- * PrimaryButton.play({ canvasElement: container })
- */
-export type ComposedStoryPlayContext = Partial<
- StoryContext & Pick, 'canvasElement'>
->;
-
-export type ComposedStoryPlayFn = (
- context: ComposedStoryPlayContext
-) => Promise | void;
-
/**
* A story function with partial args, used internally by composeStory
*/
@@ -48,6 +30,14 @@ export type PartialArgsStoryFn = T extends (...args: infer P) => infer R
+ ? (...args: { [K in keyof P]?: Partial
}) => R
+ : never;
+
+export type ComposedStoryPlayFn<
+ TRenderer extends Renderer = Renderer,
+ TArgs = Args,
+> = MakeAllParametersOptional>>;
/**
* A story that got recomposed for portable stories, containing all the necessary data to be rendered in external environments
*/
@@ -55,9 +45,9 @@ export type ComposedStoryFn<
TRenderer extends Renderer = Renderer,
TArgs = Args,
> = PartialArgsStoryFn & {
- play: ComposedStoryPlayFn | undefined;
args: TArgs;
id: StoryId;
+ play?: ComposedStoryPlayFn;
storyName: string;
parameters: Parameters;
argTypes: StrictArgTypes;
diff --git a/code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap b/code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap
index 2b92b1d68424..3f40e5446d0b 100644
--- a/code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap
+++ b/code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap
@@ -3,12 +3,17 @@
exports[`Renders CSF2Secondary story 1`] = `
-
+
+
`;
@@ -16,12 +21,17 @@ exports[`Renders CSF2Secondary story 1`] = `
exports[`Renders CSF2StoryWithLocale story 1`] = `
-
+
+
`;
@@ -29,12 +39,17 @@ exports[`Renders CSF2StoryWithLocale story 1`] = `
exports[`Renders CSF2StoryWithParamsAndDecorator story 1`] = `
-
+
+
`;
@@ -42,12 +57,17 @@ exports[`Renders CSF2StoryWithParamsAndDecorator story 1`] = `
exports[`Renders CSF3Button story 1`] = `
-
+
+
`;
@@ -55,18 +75,23 @@ exports[`Renders CSF3Button story 1`] = `
exports[`Renders CSF3ButtonWithRender story 1`] = `
-
-
- I am a custom render function
-
-
+
+
+
+ I am a custom render function
+
+
+
@@ -75,9 +100,14 @@ exports[`Renders CSF3ButtonWithRender story 1`] = `
exports[`Renders CSF3InputFieldFilled story 1`] = `
-
+
+
+
`;
@@ -85,12 +115,17 @@ exports[`Renders CSF3InputFieldFilled story 1`] = `
exports[`Renders CSF3Primary story 1`] = `
-
+
+
`;
diff --git a/code/renderers/react/src/__test__/portable-stories.test.tsx b/code/renderers/react/src/__test__/portable-stories.test.tsx
index afa0b70142e4..f137424fff0b 100644
--- a/code/renderers/react/src/__test__/portable-stories.test.tsx
+++ b/code/renderers/react/src/__test__/portable-stories.test.tsx
@@ -58,7 +58,7 @@ describe('projectAnnotations', () => {
expect(buttonElement).not.toBeNull();
});
- it('renders with custom projectAnnotations via composeStory params', () => {
+ it('renders with custom globals from projectAnnotations via composeStory params', () => {
const WithPortugueseText = composeStory(stories.CSF2StoryWithLocale, stories.default, {
globalTypes: { locale: { defaultValue: 'pt' } } as any,
});
@@ -94,7 +94,18 @@ describe('CSF3', () => {
expect(screen.getByTestId('custom-render')).not.toBeNull();
});
- it('renders with play function', async () => {
+ it('renders with play function without canvas element', async () => {
+ const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default);
+
+ render();
+
+ await CSF3InputFieldFilled.play!();
+
+ const input = screen.getByTestId('input') as HTMLInputElement;
+ expect(input.value).toEqual('Hello world!');
+ });
+
+ it('renders with play function with canvas element', async () => {
const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default);
const { container } = render();
diff --git a/code/renderers/react/src/portable-stories.ts b/code/renderers/react/src/portable-stories.tsx
similarity index 94%
rename from code/renderers/react/src/portable-stories.ts
rename to code/renderers/react/src/portable-stories.tsx
index 385e0dc4c804..55ca55c69ff4 100644
--- a/code/renderers/react/src/portable-stories.ts
+++ b/code/renderers/react/src/portable-stories.tsx
@@ -1,7 +1,9 @@
+import React from 'react';
import {
composeStory as originalComposeStory,
composeStories as originalComposeStories,
setProjectAnnotations as originalSetProjectAnnotations,
+ getPortableStoryWrapperId,
} from '@storybook/preview-api';
import type {
Args,
@@ -39,6 +41,15 @@ export function setProjectAnnotations(
// This will not be necessary once we have auto preset loading
const defaultProjectAnnotations: ProjectAnnotations = {
render,
+ decorators: [
+ function addStorybookId(StoryFn, { id }) {
+ return (
+
`;
-exports[`Renders CSF3Button story 1`] = `
+exports[`Renders CSF2StoryWithLocale story 1`] = `
-
+
+
+ locale: undefined
+
+
+
+
`;
-exports[`Renders CSF3ButtonWithRender story 1`] = `
+exports[`Renders CSF3Button story 1`] = `
-
-
- I am a custom render function
-
+
`;
diff --git a/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts b/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts
index 4c541e1c4536..97fb295c47bc 100644
--- a/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts
+++ b/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts
@@ -84,9 +84,9 @@ describe('CSF3', () => {
it('renders with play function', async () => {
const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default);
- const { container } = render(CSF3InputFieldFilled());
+ render(CSF3InputFieldFilled());
- await CSF3InputFieldFilled.play!({ canvasElement: container as HTMLElement });
+ await CSF3InputFieldFilled.play!();
const input = screen.getByTestId('input') as HTMLInputElement;
expect(input.value).toEqual('Hello world!');
diff --git a/code/renderers/vue3/src/portable-stories.ts b/code/renderers/vue3/src/portable-stories.ts
index 4e009b25d672..aef26b39a5e7 100644
--- a/code/renderers/vue3/src/portable-stories.ts
+++ b/code/renderers/vue3/src/portable-stories.ts
@@ -2,6 +2,7 @@ import {
composeStory as originalComposeStory,
composeStories as originalComposeStories,
setProjectAnnotations as originalSetProjectAnnotations,
+ getPortableStoryWrapperId,
} from '@storybook/preview-api';
import type {
Args,
@@ -11,10 +12,24 @@ import type {
StoriesWithPartialProps,
} from '@storybook/types';
-import * as defaultProjectAnnotations from './render';
+import * as vueProjectAnnotations from './entry-preview';
import type { Meta } from './public-types';
import type { VueRenderer } from './types';
+const defaultProjectAnnotations: ProjectAnnotations = {
+ ...vueProjectAnnotations,
+ decorators: [
+ function addStorybookId(story, { id }) {
+ return {
+ components: { story },
+ template: `
+
+
`,
+ };
+ },
+ ],
+};
+
/** Function that sets the globalConfig of your Storybook. The global config is the preview module of your .storybook folder.
*
* It should be run a single time, so that your global config (e.g. decorators) is applied to your stories when using `composeStories` or `composeStory`.
From 0694772841f956bcf55ec81f8aba71771870f71e Mon Sep 17 00:00:00 2001
From: Yann Braga
Date: Fri, 1 Mar 2024 12:23:40 +0100
Subject: [PATCH 02/11] Update
code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts
Co-authored-by: Jeppe Reinhold
---
.../src/modules/store/csf/portable-stories.test.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts b/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts
index 54e82553757f..9d3602ecc492 100644
--- a/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts
+++ b/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts
@@ -53,8 +53,8 @@ describe('composeStory', () => {
composedStory();
- expect(decoratorFromProjectAnnotations).toHaveBeenCalled();
- expect(decoratorFromStoryAnnotations).toHaveBeenCalled();
+ expect(decoratorFromProjectAnnotations).toHaveBeenCalledOnce();
+ expect(decoratorFromStoryAnnotations).toHaveBeenCalledOnce();
});
it('should compose with a play function', async () => {
From d39b6c93b433750ebdf33f68c855923872d41954 Mon Sep 17 00:00:00 2001
From: Yann Braga
Date: Fri, 1 Mar 2024 14:03:12 +0100
Subject: [PATCH 03/11] use composeConfigs function
---
.../src/modules/store/csf/portable-stories.ts | 43 +------------------
1 file changed, 1 insertion(+), 42 deletions(-)
diff --git a/code/lib/preview-api/src/modules/store/csf/portable-stories.ts b/code/lib/preview-api/src/modules/store/csf/portable-stories.ts
index 6f24b2933cdf..fbe3898fc3d2 100644
--- a/code/lib/preview-api/src/modules/store/csf/portable-stories.ts
+++ b/code/lib/preview-api/src/modules/store/csf/portable-stories.ts
@@ -23,7 +23,6 @@ import { normalizeStory } from './normalizeStory';
import { normalizeComponentAnnotations } from './normalizeComponentAnnotations';
import { getValuesFromArgTypes } from './getValuesFromArgTypes';
import { normalizeProjectAnnotations } from './normalizeProjectAnnotations';
-import { normalizeArrays } from './normalizeArrays';
let GLOBAL_STORYBOOK_PROJECT_ANNOTATIONS = composeConfigs([]);
@@ -38,46 +37,6 @@ export function setProjectAnnotations(
GLOBAL_STORYBOOK_PROJECT_ANNOTATIONS = composeConfigs(annotations);
}
-// TODO: we don't have any utility that merges two sets of annotations (e.g. two project annotations)
-// this should be elsewhere, probably reused
-function mergeAnnotations(
- firstAnnotations: ProjectAnnotations,
- secondAnnotations: ProjectAnnotations
-) {
- return {
- ...firstAnnotations,
- ...secondAnnotations,
- args: {
- ...firstAnnotations.args,
- ...secondAnnotations.args,
- },
- argTypes: {
- ...firstAnnotations.argTypes,
- ...secondAnnotations.argTypes,
- },
- parameters: {
- ...firstAnnotations.parameters,
- ...secondAnnotations.parameters,
- },
- decorators: [
- ...normalizeArrays(firstAnnotations.decorators),
- ...normalizeArrays(secondAnnotations.decorators),
- ],
- loaders: [
- ...normalizeArrays(firstAnnotations.loaders),
- ...normalizeArrays(secondAnnotations.loaders),
- ],
- argsEnhancers: [
- ...normalizeArrays(firstAnnotations.argsEnhancers),
- ...normalizeArrays(secondAnnotations.argsEnhancers),
- ],
- argTypesEnhancers: [
- ...normalizeArrays(firstAnnotations.argTypesEnhancers),
- ...normalizeArrays(secondAnnotations.argTypesEnhancers),
- ],
- };
-}
-
export function composeStory(
storyAnnotations: LegacyStoryAnnotationsOrFn,
componentAnnotations: ComponentAnnotations,
@@ -115,7 +74,7 @@ export function composeStory(
- mergeAnnotations(projectAnnotations, defaultConfig)
+ composeConfigs([projectAnnotations, defaultConfig])
);
const story = prepareStory(
From a2891859107a10b48f5392de7bd0bd30aa7a4cfe Mon Sep 17 00:00:00 2001
From: Yann Braga
Date: Fri, 1 Mar 2024 14:09:13 +0100
Subject: [PATCH 04/11] update migration notes
---
MIGRATION.md | 20 +++-----------------
1 file changed, 3 insertions(+), 17 deletions(-)
diff --git a/MIGRATION.md b/MIGRATION.md
index c51ddba6347e..62aef1593d05 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -3,7 +3,7 @@
- [From version 7.x to 8.0.0](#from-version-7x-to-800)
- [Portable stories](#portable-stories)
- [Type change in `composeStories` API](#type-change-in-composestories-api)
- - [The context in the play function is now optional](#the-context-in-the-play-function-is-now-optional)
+ - [DOM structure changed in portable stories](#dom-structure-changed-in-portable-stories)
- [Tab addons are now routed to a query parameter](#tab-addons-are-now-routed-to-a-query-parameter)
- [Default keyboard shortcuts changed](#default-keyboard-shortcuts-changed)
- [Manager addons are now rendered with React 18](#manager-addons-are-now-rendered-with-react-18)
@@ -422,23 +422,9 @@ await Primary.play!(...) // if you want a runtime error when the play function d
There are plans to make the type of the play function be inferred based on your imported story's play function in a near future, so the types will be 100% accurate.
-#### The context in the play function is now optional
+#### DOM structure changed in portable stories
-When reusing a story that has a play function, you don't have to pass the context anymore, not even the `canvasElement`. The context is built-in and if you don't pass overrides, it will still be present. It is still possible to pass overrides to the context, if you'd like.
-
-```tsx
-const { Primary } = composeStories(stories);
-test("load and render", async () => {
- const { container } = render();
- // before:
- await Primary.play({ canvasElement: container, ...ArgsOrWhateverElse });
-
- // after:
- await Primary.play();
-});
-```
-
-In order for this to be possible, the portable stories API now adds a wrapper to your stories with a unique id based on your story id, such as:
+The portable stories API now adds a wrapper to your stories with a unique id based on your story id, such as:
```html