diff --git a/code/addons/docs/src/preview.ts b/code/addons/docs/src/preview.ts index 0d1183bd0cf1..d983f454ccd2 100644 --- a/code/addons/docs/src/preview.ts +++ b/code/addons/docs/src/preview.ts @@ -1,8 +1,27 @@ +import type { PreparedStory } from '@storybook/types'; +import { global } from '@storybook/global'; + +const excludeTags = Object.entries(global.TAGS_OPTIONS ?? {}).reduce((acc, entry) => { + const [tag, option] = entry; + if ((option as any).excludeFromDocsStories) { + acc[tag] = true; + } + return acc; +}, {} as Record); + export const parameters: any = { docs: { renderer: async () => { const { DocsRenderer } = (await import('./DocsRenderer')) as any; return new DocsRenderer(); }, + stories: { + filter: (story: PreparedStory) => { + const tags = story.tags || []; + return ( + tags.filter((tag) => excludeTags[tag]).length === 0 && !story.parameters.docs?.disable + ); + }, + }, }, }; diff --git a/code/addons/docs/src/typings.d.ts b/code/addons/docs/src/typings.d.ts index cfa3c4639f8e..a3efeb653c83 100644 --- a/code/addons/docs/src/typings.d.ts +++ b/code/addons/docs/src/typings.d.ts @@ -11,3 +11,5 @@ declare module 'sveltedoc-parser' { declare var FEATURES: import('@storybook/types').StorybookConfigRaw['features']; declare var LOGLEVEL: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent' | undefined; + +declare var TAGS_OPTIONS: import('@storybook/types').TagsOptions; diff --git a/code/builders/builder-manager/src/index.ts b/code/builders/builder-manager/src/index.ts index b7923a64a2ba..d55a8bb2d898 100644 --- a/code/builders/builder-manager/src/index.ts +++ b/code/builders/builder-manager/src/index.ts @@ -138,6 +138,7 @@ const starter: StarterFunction = async function* starterGeneratorFn({ title, logLevel, docsOptions, + tagsOptions, } = await getData(options); yield; @@ -175,6 +176,7 @@ const starter: StarterFunction = async function* starterGeneratorFn({ refs, logLevel, docsOptions, + tagsOptions, options ); @@ -222,6 +224,7 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime, title, logLevel, docsOptions, + tagsOptions, } = await getData(options); yield; @@ -262,6 +265,7 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime, refs, logLevel, docsOptions, + tagsOptions, options ); diff --git a/code/builders/builder-manager/src/utils/data.ts b/code/builders/builder-manager/src/utils/data.ts index eb6754e7635a..2b7fc15e137d 100644 --- a/code/builders/builder-manager/src/utils/data.ts +++ b/code/builders/builder-manager/src/utils/data.ts @@ -14,6 +14,7 @@ export const getData = async (options: Options) => { const logLevel = options.presets.apply('logLevel'); const title = options.presets.apply('title'); const docsOptions = options.presets.apply('docs', {}); + const tagsOptions = options.presets.apply('tags', {}); const template = readTemplate('template.ejs'); const customHead = options.presets.apply('managerHead'); @@ -35,5 +36,6 @@ export const getData = async (options: Options) => { config, logLevel, favicon, + tagsOptions, }; }; diff --git a/code/builders/builder-manager/src/utils/template.ts b/code/builders/builder-manager/src/utils/template.ts index 0d7b67a1dff3..4ccb2d50864a 100644 --- a/code/builders/builder-manager/src/utils/template.ts +++ b/code/builders/builder-manager/src/utils/template.ts @@ -3,7 +3,7 @@ import fs from 'fs-extra'; import { render } from 'ejs'; -import type { DocsOptions, Options, Ref } from '@storybook/types'; +import type { DocsOptions, TagsOptions, Options, Ref } from '@storybook/types'; export const getTemplatePath = async (template: string) => { return join( @@ -34,6 +34,7 @@ export const renderHTML = async ( refs: Promise>, logLevel: Promise, docsOptions: Promise, + tagsOptions: Promise, { versionCheck, previewUrl, configType, ignorePreview }: Options ) => { const titleRef = await title; @@ -52,6 +53,7 @@ export const renderHTML = async ( // These two need to be double stringified because the UI expects a string VERSIONCHECK: JSON.stringify(JSON.stringify(versionCheck), null, 2), PREVIEW_URL: JSON.stringify(previewUrl, null, 2), // global preview URL + TAGS_OPTIONS: JSON.stringify(await tagsOptions, null, 2), }, head: (await customHead) || '', ignorePreview, diff --git a/code/builders/builder-vite/input/iframe.html b/code/builders/builder-vite/input/iframe.html index 867a16a4a223..dd976d6c4ab4 100644 --- a/code/builders/builder-vite/input/iframe.html +++ b/code/builders/builder-vite/input/iframe.html @@ -21,6 +21,7 @@ window.FEATURES = '[FEATURES HERE]'; window.STORIES = '[STORIES HERE]'; window.DOCS_OPTIONS = '[DOCS_OPTIONS HERE]'; + window.TAGS_OPTIONS = '[TAGS_OPTIONS HERE]'; ('OTHER_GLOBLALS HERE'); diff --git a/code/builders/builder-vite/src/transform-iframe-html.ts b/code/builders/builder-vite/src/transform-iframe-html.ts index 8c0546125162..a4a482b2f119 100644 --- a/code/builders/builder-vite/src/transform-iframe-html.ts +++ b/code/builders/builder-vite/src/transform-iframe-html.ts @@ -1,5 +1,5 @@ import { normalizeStories } from '@storybook/core-common'; -import type { DocsOptions, Options } from '@storybook/types'; +import type { DocsOptions, TagsOptions, Options } from '@storybook/types'; export type PreviewHtml = string | undefined; @@ -11,6 +11,7 @@ export async function transformIframeHtml(html: string, options: Options) { const bodyHtmlSnippet = await presets.apply('previewBody'); const logLevel = await presets.apply('logLevel', undefined); const docsOptions = await presets.apply('docs'); + const tagsOptions = await presets.apply('tags'); const coreOptions = await presets.apply('core'); const stories = normalizeStories(await options.presets.apply('stories', [], options), { @@ -42,6 +43,7 @@ export async function transformIframeHtml(html: string, options: Options) { .replace(`'[FEATURES HERE]'`, JSON.stringify(features || {})) .replace(`'[STORIES HERE]'`, JSON.stringify(stories || {})) .replace(`'[DOCS_OPTIONS HERE]'`, JSON.stringify(docsOptions || {})) + .replace(`'[TAGS_OPTIONS HERE]'`, JSON.stringify(tagsOptions || {})) .replace('', headHtmlSnippet || '') .replace('', bodyHtmlSnippet || ''); } diff --git a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts index c7e1b86aef10..ef510ef2378f 100644 --- a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts +++ b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts @@ -82,6 +82,7 @@ export default async ( nonNormalizedStories, modulesCount = 1000, build, + tagsOptions, ] = await Promise.all([ presets.apply('core'), presets.apply('frameworkOptions'), @@ -95,6 +96,7 @@ export default async ( presets.apply('stories', []), options.cache?.get('modulesCount').catch(() => {}), options.presets.apply('build'), + presets.apply('tags', {}), ]); const stories = normalizeStories(nonNormalizedStories, { @@ -184,6 +186,7 @@ export default async ( importPathMatcher: specifier.importPathMatcher.source, })), DOCS_OPTIONS: docsOptions, + TAGS_OPTIONS: tagsOptions, ...(build?.test?.disableBlocks ? { __STORYBOOK_BLOCKS_EMPTY_MODULE__: {} } : {}), }, headHtmlSnippet, diff --git a/code/e2e-tests/tags.spec.ts b/code/e2e-tests/tags.spec.ts new file mode 100644 index 000000000000..37fb76fb814c --- /dev/null +++ b/code/e2e-tests/tags.spec.ts @@ -0,0 +1,62 @@ +import { test, expect } from '@playwright/test'; +import { SbPage } from './util'; + +const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:8001'; + +test.describe('tags', () => { + test.beforeEach(async ({ page }) => { + await page.goto(storybookUrl); + await new SbPage(page).waitUntilLoaded(); + }); + + test('should correctly filter dev-only, docs-only, test-only stories', async ({ page }) => { + const sbPage = new SbPage(page); + + await sbPage.navigateToStory('lib/preview-api/tags', 'docs'); + + // Sidebar should include dev-only and exclude docs-only and test-only + const devOnlyEntry = await page.locator('#lib-preview-api-tags--dev-only').all(); + expect(devOnlyEntry.length).toBe(1); + + const docsOnlyEntry = await page.locator('#lib-preview-api-tags--docs-only').all(); + expect(docsOnlyEntry.length).toBe(0); + + const testOnlyEntry = await page.locator('#lib-preview-api-tags--test-only').all(); + expect(testOnlyEntry.length).toBe(0); + + // Autodocs should include docs-only and exclude dev-only and test-only + const root = sbPage.previewRoot(); + + const devOnlyAnchor = await root.locator('#anchor--lib-preview-api-tags--dev-only').all(); + expect(devOnlyAnchor.length).toBe(0); + + const docsOnlyAnchor = await root.locator('#anchor--lib-preview-api-tags--docs-only').all(); + expect(docsOnlyAnchor.length).toBe(1); + + const testOnlyAnchor = await root.locator('#anchor--lib-preview-api-tags--test-only').all(); + expect(testOnlyAnchor.length).toBe(0); + }); + + test('should correctly filter out test-only autodocs pages', async ({ page }) => { + const sbPage = new SbPage(page); + + await sbPage.selectToolbar('#lib-preview-api'); + + // Sidebar should exclude test-only stories and their docs + const componentEntry = await page.locator('#lib-preview-api-test-only-tag').all(); + expect(componentEntry.length).toBe(0); + + // Even though test-only autodocs not sidebar, it is still in the preview + // Even though the test-only story is filtered out of the stories, it is still the primary story (should it be?) + await sbPage.deepLinkToStory(storybookUrl, 'lib/preview-api/test-only-tag', 'docs'); + await sbPage.waitUntilLoaded(); + const docsButton = await sbPage.previewRoot().locator('button', { hasText: 'Button' }); + await expect(docsButton).toBeVisible(); + + // Even though test-only story not sidebar, it is still in the preview + await sbPage.deepLinkToStory(storybookUrl, 'lib/preview-api/test-only-tag', 'default'); + await sbPage.waitUntilLoaded(); + const storyButton = await sbPage.previewRoot().locator('button', { hasText: 'Button' }); + await expect(storyButton).toBeVisible(); + }); +}); diff --git a/code/e2e-tests/util.ts b/code/e2e-tests/util.ts index 38b0e78cb4c8..ce4cd6b09af0 100644 --- a/code/e2e-tests/util.ts +++ b/code/e2e-tests/util.ts @@ -33,6 +33,9 @@ export class SbPage { const storyLinkId = `${titleId}--${storyId}`; const viewMode = name === 'docs' ? 'docs' : 'story'; await this.page.goto(`${baseURL}/?path=/${viewMode}/${storyLinkId}`); + + await this.page.waitForURL((url) => url.search.includes(`path=/${viewMode}/${storyLinkId}`)); + await this.previewRoot(); } /** diff --git a/code/lib/core-server/package.json b/code/lib/core-server/package.json index 0522362a6a25..91fb46de1597 100644 --- a/code/lib/core-server/package.json +++ b/code/lib/core-server/package.json @@ -66,6 +66,7 @@ "@storybook/docs-mdx": "3.0.0", "@storybook/global": "^5.0.0", "@storybook/manager": "workspace:*", + "@storybook/manager-api": "workspace:*", "@storybook/node-logger": "workspace:*", "@storybook/preview-api": "workspace:*", "@storybook/telemetry": "workspace:*", @@ -115,6 +116,7 @@ "entries": [ "./src/index.ts", "./src/presets/common-preset.ts", + "./src/presets/common-manager.ts", "./src/presets/common-override-preset.ts" ], "platform": "node" diff --git a/code/lib/core-server/src/presets/common-manager.ts b/code/lib/core-server/src/presets/common-manager.ts new file mode 100644 index 000000000000..081722917468 --- /dev/null +++ b/code/lib/core-server/src/presets/common-manager.ts @@ -0,0 +1,21 @@ +import { addons } from '@storybook/manager-api'; +import { global } from '@storybook/global'; + +const STATIC_FILTER = 'static-filter'; + +addons.register(STATIC_FILTER, (api) => { + // FIXME: this ensures the filter is applied after the first render + // to avoid a strange race condition in Webkit only. + const excludeTags = Object.entries(global.TAGS_OPTIONS ?? {}).reduce((acc, entry) => { + const [tag, option] = entry; + if ((option as any).excludeFromSidebar) { + acc[tag] = true; + } + return acc; + }, {} as Record); + + api.experimental_setFilter(STATIC_FILTER, (item) => { + const tags = item.tags || []; + return tags.filter((tag) => excludeTags[tag]).length === 0; + }); +}); diff --git a/code/lib/core-server/src/presets/common-preset.ts b/code/lib/core-server/src/presets/common-preset.ts index 9a616e835056..15fb3bfceb26 100644 --- a/code/lib/core-server/src/presets/common-preset.ts +++ b/code/lib/core-server/src/presets/common-preset.ts @@ -359,3 +359,19 @@ export const resolvedReact = async (existing: any) => { return existing; } }; + +/** + * Set up `dev-only`, `docs-only`, `test-only` tags out of the box + */ +export const tags = async (existing: any) => { + return { + ...existing, + 'dev-only': { excludeFromDocsStories: true }, + 'docs-only': { excludeFromSidebar: true }, + 'test-only': { excludeFromSidebar: true, excludeFromDocsStories: true }, + }; +}; + +export const managerEntries = async (existing: any, options: Options) => { + return [require.resolve('./common-manager'), ...(existing || [])]; +}; diff --git a/code/lib/core-server/src/typings.d.ts b/code/lib/core-server/src/typings.d.ts index 597bae6cdc17..e6d9fc7e10a2 100644 --- a/code/lib/core-server/src/typings.d.ts +++ b/code/lib/core-server/src/typings.d.ts @@ -7,3 +7,4 @@ declare module '@discoveryjs/json-ext'; declare module 'watchpack'; declare var FEATURES: import('@storybook/types').StorybookConfigRaw['features']; +declare var TAGS_OPTIONS: import('@storybook/types').TagsOptions; diff --git a/code/lib/core-server/src/utils/StoryIndexGenerator.ts b/code/lib/core-server/src/utils/StoryIndexGenerator.ts index 78ff197d2cca..2da96396d6da 100644 --- a/code/lib/core-server/src/utils/StoryIndexGenerator.ts +++ b/code/lib/core-server/src/utils/StoryIndexGenerator.ts @@ -318,7 +318,7 @@ export class StoryIndexGenerator { const name = this.options.docs.defaultName ?? 'Docs'; const { metaId } = indexInputs[0]; const { title } = entries[0]; - const tags = indexInputs[0].tags || []; + const metaTags = indexInputs[0].metaTags || []; const id = toId(metaId ?? title, name); entries.unshift({ id, @@ -326,7 +326,7 @@ export class StoryIndexGenerator { name, importPath, type: 'docs', - tags: [...tags, 'docs', ...(!hasAutodocsTag && !isStoriesMdx ? [AUTODOCS_TAG] : [])], + tags: [...metaTags, 'docs', ...(!hasAutodocsTag && !isStoriesMdx ? [AUTODOCS_TAG] : [])], storiesImports: [], }); } @@ -432,6 +432,7 @@ export class StoryIndexGenerator { importPath, storiesImports: sortedDependencies.map((dep) => dep.entries[0].importPath), type: 'docs', + // FIXME: update this to use the index entry's metaTags once we update this to run on `IndexInputs` tags: [...(result.tags || []), csfEntry ? 'attached-mdx' : 'unattached-mdx', 'docs'], }; return docsEntry; diff --git a/code/lib/core-server/src/utils/__tests__/index-extraction.test.ts b/code/lib/core-server/src/utils/__tests__/index-extraction.test.ts index dacec6df8e2b..a1936c6719dd 100644 --- a/code/lib/core-server/src/utils/__tests__/index-extraction.test.ts +++ b/code/lib/core-server/src/utils/__tests__/index-extraction.test.ts @@ -405,7 +405,6 @@ describe('docs entries from story extraction', () => { "name": "docs", "storiesImports": [], "tags": [ - "story-tag-from-indexer", "docs", "autodocs", ], @@ -466,8 +465,6 @@ describe('docs entries from story extraction', () => { "name": "docs", "storiesImports": [], "tags": [ - "autodocs", - "story-tag-from-indexer", "docs", ], "title": "A", @@ -577,8 +574,6 @@ describe('docs entries from story extraction', () => { "name": "docs", "storiesImports": [], "tags": [ - "stories-mdx", - "story-tag-from-indexer", "docs", ], "title": "A", diff --git a/code/lib/csf-tools/src/CsfFile.test.ts b/code/lib/csf-tools/src/CsfFile.test.ts index c503cf795672..400e4306ccb6 100644 --- a/code/lib/csf-tools/src/CsfFile.test.ts +++ b/code/lib/csf-tools/src/CsfFile.test.ts @@ -1098,6 +1098,8 @@ describe('CsfFile', () => { - component-tag - story-tag - play-fn + metaTags: &ref_0 + - component-tag __id: component-id--a - type: story importPath: foo/bar.stories.js @@ -1109,6 +1111,7 @@ describe('CsfFile', () => { - component-tag - story-tag - play-fn + metaTags: *ref_0 __id: component-id--b `); }); @@ -1138,6 +1141,8 @@ describe('CsfFile', () => { metaId: component-id tags: - component-tag + metaTags: + - component-tag __id: custom-story-id `); }); @@ -1169,6 +1174,11 @@ describe('CsfFile', () => { - inherit-tag-dup - story-tag - story-tag-dup + metaTags: + - component-tag + - component-tag-dup + - component-tag-dup + - inherit-tag-dup __id: custom-foo-title--a `); }); diff --git a/code/lib/csf-tools/src/CsfFile.ts b/code/lib/csf-tools/src/CsfFile.ts index 05cd34cec320..2d623005cb69 100644 --- a/code/lib/csf-tools/src/CsfFile.ts +++ b/code/lib/csf-tools/src/CsfFile.ts @@ -577,6 +577,7 @@ export class CsfFile { title: this.meta?.title, metaId: this.meta?.id, tags, + metaTags: this.meta?.tags, __id: story.id, }; }); diff --git a/code/lib/manager-api/src/typings.d.ts b/code/lib/manager-api/src/typings.d.ts index d14b4196753e..63edef9413da 100644 --- a/code/lib/manager-api/src/typings.d.ts +++ b/code/lib/manager-api/src/typings.d.ts @@ -3,6 +3,7 @@ declare var __STORYBOOK_ADDONS_MANAGER: any; declare var CONFIG_TYPE: string; declare var FEATURES: import('@storybook/types').StorybookConfigRaw['features']; +declare var TAGS_OPTIONS: import('@storybook/types').StorybookConfigRaw['tags']; declare var REFS: any; declare var VERSIONCHECK: any; declare var LOGLEVEL: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent' | undefined; diff --git a/code/lib/preview-api/src/typings.d.ts b/code/lib/preview-api/src/typings.d.ts index d4009d04f481..b1ff997a733d 100644 --- a/code/lib/preview-api/src/typings.d.ts +++ b/code/lib/preview-api/src/typings.d.ts @@ -11,6 +11,7 @@ declare var FEATURES: import('@storybook/types').StorybookConfigRaw['features']; declare var STORIES: any; declare var DOCS_OPTIONS: any; +declare var TAGS_OPTIONS: import('@storybook/types').StorybookConfigRaw['tags']; // To enable user code to detect if it is running in Storybook declare var IS_STORYBOOK: boolean; diff --git a/code/lib/preview-api/template/stories/tags.stories.ts b/code/lib/preview-api/template/stories/tags.stories.ts index bae16f25e678..d896124dffa6 100644 --- a/code/lib/preview-api/template/stories/tags.stories.ts +++ b/code/lib/preview-api/template/stories/tags.stories.ts @@ -5,7 +5,7 @@ import { expect } from '@storybook/jest'; export default { component: globalThis.Components.Pre, - tags: ['component-one', 'component-two'], + tags: ['component-one', 'component-two', 'autodocs'], decorators: [ (storyFn: PartialStoryFn, context: StoryContext) => { return storyFn({ @@ -13,6 +13,7 @@ export default { }); }, ], + parameters: { chromatic: { disable: true } }, }; export const Inheritance = { @@ -23,4 +24,17 @@ export const Inheritance = { tags: ['story-one', 'story-two', 'story'], }); }, + parameters: { chromatic: { disable: false } }, +}; + +export const DocsOnly = { + tags: ['docs-only'], +}; + +export const TestOnly = { + tags: ['test-only'], +}; + +export const DevOnly = { + tags: ['dev-only'], }; diff --git a/code/lib/preview-api/template/stories/test-only-tag.stories.ts b/code/lib/preview-api/template/stories/test-only-tag.stories.ts new file mode 100644 index 000000000000..138f221d3ff7 --- /dev/null +++ b/code/lib/preview-api/template/stories/test-only-tag.stories.ts @@ -0,0 +1,11 @@ +import { global as globalThis } from '@storybook/global'; + +export default { + component: globalThis.Components.Button, + tags: ['autodocs', 'test-only'], + parameters: { chromatic: { disable: true } }, +}; + +export const Default = { + args: { label: 'Button' }, +}; diff --git a/code/lib/types/src/modules/core-common.ts b/code/lib/types/src/modules/core-common.ts index aecbd4941e7e..377081831972 100644 --- a/code/lib/types/src/modules/core-common.ts +++ b/code/lib/types/src/modules/core-common.ts @@ -326,6 +326,15 @@ export interface TestBuildConfig { test?: TestBuildFlags; } +type Tag = string; + +export interface TagOptions { + excludeFromSidebar: boolean; + excludeFromDocsStories: boolean; +} + +export type TagsOptions = Record>; + /** * The interface for Storybook configuration used internally in presets * The difference is that these values are the raw values, AKA, not wrapped with `PresetValue<>` @@ -404,6 +413,8 @@ export interface StorybookConfigRaw { previewMainTemplate?: string; managerHead?: string; + + tags?: TagsOptions; } /** @@ -518,6 +529,11 @@ export interface StorybookConfig { * which is the existing head content, and return a modified string. */ managerHead?: PresetValue; + + /** + * Configure non-standard tag behaviors + */ + tags?: PresetValue; } export type PresetValue = T | ((config: T, options: Options) => T | Promise); diff --git a/code/lib/types/src/modules/indexer.ts b/code/lib/types/src/modules/indexer.ts index 56d435cdb533..1064354eefe0 100644 --- a/code/lib/types/src/modules/indexer.ts +++ b/code/lib/types/src/modules/indexer.ts @@ -107,6 +107,8 @@ export type BaseIndexInput = { metaId?: MetaId; /** Tags for filtering entries in Storybook and its tools. */ tags?: Tag[]; + /** Tags from the meta for filtering entries in Storybook and its tools. */ + metaTags?: Tag[]; /** * The id of the entry, auto-generated from {@link title}/{@link metaId} and {@link exportName} if unspecified. * If specified, the story in the CSF file _must_ have a matching id set at `parameters.__id`, to be correctly matched. diff --git a/code/ui/blocks/src/blocks/Stories.tsx b/code/ui/blocks/src/blocks/Stories.tsx index de42c50d2e41..8681e4151c4d 100644 --- a/code/ui/blocks/src/blocks/Stories.tsx +++ b/code/ui/blocks/src/blocks/Stories.tsx @@ -27,9 +27,13 @@ const StyledHeading: typeof Heading = styled(Heading)(({ theme }) => ({ })); export const Stories: FC = ({ title = 'Stories', includePrimary = true }) => { - const { componentStories } = useContext(DocsContext); + const { componentStories, projectAnnotations, getStoryContext } = useContext(DocsContext); - let stories = componentStories().filter((story) => !story.parameters?.docs?.disable); + let stories = componentStories(); + const { stories: { filter } = { filter: undefined } } = projectAnnotations.parameters?.docs || {}; + if (filter) { + stories = stories.filter((story) => filter(story, getStoryContext(story))); + } if (!includePrimary) stories = stories.slice(1); diff --git a/code/ui/manager/src/runtime.ts b/code/ui/manager/src/runtime.ts index 0a1e153bffb4..2a4cf45dafd9 100644 --- a/code/ui/manager/src/runtime.ts +++ b/code/ui/manager/src/runtime.ts @@ -43,4 +43,9 @@ class ReactProvider extends Provider { const { document } = global; const rootEl = document.getElementById('root'); -renderStorybookUI(rootEl, new ReactProvider()); + +// We need to wait for the script tag containing the global objects +// to be run by Webkit before rendering the UI. This is fine in most browsers. +setTimeout(() => { + renderStorybookUI(rootEl, new ReactProvider()); +}, 0); diff --git a/code/yarn.lock b/code/yarn.lock index a52c475da3e7..2590e466330b 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -5553,6 +5553,7 @@ __metadata: "@storybook/docs-mdx": "npm:3.0.0" "@storybook/global": "npm:^5.0.0" "@storybook/manager": "workspace:*" + "@storybook/manager-api": "workspace:*" "@storybook/node-logger": "workspace:*" "@storybook/preview-api": "workspace:*" "@storybook/telemetry": "workspace:*"