Skip to content

Commit

Permalink
Merge pull request #19659 from storybookjs/tom/sb-888-disable-play-fu…
Browse files Browse the repository at this point in the history
…nctions-in-docs-if-not

Disable play functions in docs mode, unless you set `parameters.docs.autoplay`
  • Loading branch information
tmeasday authored Nov 4, 2022
2 parents cc02a5c + aa5a2a1 commit b0b6c6b
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 6 deletions.
7 changes: 7 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
- [MDX2 upgrade](#mdx2-upgrade)
- [Dropped source loader / storiesOf static snippets](#dropped-source-loader--storiesof-static-snippets)
- [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 @@ -780,6 +781,12 @@ module.exports = {

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');
});
});
8 changes: 7 additions & 1 deletion code/lib/preview-web/src/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
} from '@storybook/types';
import { StoryStore } from '@storybook/store';

import type { StoryRenderOptions } from './render/StoryRender';
import { StoryRender } from './render/StoryRender';
import type { TemplateDocsRender } from './render/TemplateDocsRender';
import type { StandaloneDocsRender } from './render/StandaloneDocsRender';
Expand Down Expand Up @@ -304,7 +305,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 +320,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 type { 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 autoplay=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',
{ autoplay: true },
story as any
);

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

it('does not run play function if passed autoplay=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',
{ autoplay: false },
story as any
);

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

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

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

Expand All @@ -73,6 +77,7 @@ export class StoryRender<TFramework extends AnyFramework> implements Render<TFra
private callbacks: RenderContextCallbacks<TFramework>,
public id: StoryId,
public viewMode: ViewMode,
public renderOptions: StoryRenderOptions = { autoplay: true },
story?: Store_Story<TFramework>
) {
this.abortController = new AbortController();
Expand Down Expand Up @@ -220,7 +225,7 @@ 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.autoplay && 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 @@ -90,11 +90,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 { autoplay } = story.parameters.docs || {};
cleanup = context.renderStoryToElement(story, element, { autoplay });
setShowLoader(false);
}
return () => cleanup && cleanup();
}, [story]);
}, [context, story]);

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

0 comments on commit b0b6c6b

Please sign in to comment.