Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disable play functions in docs mode, unless you set parameters.docs.autoplay #19659

Merged
merged 10 commits into from
Nov 4, 2022
7 changes: 7 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
- [External Docs](#external-docs)
- [MDX2 upgrade](#mdx2-upgrade)
- [Dropped addon-docs manual configuration](#dropped-addon-docs-manual-configuration)
- [Autoplay in docs](#autoplay-in-docs)
- [7.0 Deprecations](#70-deprecations)
- [`Story` type deprecated](#story-type-deprecated)
- [`ComponentStory`, `ComponentStoryObj`, `ComponentStoryFn` and `ComponentMeta` types are deprecated](#componentstory-componentstoryobj-componentstoryfn-and-componentmeta-types-are-deprecated)
Expand Down Expand Up @@ -741,6 +742,12 @@ As part of the upgrade we deleted the codemod `mdx-to-csf` and will be replacing

Storybook Docs 5.x shipped with instructions for how to manually configure webpack and storybook without the use of Storybook's "presets" feature. Over time, these docs went out of sync. Now in Storybook 7 we have removed support for manual configuration entirely.

#### Autoplay in docs

Running play functions in docs is generally tricky, as they can steal focus and cause the window to scroll. Consequently, we've disabled play functions in docs by default.

If your story depends on a play function to render correctly, _and_ you are confident the function autoplaying won't mess up your docs, you can set `parameters.docs.autoplay = true` to have it auto play.

### 7.0 Deprecations

#### `Story` type deprecated
Expand Down
33 changes: 33 additions & 0 deletions code/addons/docs/template/stories/docspage/autoplay.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import globalThis from 'global';
import { expect } from '@storybook/jest';
import { within } from '@storybook/testing-library';

export default {
component: globalThis.Components.Pre,
tags: ['docsPage'],
args: { text: 'Play has not run' },
parameters: { chromatic: { disable: true } },
};

// Should not autoplay
export const NoAutoplay = {
play: async ({ viewMode, canvasElement }) => {
const pre = await within(canvasElement).findByText('Play has not run');
if (viewMode === 'docs') {
pre.innerText = 'Play should not have run!';
// Sort of pointless
expect(viewMode).not.toBe('docs');
} else {
pre.innerText = 'Play has run';
}
},
};

// Should autoplay
export const Autoplay = {
parameters: { docs: { autoplay: true } },
play: async ({ canvasElement }) => {
const pre = await within(canvasElement).findByText('Play has not run');
pre.innerText = 'Play has run';
},
};
12 changes: 12 additions & 0 deletions code/e2e-tests/addon-docs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,16 @@ test.describe('addon-docs', () => {
await expect(text).not.toMatch(/^\(args\) => /);
}
});

test('should not run autoplay stories without parameter', async ({ page }) => {
const sbPage = new SbPage(page);
await sbPage.navigateToStory('addons/docs/docspage/autoplay', 'docs');

const root = sbPage.previewRoot();
const autoplayPre = root.locator('#story--addons-docs-docspage-autoplay--autoplay pre');
await expect(autoplayPre).toHaveText('Play has run');

const noAutoplayPre = root.locator('#story--addons-docs-docspage-autoplay--no-autoplay pre');
await expect(noAutoplayPre).toHaveText('Play has not run');
});
});
9 changes: 7 additions & 2 deletions code/lib/preview-web/src/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import type {
} from '@storybook/types';
import { StoryStore } from '@storybook/store';

import { StoryRender } from './render/StoryRender';
import { StoryRender, StoryRenderOptions } from './render/StoryRender';
import { TemplateDocsRender } from './render/TemplateDocsRender';
import { StandaloneDocsRender } from './render/StandaloneDocsRender';

Expand Down Expand Up @@ -304,7 +304,11 @@ export class Preview<TFramework extends AnyFramework> {
// main to be consistent with the previous behaviour. In the future,
// we will change it to go ahead and load the story, which will end up being
// "instant", although async.
renderStoryToElement(story: Store_Story<TFramework>, element: HTMLElement) {
renderStoryToElement(
story: Store_Story<TFramework>,
element: HTMLElement,
options: StoryRenderOptions
) {
if (!this.renderToDOM)
throw new Error(`Cannot call renderStoryToElement before initialization`);

Expand All @@ -315,6 +319,7 @@ export class Preview<TFramework extends AnyFramework> {
this.inlineStoryCallbacks(story.id),
story.id,
'docs',
options,
story
);
render.renderToElement(element);
Expand Down
4 changes: 3 additions & 1 deletion code/lib/preview-web/src/docs-context/DocsContextProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
StoryName,
} from '@storybook/types';
import type { Channel } from '@storybook/channels';
import { StoryRenderOptions } from '../render/StoryRender';

export interface DocsContextProps<TFramework extends AnyFramework = AnyFramework> {
/**
Expand Down Expand Up @@ -54,7 +55,8 @@ export interface DocsContextProps<TFramework extends AnyFramework = AnyFramework
*/
renderStoryToElement: (
story: Store_Story<TFramework>,
element: HTMLElement
element: HTMLElement,
options: StoryRenderOptions
) => () => Promise<void>;

/**
Expand Down
7 changes: 6 additions & 1 deletion code/lib/preview-web/src/render/Render.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { StoryId, AnyFramework } from '@storybook/types';
import type { StoryRenderOptions } from './StoryRender';

export type RenderType = 'story' | 'docs';

Expand All @@ -17,7 +18,11 @@ export interface Render<TFramework extends AnyFramework> {
disableKeyListeners: boolean;
teardown?: (options: { viewModeChanged: boolean }) => Promise<void>;
torndown: boolean;
renderToElement: (canvasElement: HTMLElement, renderStoryToElement?: any) => Promise<void>;
renderToElement: (
canvasElement: HTMLElement,
renderStoryToElement?: any,
options?: StoryRenderOptions
) => Promise<void>;
}

export const PREPARE_ABORTED = new Error('prepareAborted');
52 changes: 52 additions & 0 deletions code/lib/preview-web/src/render/StoryRender.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,56 @@ describe('StoryRender', () => {

await expect(preparePromise).rejects.toThrowError(PREPARE_ABORTED);
});

it('does run play function if passed runPlayFunction=true', async () => {
const story = {
id: 'id',
title: 'title',
name: 'name',
tags: [],
applyLoaders: jest.fn(),
unboundStoryFn: jest.fn(),
playFunction: jest.fn(),
};

const render = new StoryRender(
new Channel(),
{ getStoryContext: () => ({}) } as any,
jest.fn() as any,
{} as any,
entry.id,
'story',
{ runPlayFunction: true },
story as any
);

await render.renderToElement({} as any);
expect(story.playFunction).toHaveBeenCalled();
});

it('does not run play function if passed runPlayFunction=false', async () => {
const story = {
id: 'id',
title: 'title',
name: 'name',
tags: [],
applyLoaders: jest.fn(),
unboundStoryFn: jest.fn(),
playFunction: jest.fn(),
};

const render = new StoryRender(
new Channel(),
{ getStoryContext: () => ({}) } as any,
jest.fn() as any,
{} as any,
entry.id,
'story',
{ runPlayFunction: false },
story as any
);

await render.renderToElement({} as any);
expect(story.playFunction).not.toHaveBeenCalled();
});
});
12 changes: 11 additions & 1 deletion code/lib/preview-web/src/render/StoryRender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export type RenderContextCallbacks<TFramework extends AnyFramework> = Pick<
'showMain' | 'showError' | 'showException'
>;

export type StoryRenderOptions = {
runPlayFunction?: boolean;
};

export class StoryRender<TFramework extends AnyFramework> implements Render<TFramework> {
public type: RenderType = 'story';

Expand All @@ -72,6 +76,7 @@ export class StoryRender<TFramework extends AnyFramework> implements Render<TFra
private callbacks: RenderContextCallbacks<TFramework>,
public id: StoryId,
public viewMode: ViewMode,
public renderOptions: StoryRenderOptions = { runPlayFunction: true },
story?: Store_Story<TFramework>
) {
this.abortController = new AbortController();
Expand Down Expand Up @@ -218,7 +223,12 @@ export class StoryRender<TFramework extends AnyFramework> implements Render<TFra
if (abortSignal.aborted) return;

// The phase should be 'rendering' but it might be set to 'aborted' by another render cycle
if (forceRemount && playFunction && this.phase !== 'errored') {
if (
this.renderOptions.runPlayFunction &&
forceRemount &&
playFunction &&
this.phase !== 'errored'
) {
this.disableKeyListeners = true;
try {
await this.runPhase(abortSignal, 'playing', async () => {
Expand Down
5 changes: 3 additions & 2 deletions code/ui/blocks/src/blocks/Story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,12 @@ const Story: FC<StoryProps> = (props) => {
let cleanup: () => void;
if (story && storyRef.current) {
const element = storyRef.current as HTMLElement;
cleanup = context.renderStoryToElement(story, element);
const runPlayFunction = story.parameters.docs?.autoplay;
cleanup = context.renderStoryToElement(story, element, { runPlayFunction });
setShowLoader(false);
}
return () => cleanup && cleanup();
}, [story]);
}, [context, story]);

if (!story) {
return <StorySkeleton />;
Expand Down