diff --git a/code/addons/docs/docs/multiframework.md b/code/addons/docs/docs/multiframework.md
index aeaa394ea3ba..f852c8d42ebe 100644
--- a/code/addons/docs/docs/multiframework.md
+++ b/code/addons/docs/docs/multiframework.md
@@ -129,7 +129,9 @@ export const jsxDecorator = (storyFn: any, context: StoryContext) => {
const options = {}; // retrieve from story parameters
const jsx = renderJsx(story, options);
- channel.emit(SNIPPET_RENDERED, (context || {}).id, jsx);
+
+ const { id, args } = context;
+ channel.emit(SNIPPET_RENDERED, { id, args, source: jsx });
return story;
};
diff --git a/code/e2e-tests/addon-docs.spec.ts b/code/e2e-tests/addon-docs.spec.ts
index 37048da3d882..c5298b19cecc 100644
--- a/code/e2e-tests/addon-docs.spec.ts
+++ b/code/e2e-tests/addon-docs.spec.ts
@@ -41,6 +41,56 @@ test.describe('addon-docs', () => {
}
});
+ test('source snippet should not change in stories block', async ({ page }) => {
+ const skipped = [
+ 'vue3',
+ 'vue-cli',
+ 'preact',
+ // NextJS snippets are broken, see: https://github.com/storybookjs/storybook/issues/20356
+ 'nextjs',
+ // SSv6 does not render stories in the correct order in our sandboxes
+ 'internal\\/ssv6',
+ // Angular bug: https://github.com/storybookjs/storybook/issues/21066
+ 'angular',
+ // Lit seems to render incorrectly for our template-stories but not real stories
+ // - template: https://638db567ed97c3fb3e21cc22-ulhjwkqzzj.chromatic.com/?path=/docs/addons-docs-docspage-basic--docs
+ // - real: https://638db567ed97c3fb3e21cc22-ulhjwkqzzj.chromatic.com/?path=/docs/example-button--docs
+ 'lit-vite',
+ // Vue doesn't update when you change args, apparently fixed by this:
+ // https://github.com/storybookjs/storybook/pull/20995
+ 'vue2-vite',
+ ];
+ test.skip(
+ new RegExp(`^${skipped.join('|')}`, 'i').test(`${templateName}`),
+ `Skipping ${templateName}, which does not support dynamic source snippets`
+ );
+
+ const sbPage = new SbPage(page);
+ await sbPage.navigateToStory('addons/docs/docspage/basic', 'docs');
+ const root = sbPage.previewRoot();
+ const toggles = root.locator('.docblock-code-toggle');
+
+ // Open up the first and second code toggle (i.e the "Basic" story outside and inside the Stories block)
+ await (await toggles.nth(0)).click({ force: true });
+ await (await toggles.nth(1)).click({ force: true });
+
+ // Check they both say "Basic"
+ const codes = root.locator('pre.prismjs');
+ const primaryCode = await codes.nth(0);
+ const storiesCode = await codes.nth(1);
+ await expect(primaryCode).toContainText('Basic');
+ await expect(storiesCode).toContainText('Basic');
+
+ const labelControl = root.locator('textarea[name=label]');
+ labelControl.fill('Changed');
+ labelControl.blur();
+
+ // Check the Primary one has changed
+ await expect(primaryCode).toContainText('Changed');
+ // Check the stories one still says "Basic"
+ await expect(storiesCode).toContainText('Basic');
+ });
+
test('should not run autoplay stories without parameter', async ({ page }) => {
const sbPage = new SbPage(page);
await sbPage.navigateToStory('addons/docs/docspage/autoplay', 'docs');
diff --git a/code/frameworks/angular/src/client/docs/sourceDecorator.ts b/code/frameworks/angular/src/client/docs/sourceDecorator.ts
index b60ad0fa9a40..e86df2a05835 100644
--- a/code/frameworks/angular/src/client/docs/sourceDecorator.ts
+++ b/code/frameworks/angular/src/client/docs/sourceDecorator.ts
@@ -38,7 +38,8 @@ export const sourceDecorator = (
useEffect(() => {
if (toEmit) {
- channel.emit(SNIPPET_RENDERED, context.id, toEmit, 'angular');
+ const { id, args } = context;
+ channel.emit(SNIPPET_RENDERED, { id, args, source: toEmit, format: 'angular' });
}
});
diff --git a/code/renderers/html/src/docs/sourceDecorator.test.ts b/code/renderers/html/src/docs/sourceDecorator.test.ts
index 06534288bf71..deadcfe315ef 100644
--- a/code/renderers/html/src/docs/sourceDecorator.test.ts
+++ b/code/renderers/html/src/docs/sourceDecorator.test.ts
@@ -47,11 +47,11 @@ describe('sourceDecorator', () => {
const context = makeContext('args', { __isArgsStory: true }, {});
sourceDecorator(storyFn, context);
await tick();
- expect(mockChannel.emit).toHaveBeenCalledWith(
- SNIPPET_RENDERED,
- 'html-test--args',
- '
args story
'
- );
+ expect(mockChannel.emit).toHaveBeenCalledWith(SNIPPET_RENDERED, {
+ id: 'html-test--args',
+ args: {},
+ source: 'args story
',
+ });
});
it('should dedent source by default', async () => {
@@ -63,11 +63,11 @@ describe('sourceDecorator', () => {
const context = makeContext('args', { __isArgsStory: true }, {});
sourceDecorator(storyFn, context);
await tick();
- expect(mockChannel.emit).toHaveBeenCalledWith(
- SNIPPET_RENDERED,
- 'html-test--args',
- ['', ' args story', '
'].join('\n')
- );
+ expect(mockChannel.emit).toHaveBeenCalledWith(SNIPPET_RENDERED, {
+ id: 'html-test--args',
+ args: {},
+ source: ['', ' args story', '
'].join('\n'),
+ });
});
it('should skip dynamic rendering for no-args stories', async () => {
@@ -98,11 +98,11 @@ describe('sourceDecorator', () => {
);
sourceDecorator(decoratedStoryFn, context);
await tick();
- expect(mockChannel.emit).toHaveBeenCalledWith(
- SNIPPET_RENDERED,
- 'html-test--args',
- 'args story
'
- );
+ expect(mockChannel.emit).toHaveBeenCalledWith(SNIPPET_RENDERED, {
+ id: 'html-test--args',
+ args: {},
+ source: 'args story
',
+ });
});
it('allows the snippet output to be modified by transformSource', async () => {
@@ -112,11 +112,11 @@ describe('sourceDecorator', () => {
const context = makeContext('args', { __isArgsStory: true, docs }, {});
sourceDecorator(storyFn, context);
await tick();
- expect(mockChannel.emit).toHaveBeenCalledWith(
- SNIPPET_RENDERED,
- 'html-test--args',
- 'args story
'
- );
+ expect(mockChannel.emit).toHaveBeenCalledWith(SNIPPET_RENDERED, {
+ id: 'html-test--args',
+ args: {},
+ source: 'args story
',
+ });
});
it('provides the story context to transformSource', () => {
diff --git a/code/renderers/html/src/docs/sourceDecorator.ts b/code/renderers/html/src/docs/sourceDecorator.ts
index 459ea267ce9f..9630dcda9a04 100644
--- a/code/renderers/html/src/docs/sourceDecorator.ts
+++ b/code/renderers/html/src/docs/sourceDecorator.ts
@@ -54,7 +54,8 @@ export function sourceDecorator(storyFn: PartialStoryFn, context:
}
}
useEffect(() => {
- if (source) addons.getChannel().emit(SNIPPET_RENDERED, context.id, source);
+ const { id, args } = context;
+ if (source) addons.getChannel().emit(SNIPPET_RENDERED, { id, args, source });
});
return story;
diff --git a/code/renderers/react/src/docs/jsxDecorator.test.tsx b/code/renderers/react/src/docs/jsxDecorator.test.tsx
index 9adeea67d5ec..aee1a97685db 100644
--- a/code/renderers/react/src/docs/jsxDecorator.test.tsx
+++ b/code/renderers/react/src/docs/jsxDecorator.test.tsx
@@ -203,11 +203,11 @@ describe('jsxDecorator', () => {
const context = makeContext('args', { __isArgsStory: true }, {});
jsxDecorator(storyFn, context);
await new Promise((r) => setTimeout(r, 0));
- expect(mockChannel.emit).toHaveBeenCalledWith(
- SNIPPET_RENDERED,
- 'jsx-test--args',
- '\n args story\n
'
- );
+ expect(mockChannel.emit).toHaveBeenCalledWith(SNIPPET_RENDERED, {
+ id: 'jsx-test--args',
+ args: {},
+ source: '\n args story\n
',
+ });
});
it('should not render decorators when provided excludeDecorators parameter', async () => {
@@ -231,11 +231,11 @@ describe('jsxDecorator', () => {
jsxDecorator(decoratedStoryFn, context);
await new Promise((r) => setTimeout(r, 0));
- expect(mockChannel.emit).toHaveBeenCalledWith(
- SNIPPET_RENDERED,
- 'jsx-test--args',
- '\n args story\n
'
- );
+ expect(mockChannel.emit).toHaveBeenCalledWith(SNIPPET_RENDERED, {
+ id: 'jsx-test--args',
+ args: {},
+ source: '\n args story\n
',
+ });
});
it('should skip dynamic rendering for no-args stories', async () => {
@@ -255,11 +255,11 @@ describe('jsxDecorator', () => {
jsxDecorator(storyFn, context);
await new Promise((r) => setTimeout(r, 0));
- expect(mockChannel.emit).toHaveBeenCalledWith(
- SNIPPET_RENDERED,
- 'jsx-test--args',
- '\n args story\n
'
- );
+ expect(mockChannel.emit).toHaveBeenCalledWith(SNIPPET_RENDERED, {
+ id: 'jsx-test--args',
+ args: {},
+ source: '\n args story\n
',
+ });
});
it('provides the story context to transformSource', () => {
@@ -286,11 +286,11 @@ describe('jsxDecorator', () => {
jsxDecorator(() => mdxElement, makeContext('mdx-args', { __isArgsStory: true }, {}));
await new Promise((r) => setTimeout(r, 0));
- expect(mockChannel.emit).toHaveBeenCalledWith(
- SNIPPET_RENDERED,
- 'jsx-test--mdx-args',
- ''
- );
+ expect(mockChannel.emit).toHaveBeenCalledWith(SNIPPET_RENDERED, {
+ id: 'jsx-test--mdx-args',
+ args: {},
+ source: '',
+ });
});
it('handles stories that trigger Suspense', async () => {
@@ -314,12 +314,15 @@ describe('jsxDecorator', () => {
await new Promise((r) => setTimeout(r, 0));
expect(mockChannel.emit).toHaveBeenCalledTimes(2);
- expect(mockChannel.emit).nthCalledWith(1, SNIPPET_RENDERED, 'jsx-test--args', '');
- expect(mockChannel.emit).nthCalledWith(
- 2,
- SNIPPET_RENDERED,
- 'jsx-test--args',
- '\n resolved args story\n
'
- );
+ expect(mockChannel.emit).nthCalledWith(1, SNIPPET_RENDERED, {
+ id: 'jsx-test--args',
+ args: {},
+ source: '',
+ });
+ expect(mockChannel.emit).nthCalledWith(2, SNIPPET_RENDERED, {
+ id: 'jsx-test--args',
+ args: {},
+ source: '\n resolved args story\n
',
+ });
});
});
diff --git a/code/renderers/react/src/docs/jsxDecorator.tsx b/code/renderers/react/src/docs/jsxDecorator.tsx
index 3042618fe552..e86e1a38db03 100644
--- a/code/renderers/react/src/docs/jsxDecorator.tsx
+++ b/code/renderers/react/src/docs/jsxDecorator.tsx
@@ -190,7 +190,12 @@ export const jsxDecorator = (
useEffect(() => {
if (!skip) {
- channel.emit(SNIPPET_RENDERED, (context || {}).id, jsx);
+ const { id, args } = context;
+ channel.emit(SNIPPET_RENDERED, {
+ id,
+ source: jsx,
+ args,
+ });
}
});
diff --git a/code/renderers/svelte/src/docs/sourceDecorator.ts b/code/renderers/svelte/src/docs/sourceDecorator.ts
index 6f9dbdb282c6..efc3852255b1 100644
--- a/code/renderers/svelte/src/docs/sourceDecorator.ts
+++ b/code/renderers/svelte/src/docs/sourceDecorator.ts
@@ -157,7 +157,8 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) =
useEffect(() => {
if (!skip && source) {
- channel.emit(SNIPPET_RENDERED, (context || {}).id, source);
+ const { id, args } = context;
+ channel.emit(SNIPPET_RENDERED, { id, args, source });
}
});
diff --git a/code/renderers/vue/src/docs/sourceDecorator.ts b/code/renderers/vue/src/docs/sourceDecorator.ts
index 8c9839acb138..8d64ffbb217b 100644
--- a/code/renderers/vue/src/docs/sourceDecorator.ts
+++ b/code/renderers/vue/src/docs/sourceDecorator.ts
@@ -54,7 +54,13 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) => {
// @ts-expect-error TS says it is called $vnode
const code = vnodeToString(storyNode._vnode);
- channel.emit(SNIPPET_RENDERED, (context || {}).id, `${code}`, 'vue');
+ const { id, args } = context;
+ channel.emit(SNIPPET_RENDERED, {
+ id,
+ args,
+ source: `${code}`,
+ format: 'vue',
+ });
} catch (e) {
logger.warn(`Failed to generate dynamic story source: ${e}`);
}
diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts
index 182b8b6a758e..93a2fbd18c38 100644
--- a/code/renderers/vue3/src/docs/sourceDecorator.ts
+++ b/code/renderers/vue3/src/docs/sourceDecorator.ts
@@ -289,7 +289,8 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) =
useEffect(() => {
if (!skip && source) {
- channel.emit(SNIPPET_RENDERED, (context || {}).id, source, 'vue');
+ const { id, args } = context;
+ channel.emit(SNIPPET_RENDERED, { id, args, source, format: 'vue' });
}
});
diff --git a/code/renderers/web-components/src/docs/sourceDecorator.test.ts b/code/renderers/web-components/src/docs/sourceDecorator.test.ts
index 23b805609acf..0a0108ca38f3 100644
--- a/code/renderers/web-components/src/docs/sourceDecorator.test.ts
+++ b/code/renderers/web-components/src/docs/sourceDecorator.test.ts
@@ -43,11 +43,11 @@ describe('sourceDecorator', () => {
const context = makeContext('args', { __isArgsStory: true }, {});
sourceDecorator(storyFn, context);
await tick();
- expect(mockChannel.emit).toHaveBeenCalledWith(
- SNIPPET_RENDERED,
- 'lit-test--args',
- 'args story
'
- );
+ expect(mockChannel.emit).toHaveBeenCalledWith(SNIPPET_RENDERED, {
+ id: 'lit-test--args',
+ args: {},
+ source: 'args story
',
+ });
});
it('should skip dynamic rendering for no-args stories', async () => {
@@ -78,11 +78,11 @@ describe('sourceDecorator', () => {
);
sourceDecorator(decoratedStoryFn, context);
await tick();
- expect(mockChannel.emit).toHaveBeenCalledWith(
- SNIPPET_RENDERED,
- 'lit-test--args',
- 'args story
'
- );
+ expect(mockChannel.emit).toHaveBeenCalledWith(SNIPPET_RENDERED, {
+ id: 'lit-test--args',
+ args: {},
+ source: 'args story
',
+ });
});
it('allows the snippet output to be modified by transformSource', async () => {
@@ -92,11 +92,11 @@ describe('sourceDecorator', () => {
const context = makeContext('args', { __isArgsStory: true, docs }, {});
sourceDecorator(storyFn, context);
await tick();
- expect(mockChannel.emit).toHaveBeenCalledWith(
- SNIPPET_RENDERED,
- 'lit-test--args',
- 'args story
'
- );
+ expect(mockChannel.emit).toHaveBeenCalledWith(SNIPPET_RENDERED, {
+ id: 'lit-test--args',
+ args: {},
+ source: 'args story
',
+ });
});
it('provides the story context to transformSource', () => {
@@ -120,10 +120,10 @@ describe('sourceDecorator', () => {
const boundStoryFn = storyFn.bind(null, context.args);
sourceDecorator(boundStoryFn, context);
await tick();
- expect(mockChannel.emit).toHaveBeenCalledWith(
- SNIPPET_RENDERED,
- 'lit-test--args',
- 'some content
'
- );
+ expect(mockChannel.emit).toHaveBeenCalledWith(SNIPPET_RENDERED, {
+ id: 'lit-test--args',
+ args: { slot: 'some content' },
+ source: 'some content
',
+ });
});
});
diff --git a/code/renderers/web-components/src/docs/sourceDecorator.ts b/code/renderers/web-components/src/docs/sourceDecorator.ts
index ad9da01a9f8a..674fde3e48e2 100644
--- a/code/renderers/web-components/src/docs/sourceDecorator.ts
+++ b/code/renderers/web-components/src/docs/sourceDecorator.ts
@@ -43,7 +43,8 @@ export function sourceDecorator(
let source: string;
useEffect(() => {
- if (source) addons.getChannel().emit(SNIPPET_RENDERED, context.id, source);
+ const { id, args } = context;
+ if (source) addons.getChannel().emit(SNIPPET_RENDERED, { id, source, args });
});
if (!skipSourceRender(context)) {
const container = window.document.createElement('div');
diff --git a/code/ui/blocks/package.json b/code/ui/blocks/package.json
index 34a43baa71e8..28af81b5c980 100644
--- a/code/ui/blocks/package.json
+++ b/code/ui/blocks/package.json
@@ -62,6 +62,7 @@
"memoizerific": "^1.11.3",
"polished": "^4.2.2",
"react-colorful": "^5.1.2",
+ "telejson": "^7.0.3",
"ts-dedent": "^2.0.0",
"util-deprecate": "^1.0.2"
},
diff --git a/code/ui/blocks/src/blocks/Controls.tsx b/code/ui/blocks/src/blocks/Controls.tsx
index 390b2f2e3d66..8a70a4a29520 100644
--- a/code/ui/blocks/src/blocks/Controls.tsx
+++ b/code/ui/blocks/src/blocks/Controls.tsx
@@ -1,20 +1,16 @@
/* eslint-disable react/destructuring-assignment */
-import type { Args, Globals, Renderer } from '@storybook/csf';
-import type { DocsContextProps, ModuleExports, PreparedStory } from '@storybook/types';
+import type { Renderer } from '@storybook/csf';
+import type { ModuleExports } from '@storybook/types';
import type { FC } from 'react';
-import React, { useCallback, useEffect, useState, useContext } from 'react';
-import type { PropDescriptor } from '@storybook/preview-api';
+import React, { useContext } from 'react';
import { filterArgTypes } from '@storybook/preview-api';
-import {
- STORY_ARGS_UPDATED,
- UPDATE_STORY_ARGS,
- RESET_STORY_ARGS,
- GLOBALS_UPDATED,
-} from '@storybook/core-events';
+import type { PropDescriptor } from '@storybook/preview-api';
import type { SortType } from '../components';
import { ArgsTable as PureArgsTable } from '../components';
import { DocsContext } from './DocsContext';
+import { useGlobals } from './useGlobals';
+import { useArgs } from './useArgs';
type ControlsParameters = {
include?: PropDescriptor;
@@ -26,49 +22,6 @@ type ControlsProps = ControlsParameters & {
of?: Renderer['component'] | ModuleExports;
};
-const useArgs = (
- story: PreparedStory,
- context: DocsContextProps
-): [Args, (args: Args) => void, (argNames?: string[]) => void] => {
- const storyContext = context.getStoryContext(story);
- const { id: storyId } = story;
-
- const [args, setArgs] = useState(storyContext.args);
- useEffect(() => {
- const onArgsUpdated = (changed: { storyId: string; args: Args }) => {
- if (changed.storyId === storyId) {
- setArgs(changed.args);
- }
- };
- context.channel.on(STORY_ARGS_UPDATED, onArgsUpdated);
- return () => context.channel.off(STORY_ARGS_UPDATED, onArgsUpdated);
- }, [storyId, context.channel]);
- const updateArgs = useCallback(
- (updatedArgs) => context.channel.emit(UPDATE_STORY_ARGS, { storyId, updatedArgs }),
- [storyId, context.channel]
- );
- const resetArgs = useCallback(
- (argNames?: string[]) => context.channel.emit(RESET_STORY_ARGS, { storyId, argNames }),
- [storyId, context.channel]
- );
- return [args, updateArgs, resetArgs];
-};
-
-const useGlobals = (story: PreparedStory, context: DocsContextProps): [Globals] => {
- const storyContext = context.getStoryContext(story);
-
- const [globals, setGlobals] = useState(storyContext.globals);
- useEffect(() => {
- const onGlobalsUpdated = (changed: { globals: Globals }) => {
- setGlobals(changed.globals);
- };
- context.channel.on(GLOBALS_UPDATED, onGlobalsUpdated);
- return () => context.channel.off(GLOBALS_UPDATED, onGlobalsUpdated);
- }, [context.channel]);
-
- return [globals];
-};
-
export const Controls: FC = (props) => {
const { of } = props;
const context = useContext(DocsContext);
diff --git a/code/ui/blocks/src/blocks/DocsStory.tsx b/code/ui/blocks/src/blocks/DocsStory.tsx
index 70960447402f..b648e67881e3 100644
--- a/code/ui/blocks/src/blocks/DocsStory.tsx
+++ b/code/ui/blocks/src/blocks/DocsStory.tsx
@@ -27,7 +27,12 @@ export const DocsStory: FC = ({
>
)}
-
+
);
};
diff --git a/code/ui/blocks/src/blocks/Source.stories.tsx b/code/ui/blocks/src/blocks/Source.stories.tsx
index d9bd1d747b52..ad70eafedf80 100644
--- a/code/ui/blocks/src/blocks/Source.stories.tsx
+++ b/code/ui/blocks/src/blocks/Source.stories.tsx
@@ -4,18 +4,22 @@ import type { Meta, StoryObj } from '@storybook/react';
import { Source } from './Source';
import * as ParametersStories from '../examples/SourceParameters.stories';
-import { SourceContext } from './SourceContainer';
+import { argsHash, SourceContext } from './SourceContainer';
const meta: Meta = {
component: Source,
parameters: {
relativeCsfPaths: ['../examples/SourceParameters.stories'],
snippets: {
- 'storybook-blocks-example-sourceparameters--no-parameters': {
- code: `const emitted = 'source';`,
+ 'storybook-blocks-examples-stories-for-the-source-block--no-parameters': {
+ [argsHash({})]: {
+ code: `const emitted = 'source';`,
+ },
},
- 'storybook-blocks-example-sourceparameters--type-dynamic': {
- code: `const emitted = 'source';`,
+ 'storybook-blocks-examples-stories-for-the-source-block--type-dynamic': {
+ [argsHash({})]: {
+ code: `const emitted = 'source';`,
+ },
},
},
docsStyles: true,
diff --git a/code/ui/blocks/src/blocks/Source.tsx b/code/ui/blocks/src/blocks/Source.tsx
index 2c6e05f98ca5..335c338dc611 100644
--- a/code/ui/blocks/src/blocks/Source.tsx
+++ b/code/ui/blocks/src/blocks/Source.tsx
@@ -1,6 +1,6 @@
import type { ComponentProps, FC } from 'react';
import React, { useContext } from 'react';
-import type { StoryId, PreparedStory, ModuleExport } from '@storybook/types';
+import type { StoryId, PreparedStory, ModuleExport, Args } from '@storybook/types';
import { SourceType } from '@storybook/docs-tools';
import { deprecate } from '@storybook/client-logger';
@@ -10,9 +10,10 @@ import { Source as PureSource, SourceError } from '../components/Source';
import type { DocsContextProps } from './DocsContext';
import { DocsContext } from './DocsContext';
import type { SourceContextProps, SourceItem } from './SourceContainer';
-import { SourceContext } from './SourceContainer';
+import { UNKNOWN_ARGS_HASH, argsHash, SourceContext } from './SourceContainer';
import { useStories } from './useStory';
+import { useArgsList } from './useArgs';
export enum SourceState {
OPEN = 'open',
@@ -53,6 +54,11 @@ export type SourceProps = Omit {
@@ -62,11 +68,22 @@ const getSourceState = (stories: PreparedStory[]) => {
return states[0];
};
-const getStorySource = (storyId: StoryId, sourceContext: SourceContextProps): SourceItem => {
+const getStorySource = (
+ storyId: StoryId,
+ args: Args,
+ sourceContext: SourceContextProps
+): SourceItem => {
const { sources } = sourceContext;
+
+ const sourceMap = sources?.[storyId];
+ // If the source decorator hasn't provided args, we fallback to the "unknown args"
+ // version of the source (which means if you render a story >1 time with different args
+ // you'll get the same source value both times).
+ const source = sourceMap?.[argsHash(args)] || sourceMap?.[UNKNOWN_ARGS_HASH];
+
// source rendering is async so source is unavailable at the start of the render cycle,
// so we fail gracefully here without warning
- return sources?.[storyId] || { code: '' };
+ return source || { code: '' };
};
const getSnippet = (
@@ -106,9 +123,6 @@ export const useSourceProps = (
): PureSourceProps & SourceStateProps => {
const storyIds = props.ids || (props.id ? [props.id] : []);
const storiesFromIds = useStories(storyIds, docsContext);
- if (!storiesFromIds.every(Boolean)) {
- return { error: SourceError.SOURCE_UNAVAILABLE, state: SourceState.NONE };
- }
// The check didn't actually change the type.
let stories: PreparedStory[] = storiesFromIds as PreparedStory[];
@@ -123,6 +137,11 @@ export const useSourceProps = (
// You are allowed to use and unattached.
}
}
+ const argsFromStories = useArgsList(stories, docsContext);
+
+ if (!storiesFromIds.every(Boolean)) {
+ return { error: SourceError.SOURCE_UNAVAILABLE, state: SourceState.NONE };
+ }
const sourceParameters = (stories[0]?.parameters?.docs?.source || {}) as SourceParameters;
let { code } = props; // We will fall back to `sourceParameters.code`, but per story below
@@ -133,7 +152,18 @@ export const useSourceProps = (
if (!code) {
code = stories
.map((story, index) => {
- const source = getStorySource(story.id, sourceContext);
+ // In theory you can use a storyId from a different CSF file that hasn't loaded yet.
+ if (!story) return '';
+
+ // NOTE: args *does* have to be defined here due to the null check on story above
+ const [args] = argsFromStories[index] || [];
+
+ // eslint-disable-next-line no-underscore-dangle
+ const argsForSource = props.__forceInitialArgs
+ ? docsContext.getStoryContext(story).initialArgs
+ : args;
+
+ const source = getStorySource(story.id, argsForSource, sourceContext);
if (index === 0) {
// Take the format from the first story
format = source.format ?? story.parameters.docs?.source?.format ?? false;
diff --git a/code/ui/blocks/src/blocks/SourceContainer.tsx b/code/ui/blocks/src/blocks/SourceContainer.tsx
index ffc78191de28..d12e74ec36f1 100644
--- a/code/ui/blocks/src/blocks/SourceContainer.tsx
+++ b/code/ui/blocks/src/blocks/SourceContainer.tsx
@@ -6,13 +6,21 @@ import type { Channel } from '@storybook/channels';
import { SNIPPET_RENDERED } from '@storybook/docs-tools';
import type { SyntaxHighlighterFormatTypes } from '@storybook/components';
-import type { StoryId } from '@storybook/types';
+import type { StoryId, Args } from '@storybook/types';
+
+import { stringify } from 'telejson';
+
+type ArgsHash = string;
+export function argsHash(args: Args): ArgsHash {
+ return stringify(args);
+}
export interface SourceItem {
code: string;
format?: SyntaxHighlighterFormatTypes;
}
-export type StorySources = Record;
+
+export type StorySources = Record>;
export interface SourceContextProps {
sources: StorySources;
@@ -21,24 +29,51 @@ export interface SourceContextProps {
export const SourceContext: Context = createContext({ sources: {} });
+type SnippetRenderedEvent = {
+ id: StoryId;
+ source: string;
+ args?: Args;
+ format?: SyntaxHighlighterFormatTypes;
+};
+
+export const UNKNOWN_ARGS_HASH = '--unknown--';
+
export const SourceContainer: FC<{ channel: Channel }> = ({ children, channel }) => {
const [sources, setSources] = useState({});
useEffect(() => {
const handleSnippetRendered = (
- id: StoryId,
- newSource: string,
- format: SyntaxHighlighterFormatTypes = false
+ idOrEvent: StoryId | SnippetRenderedEvent,
+ inputSource: string = null,
+ inputFormat: SyntaxHighlighterFormatTypes = false
) => {
+ const {
+ id,
+ args = undefined,
+ source,
+ format,
+ } = typeof idOrEvent === 'string'
+ ? {
+ id: idOrEvent,
+ source: inputSource,
+ format: inputFormat,
+ }
+ : idOrEvent;
+
+ const hash = args ? argsHash(args) : UNKNOWN_ARGS_HASH;
+
// optimization: if the source is the same, ignore the incoming event
- if (sources[id] && sources[id].code === newSource) {
+ if (sources[id] && sources[id][hash] && sources[id][hash].code === source) {
return;
}
setSources((current) => {
const newSources = {
...current,
- [id]: { code: newSource, format },
+ [id]: {
+ ...current[id],
+ [hash]: { code: source, format },
+ },
};
if (!deepEqual(current, newSources)) {
diff --git a/code/ui/blocks/src/blocks/useArgs.ts b/code/ui/blocks/src/blocks/useArgs.ts
new file mode 100644
index 000000000000..77859f20f027
--- /dev/null
+++ b/code/ui/blocks/src/blocks/useArgs.ts
@@ -0,0 +1,48 @@
+import { useCallback, useEffect, useState } from 'react';
+import type { Args, DocsContextProps, PreparedStory, StoryId, Renderer } from '@storybook/types';
+import { STORY_ARGS_UPDATED, UPDATE_STORY_ARGS, RESET_STORY_ARGS } from '@storybook/core-events';
+import { useStories } from './useStory';
+
+export const useArgs = (
+ story: PreparedStory,
+ context: DocsContextProps
+): [Args, (args: Args) => void, (argNames?: string[]) => void] => {
+ const result = useArgsIfDefined(story, context);
+ if (!result) throw new Error('No result when story was defined');
+ return result;
+};
+
+export const useArgsIfDefined = (
+ story: PreparedStory | void,
+ context: DocsContextProps
+): [Args, (args: Args) => void, (argNames?: string[]) => void] | void => {
+ const storyContext = story ? context.getStoryContext(story) : { args: {} };
+ const { id: storyId } = story || { id: 'none' };
+
+ const [args, setArgs] = useState(storyContext.args);
+ useEffect(() => {
+ const onArgsUpdated = (changed: { storyId: string; args: Args }) => {
+ if (changed.storyId === storyId) {
+ setArgs(changed.args);
+ }
+ };
+ context.channel.on(STORY_ARGS_UPDATED, onArgsUpdated);
+ return () => context.channel.off(STORY_ARGS_UPDATED, onArgsUpdated);
+ }, [storyId, context.channel]);
+ const updateArgs = useCallback(
+ (updatedArgs) => context.channel.emit(UPDATE_STORY_ARGS, { storyId, updatedArgs }),
+ [storyId, context.channel]
+ );
+ const resetArgs = useCallback(
+ (argNames?: string[]) => context.channel.emit(RESET_STORY_ARGS, { storyId, argNames }),
+ [storyId, context.channel]
+ );
+ return story && [args, updateArgs, resetArgs];
+};
+
+export function useArgsList(
+ stories: (PreparedStory | void)[],
+ context: DocsContextProps
+) {
+ return stories.map((story) => useArgsIfDefined(story, context));
+}
diff --git a/code/ui/blocks/src/blocks/useGlobals.ts b/code/ui/blocks/src/blocks/useGlobals.ts
new file mode 100644
index 000000000000..7c0552705d5d
--- /dev/null
+++ b/code/ui/blocks/src/blocks/useGlobals.ts
@@ -0,0 +1,19 @@
+import type { Globals } from '@storybook/csf';
+import type { DocsContextProps, PreparedStory } from '@storybook/types';
+import { useEffect, useState } from 'react';
+import { GLOBALS_UPDATED } from '@storybook/core-events';
+
+export const useGlobals = (story: PreparedStory, context: DocsContextProps): [Globals] => {
+ const storyContext = context.getStoryContext(story);
+
+ const [globals, setGlobals] = useState(storyContext.globals);
+ useEffect(() => {
+ const onGlobalsUpdated = (changed: { globals: Globals }) => {
+ setGlobals(changed.globals);
+ };
+ context.channel.on(GLOBALS_UPDATED, onGlobalsUpdated);
+ return () => context.channel.off(GLOBALS_UPDATED, onGlobalsUpdated);
+ }, [context.channel]);
+
+ return [globals];
+};
diff --git a/code/yarn.lock b/code/yarn.lock
index 92626af85812..438a77dbb817 100644
--- a/code/yarn.lock
+++ b/code/yarn.lock
@@ -5724,6 +5724,7 @@ __metadata:
memoizerific: ^1.11.3
polished: ^4.2.2
react-colorful: ^5.1.2
+ telejson: ^7.0.3
ts-dedent: ^2.0.0
util-deprecate: ^1.0.2
peerDependencies: