diff --git a/code/addons/docs/src/blocks/components/Preview.tsx b/code/addons/docs/src/blocks/components/Preview.tsx index b43dd30519a3..edaeb88a9876 100644 --- a/code/addons/docs/src/blocks/components/Preview.tsx +++ b/code/addons/docs/src/blocks/components/Preview.tsx @@ -260,7 +260,6 @@ export const Preview: FC = ({ zoom={(z: number) => setScale(scale * z)} resetZoom={() => setScale(1)} storyId={!isLoading && childProps ? getStoryId(childProps, context) : undefined} - baseUrl="./iframe.html" /> )} diff --git a/code/addons/docs/src/blocks/components/Story.tsx b/code/addons/docs/src/blocks/components/Story.tsx index 5af081fce655..144d14445fba 100644 --- a/code/addons/docs/src/blocks/components/Story.tsx +++ b/code/addons/docs/src/blocks/components/Story.tsx @@ -2,17 +2,15 @@ import type { FunctionComponent } from 'react'; import React, { useEffect, useRef, useState } from 'react'; -import { ErrorFormatter, Loader, getStoryHref } from 'storybook/internal/components'; +import { ErrorFormatter, Loader } from 'storybook/internal/components'; import type { DocsContextProps, PreparedStory } from 'storybook/internal/types'; import { styled } from 'storybook/theming'; +import { getStoryHref } from '../getStoryHref'; import { IFrame } from './IFrame'; import { ZoomContext } from './ZoomContext'; -const { PREVIEW_URL } = globalThis; -const BASE_URL = PREVIEW_URL || 'iframe.html'; - interface CommonProps { story: PreparedStory; inline: boolean; @@ -98,7 +96,7 @@ const IFrameStory: FunctionComponent = ({ story, height = '500 key="iframe" id={`iframe--${story.id}`} title={story.name} - src={getStoryHref(BASE_URL, story.id, { viewMode: 'story' })} + src={getStoryHref(story.id, { viewMode: 'story' })} allowFullScreen scale={scale} style={{ diff --git a/code/addons/docs/src/blocks/components/Toolbar.tsx b/code/addons/docs/src/blocks/components/Toolbar.tsx index 18713fc474fc..afebccb47b68 100644 --- a/code/addons/docs/src/blocks/components/Toolbar.tsx +++ b/code/addons/docs/src/blocks/components/Toolbar.tsx @@ -1,12 +1,14 @@ import type { FC, SyntheticEvent } from 'react'; import React from 'react'; -import { Button, Toolbar as SharedToolbar, getStoryHref } from 'storybook/internal/components'; +import { Button, Toolbar as SharedToolbar } from 'storybook/internal/components'; import { ShareAltIcon, ZoomIcon, ZoomOutIcon, ZoomResetIcon } from '@storybook/icons'; import { styled } from 'storybook/theming'; +import { getStoryHref } from '../getStoryHref'; + interface ZoomProps { zoom: (val: number) => void; resetZoom: () => void; @@ -14,7 +16,6 @@ interface ZoomProps { interface EjectProps { storyId?: string; - baseUrl?: string; } interface BarProps { @@ -52,14 +53,7 @@ const IconPlaceholder = styled.div(({ theme }) => ({ animation: `${theme.animation.glow} 1.5s ease-in-out infinite`, })); -export const Toolbar: FC = ({ - isLoading, - storyId, - baseUrl, - zoom, - resetZoom, - ...rest -}) => ( +export const Toolbar: FC = ({ isLoading, storyId, zoom, resetZoom, ...rest }) => ( {isLoading ? ( @@ -111,7 +105,6 @@ export const Toolbar: FC = ({ ) : ( - baseUrl && storyId && ( diff --git a/code/addons/docs/src/blocks/getStoryHref.ts b/code/addons/docs/src/blocks/getStoryHref.ts new file mode 100644 index 000000000000..524fbc43f613 --- /dev/null +++ b/code/addons/docs/src/blocks/getStoryHref.ts @@ -0,0 +1,17 @@ +/** + * Only for internal use in addon-docs code, because the parent util in `core` cannot be imported. + * Unlike the parent util, this one only returns the preview URL. + */ +export const getStoryHref = (storyId: string, additionalParams: Record = {}) => { + const baseUrl = globalThis.PREVIEW_URL || 'iframe.html'; + const [url, paramsStr] = baseUrl.split('?'); + const params = new URLSearchParams(paramsStr || ''); + + Object.entries(additionalParams).forEach(([key, value]) => { + params.set(key, value); + }); + + params.set('id', storyId); + + return `${url}?${params.toString()}`; +}; diff --git a/code/core/src/components/components/utils/getStoryHref.ts b/code/core/src/components/components/utils/getStoryHref.ts index 335bead96aa3..f2eabf080514 100644 --- a/code/core/src/components/components/utils/getStoryHref.ts +++ b/code/core/src/components/components/utils/getStoryHref.ts @@ -1,3 +1,5 @@ +import { deprecate } from 'storybook/internal/client-logger'; + function parseQuery(queryString: string) { const query: Record = {}; const pairs = queryString.split('&'); @@ -9,11 +11,15 @@ function parseQuery(queryString: string) { return query; } +/** @deprecated Use the api.getStoryHrefs method instead (from `storybook/manager-api`) */ export const getStoryHref = ( baseUrl: string, storyId: string, additionalParams: Record = {} ) => { + deprecate( + 'getStoryHref is deprecated and will be removed in Storybook 11, use the api.getStoryHrefs method instead' + ); const [url, paramsStr] = baseUrl.split('?'); const params = paramsStr ? { diff --git a/code/core/src/manager-api/modules/shortcuts.ts b/code/core/src/manager-api/modules/shortcuts.ts index 57233fca2eaa..0c16b169686d 100644 --- a/code/core/src/manager-api/modules/shortcuts.ts +++ b/code/core/src/manager-api/modules/shortcuts.ts @@ -113,6 +113,7 @@ export interface API_Shortcuts { expandAll: API_KeyCollection; remount: API_KeyCollection; openInEditor: API_KeyCollection; + openInIsolation: API_KeyCollection; copyStoryLink: API_KeyCollection; // TODO: bring this back once we want to add shortcuts for this // copyStoryName: API_KeyCollection; @@ -152,6 +153,7 @@ export const defaultShortcuts: API_Shortcuts = Object.freeze({ expandAll: [controlOrMetaKey(), 'shift', 'ArrowDown'], remount: ['alt', 'R'], openInEditor: ['alt', 'shift', 'E'], + openInIsolation: ['alt', 'shift', 'I'], copyStoryLink: ['alt', 'shift', 'L'], // TODO: bring this back once we want to add shortcuts for this // copyStoryName: ['alt', 'shift', 'C'], @@ -247,6 +249,8 @@ export const init: ModuleFn = ({ store, fullAPI, provider }) => { const { ui: { enableShortcuts }, storyId, + refId, + viewMode, } = store.getState(); if (!enableShortcuts) { return; @@ -397,6 +401,13 @@ export const init: ModuleFn = ({ store, fullAPI, provider }) => { } break; } + case 'openInIsolation': { + if (storyId && viewMode === 'story') { + const { previewHref } = fullAPI.getStoryHrefs(storyId, { refId }); + window.open(previewHref, '_blank', 'noopener,noreferrer'); + } + break; + } // TODO: bring this back once we want to add shortcuts for this // case 'copyStoryName': { // const storyData = fullAPI.getCurrentStoryData(); @@ -406,7 +417,10 @@ export const init: ModuleFn = ({ store, fullAPI, provider }) => { // break; // } case 'copyStoryLink': { - copy(window.location.href); + if (storyId) { + const { managerHref } = fullAPI.getStoryHrefs(storyId, { refId }); + copy(managerHref); + } break; } default: diff --git a/code/core/src/manager-api/modules/url.ts b/code/core/src/manager-api/modules/url.ts index 2eda0d3c4752..c37e95537bf3 100644 --- a/code/core/src/manager-api/modules/url.ts +++ b/code/core/src/manager-api/modules/url.ts @@ -7,17 +7,16 @@ import { } from 'storybook/internal/core-events'; import { buildArgsParam, queryFromLocation } from 'storybook/internal/router'; import type { NavigateOptions } from 'storybook/internal/router'; -import type { API_Layout, API_UI, Args } from 'storybook/internal/types'; +import type { API_Layout, API_UI, API_ViewMode, Args } from 'storybook/internal/types'; import { global } from '@storybook/global'; import { dequal as deepEqual } from 'dequal'; +import { stringify } from 'picoquery'; import type { ModuleArgs, ModuleFn } from '../lib/types'; import { defaultLayoutState } from './layout'; -const { window: globalWindow } = global; - export interface SubState { customQueryParams: QueryParams; } @@ -33,6 +32,24 @@ const parseBoolean = (value: string) => { return undefined; }; +const parseSerializedParam = (param: string) => + Object.fromEntries( + param + .split(';') + .map((pair) => pair.split(':')) + // Encoding values ensures we don't break already encoded args/globals but also don't encode our own special characters like ; and :. + .map(([key, value]) => [key, encodeURIComponent(value)]) + .filter(([key, value]) => key && value) + ); + +const mergeSerializedParams = (params: string, extraParams: string) => { + const pairs = parseSerializedParam(params); + const extra = parseSerializedParam(extraParams); + return Object.entries({ ...pairs, ...extra }) + .map(([key, value]) => `${key}:${value}`) + .join(';'); +}; + // Initialize the state based on the URL. // NOTE: // Although we don't change the URL when you change the state, we do support setting initial state @@ -121,6 +138,33 @@ export interface SubAPI { * @returns {void} */ navigateUrl: (url: string, options: NavigateOptions) => void; + /** + * Get the manager and preview hrefs for a story. + * + * @param {string} storyId - The ID of the story to get the URL for. + * @param {Object} options - Options for the URL. + * @param {string} [options.base] - Return an absolute href based on the current origin or network + * address. + * @param {boolean} [options.inheritArgs] - Inherit args from the current URL. If storyId matches + * current story, inheritArgs defaults to true. + * @param {boolean} [options.inheritGlobals] - Inherit globals from the current URL. Defaults to + * true. + * @param {QueryParams} [options.queryParams] - Query params to add to the URL. + * @param {string} [options.refId] - ID of the ref to get the URL for (for composed Storybooks) + * @param {string} [options.viewMode] - The view mode to use, defaults to 'story'. + * @returns {Object} Manager and preview hrefs for the story. + */ + getStoryHrefs( + storyId: string, + options?: { + base?: 'origin' | 'network'; + inheritArgs?: boolean; + inheritGlobals?: boolean; + queryParams?: QueryParams; + refId?: string; + viewMode?: API_ViewMode; + } + ): { managerHref: string; previewHref: string }; /** * Get the value of a query parameter from the current URL. * @@ -183,6 +227,54 @@ export const init: ModuleFn = (moduleArgs) => { }; const api: SubAPI = { + getStoryHrefs(storyId, options = {}) { + const { id: currentStoryId, refId: currentRefId } = fullAPI.getCurrentStoryData() ?? {}; + const isCurrentStory = storyId === currentStoryId && options.refId === currentRefId; + + const { customQueryParams, location, refs } = store.getState(); + const { + base, + inheritArgs = isCurrentStory, + inheritGlobals = true, + queryParams = {}, + refId, + viewMode = 'story', + } = options; + + if (refId && !refs[refId]) { + throw new Error(`Invalid refId: ${refId}`); + } + + const originAddress = global.window.location.origin + location.pathname; + const networkAddress = global.STORYBOOK_NETWORK_ADDRESS ?? originAddress; + const managerBase = + base === 'origin' ? originAddress : base === 'network' ? networkAddress : location.pathname; + const previewBase = refId + ? refs[refId].url + '/iframe.html' + : global.PREVIEW_URL || `${managerBase}iframe.html`; + + const refParam = refId ? `&refId=${encodeURIComponent(refId)}` : ''; + const { args = '', globals = '', ...otherParams } = queryParams; + let argsParam = inheritArgs + ? mergeSerializedParams(customQueryParams?.args ?? '', args) + : args; + let globalsParam = inheritGlobals + ? mergeSerializedParams(customQueryParams?.globals ?? '', globals) + : globals; + let customParams = stringify(otherParams, { + nesting: true, + nestingSyntax: 'js', + }); + + argsParam = argsParam && `&args=${argsParam}`; + globalsParam = globalsParam && `&globals=${globalsParam}`; + customParams = customParams && `&${customParams}`; + + return { + managerHref: `${managerBase}?path=/${viewMode}/${refId ? `${refId}_` : ''}${storyId}${argsParam}${globalsParam}${customParams}`, + previewHref: `${previewBase}?id=${storyId}&viewMode=${viewMode}${refParam}${argsParam}${refId ? '' : globalsParam}${customParams}`, + }; + }, getQueryParam(key) { const { customQueryParams } = store.getState(); return customQueryParams ? customQueryParams[key] : undefined; @@ -253,11 +345,11 @@ export const init: ModuleFn = (moduleArgs) => { let handleOrId: any; provider.channel?.on(STORY_ARGS_UPDATED, () => { - if ('requestIdleCallback' in globalWindow) { + if ('requestIdleCallback' in global.window) { if (handleOrId) { - globalWindow.cancelIdleCallback(handleOrId); + global.window.cancelIdleCallback(handleOrId); } - handleOrId = globalWindow.requestIdleCallback(updateArgsParam, { timeout: 1000 }); + handleOrId = global.window.requestIdleCallback(updateArgsParam, { timeout: 1000 }); } else { if (handleOrId) { clearTimeout(handleOrId); diff --git a/code/core/src/manager-api/root.tsx b/code/core/src/manager-api/root.tsx index 7388657698a9..f6526c27d1ae 100644 --- a/code/core/src/manager-api/root.tsx +++ b/code/core/src/manager-api/root.tsx @@ -105,6 +105,7 @@ export type API = addons.SubAPI & version.SubAPI & url.SubAPI & whatsnew.SubAPI & + openInEditor.SubAPI & Other; interface Other { diff --git a/code/core/src/manager-api/tests/url.test.js b/code/core/src/manager-api/tests/url.test.js index 349783223291..1b095ff0de11 100644 --- a/code/core/src/manager-api/tests/url.test.js +++ b/code/core/src/manager-api/tests/url.test.js @@ -6,11 +6,25 @@ import { UPDATE_QUERY_PARAMS, } from 'storybook/internal/core-events'; +import { global } from '@storybook/global'; + import EventEmitter from 'events'; import { init as initURL } from '../modules/url'; vi.mock('storybook/internal/client-logger'); +vi.mock('@storybook/global', () => ({ + global: { + window: { + location: { + hash: '', + href: 'http://localhost:6006', + origin: 'http://localhost:6006', + }, + }, + STORYBOOK_NETWORK_ADDRESS: 'http://192.168.1.1:6006/', + }, +})); const storyState = (storyId) => ({ path: `/story/${storyId}`, @@ -19,8 +33,6 @@ const storyState = (storyId) => ({ }); describe('initial state', () => { - const viewMode = 'story'; - describe('config query parameters', () => { it('handles full parameter', () => { const navigate = vi.fn(); @@ -236,3 +248,210 @@ describe('initModule', () => { ); }); }); + +describe('getStoryHrefs', () => { + let state = {}; + const store = { + setState: (change) => { + state = { ...state, ...change }; + }, + getState: () => state, + }; + + it('returns manager and preview URLs for a story', () => { + const { api, state } = initURL({ + store, + provider: { channel: new EventEmitter() }, + state: { location: { pathname: '/', search: '' } }, + navigate: vi.fn(), + fullAPI: { getCurrentStoryData: () => ({ id: 'test--story' }) }, + }); + store.setState(state); + + const { managerHref, previewHref } = api.getStoryHrefs('test--story'); + expect(managerHref).toEqual('/?path=/story/test--story'); + expect(previewHref).toEqual('/iframe.html?id=test--story&viewMode=story'); + }); + + it('retains args and globals from the URL', () => { + const { api, state } = initURL({ + store, + provider: { channel: new EventEmitter() }, + state: { location: { pathname: '/', search: '?args=a:1&globals=b:2' } }, + navigate: vi.fn(), + fullAPI: { getCurrentStoryData: () => ({ id: 'test--story' }) }, + }); + store.setState(state); + + const { managerHref, previewHref } = api.getStoryHrefs('test--story'); + expect(managerHref).toContain('&args=a:1&globals=b:2'); + expect(previewHref).toContain('&args=a:1&globals=b:2'); + }); + + it('retains args with special values', () => { + const { api, state } = initURL({ + store, + provider: { channel: new EventEmitter() }, + state: { location: { pathname: '/', search: '?args=a:!null;b:!hex(f00);c:!undefined' } }, + navigate: vi.fn(), + fullAPI: { getCurrentStoryData: () => ({ id: 'test--story' }) }, + }); + store.setState(state); + + const { managerHref, previewHref } = api.getStoryHrefs('test--story'); + expect(managerHref).toContain('&args=a:!null;b:!hex(f00);c:!undefined'); + expect(previewHref).toContain('&args=a:!null;b:!hex(f00);c:!undefined'); + }); + + it('drops args but retains globals when changing stories', () => { + const { api, state } = initURL({ + store, + provider: { channel: new EventEmitter() }, + state: { location: { pathname: '/', search: '?args=a:1&globals=b:2' } }, + navigate: vi.fn(), + fullAPI: { getCurrentStoryData: () => ({ id: 'test--story' }) }, + }); + store.setState(state); + + const { managerHref, previewHref } = api.getStoryHrefs('test--another-story'); + expect(managerHref).toEqual('/?path=/story/test--another-story&globals=b:2'); + expect(previewHref).toEqual('/iframe.html?id=test--another-story&viewMode=story&globals=b:2'); + }); + + it('supports disabling inheritance of args and globals', () => { + const { api, state } = initURL({ + store, + provider: { channel: new EventEmitter() }, + state: { location: { pathname: '/', search: '?args=a:1&globals=b:2' } }, + navigate: vi.fn(), + fullAPI: { getCurrentStoryData: () => ({ id: 'test--story' }) }, + }); + store.setState(state); + + const { managerHref, previewHref } = api.getStoryHrefs('test--story', { + inheritArgs: false, + inheritGlobals: false, + }); + expect(managerHref).toEqual('/?path=/story/test--story'); + expect(previewHref).toEqual('/iframe.html?id=test--story&viewMode=story'); + }); + + it('supports extra args and globals with merging', () => { + const { api, state } = initURL({ + store, + provider: { channel: new EventEmitter() }, + state: { location: { pathname: '/', search: '?args=a:1;b:2&globals=c:3;d:4' } }, + navigate: vi.fn(), + fullAPI: { getCurrentStoryData: () => ({ id: 'test--story' }) }, + }); + store.setState(state); + + const { managerHref, previewHref } = api.getStoryHrefs('test--story', { + queryParams: { args: 'a:2;c:3', globals: 'd:5' }, + }); + expect(managerHref).toContain('&args=a:2;b:2;c:3&globals=c:3;d:5'); + expect(previewHref).toContain('&args=a:2;b:2;c:3&globals=c:3;d:5'); + }); + + it('supports additional query params, including nested objects', () => { + const { api, state } = initURL({ + store, + provider: { channel: new EventEmitter() }, + state: { location: { pathname: '/', search: '?args=a:1&globals=b:2' } }, + navigate: vi.fn(), + fullAPI: { getCurrentStoryData: () => ({ id: 'test--story' }) }, + }); + store.setState(state); + + const { managerHref, previewHref } = api.getStoryHrefs('test--story', { + queryParams: { one: 1, foo: { bar: 'baz' } }, + }); + expect(managerHref).toContain('&args=a:1&globals=b:2&one=1&foo.bar=baz'); + expect(previewHref).toContain('&args=a:1&globals=b:2&one=1&foo.bar=baz'); + }); + + it('correctly preserves args and globals encoding', () => { + const { api, state } = initURL({ + store, + provider: { channel: new EventEmitter() }, + state: { location: { pathname: '/', search: '?args=equal:g%3Dh&globals=ampersand:c%26d' } }, + navigate: vi.fn(), + fullAPI: { getCurrentStoryData: () => ({ id: 'test--story' }) }, + }); + store.setState(state); + + const { managerHref, previewHref } = api.getStoryHrefs('test--story'); + expect(managerHref).toContain('&args=equal:g%3Dh&globals=ampersand:c%26d'); + expect(previewHref).toContain('&args=equal:g%3Dh&globals=ampersand:c%26d'); + }); + + it('correctly encodes query params', () => { + const { api, state } = initURL({ + store, + provider: { channel: new EventEmitter() }, + state: { location: { pathname: '/' } }, + navigate: vi.fn(), + fullAPI: { getCurrentStoryData: () => ({ id: 'test--story' }) }, + }); + store.setState(state); + + const { managerHref, previewHref } = api.getStoryHrefs('test--story', { + queryParams: { equal: 'a=b', ampersand: 'c&d' }, + }); + expect(managerHref).toContain('&equal=a%3Db&ersand=c%26d'); + expect(previewHref).toContain('&equal=a%3Db&ersand=c%26d'); + }); + + it('supports returning absolute URLs using the base option', () => { + const { api, state } = initURL({ + store, + provider: { channel: new EventEmitter() }, + state: { location: { pathname: '/', search: '' } }, + navigate: vi.fn(), + fullAPI: { getCurrentStoryData: () => ({ id: 'test--story' }) }, + }); + store.setState(state); + + const origin = api.getStoryHrefs('test--story', { base: 'origin' }); + expect(origin.managerHref).toContain('http://localhost:6006/?path='); + expect(origin.previewHref).toContain('http://localhost:6006/iframe.html'); + + const network = api.getStoryHrefs('test--story', { base: 'network' }); + expect(network.managerHref).toContain('http://192.168.1.1:6006/?path='); + expect(network.previewHref).toContain('http://192.168.1.1:6006/iframe.html'); + }); + + it('supports linking to a ref, dropping globals in preview', () => { + const { api, state } = initURL({ + store, + provider: { channel: new EventEmitter() }, + state: { location: { pathname: '/', search: '?args=a:1&globals=b:2' } }, + navigate: vi.fn(), + fullAPI: { getCurrentStoryData: () => ({ id: 'test--story' }) }, + }); + store.setState(state); + store.setState({ refs: { external: { url: 'https://sb.example.com' } } }); + + const { managerHref, previewHref } = api.getStoryHrefs('test--story', { refId: 'external' }); + expect(managerHref).toEqual('/?path=/story/external_test--story&globals=b:2'); + expect(previewHref).toEqual( + 'https://sb.example.com/iframe.html?id=test--story&viewMode=story&refId=external' + ); + }); + + it('supports PREVIEW_URL override', () => { + global.PREVIEW_URL = 'https://custom.preview.url/'; + const { api, state } = initURL({ + store, + provider: { channel: new EventEmitter() }, + state: { location: { pathname: '/', search: '' } }, + navigate: vi.fn(), + fullAPI: { getCurrentStoryData: () => ({ id: 'test--story' }) }, + }); + store.setState(state); + + const { managerHref, previewHref } = api.getStoryHrefs('test--story'); + expect(managerHref).toEqual('/?path=/story/test--story'); + expect(previewHref).toEqual('https://custom.preview.url/?id=test--story&viewMode=story'); + }); +}); diff --git a/code/core/src/manager/components/preview/FramesRenderer.tsx b/code/core/src/manager/components/preview/FramesRenderer.tsx index fd15d6e19a95..03a47e247676 100644 --- a/code/core/src/manager/components/preview/FramesRenderer.tsx +++ b/code/core/src/manager/components/preview/FramesRenderer.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react'; import React, { Fragment, useRef } from 'react'; -import { Button, getStoryHref } from 'storybook/internal/components'; +import { Button } from 'storybook/internal/components'; import type { Combo } from 'storybook/manager-api'; import { Consumer } from 'storybook/manager-api'; @@ -9,7 +9,6 @@ import { Global, styled } from 'storybook/theming'; import type { CSSObject } from 'storybook/theming'; import { IFrame } from './Iframe'; -import { stringifyQueryParams } from './utils/stringifyQueryParams'; import type { FramesRendererProps } from './utils/types'; const getActive = (refId: FramesRendererProps['refId'], refs: FramesRendererProps['refs']) => { @@ -53,19 +52,15 @@ const styles: CSSObject = { }; export const FramesRenderer: FC = ({ + api, refs, scale, viewMode = 'story', refId, queryParams = {}, - baseUrl, storyId = '*', }) => { const version = refs[refId]?.version; - const stringifiedQueryParams = stringifyQueryParams({ - ...queryParams, - ...(version && { version }), - }); const active = getActive(refId, refs); const { current: frames } = useRef>({}); @@ -74,19 +69,21 @@ export const FramesRenderer: FC = ({ }, {}); if (!frames['storybook-preview-iframe']) { - frames['storybook-preview-iframe'] = getStoryHref(baseUrl, storyId, { - ...queryParams, - ...(version && { version }), + frames['storybook-preview-iframe'] = api.getStoryHrefs(storyId, { + queryParams: { ...queryParams, ...(version && { version }) }, + refId, viewMode, - }); + }).previewHref; } refsToLoad.forEach((ref) => { const id = `storybook-ref-${ref.id}`; - const existingUrl = frames[id]?.split('/iframe.html')[0]; - if (!existingUrl || ref.url !== existingUrl) { - const newUrl = `${ref.url}/iframe.html?id=${storyId}&viewMode=${viewMode}&refId=${ref.id}${stringifiedQueryParams}`; - frames[id] = newUrl; + if (!frames[id]?.startsWith(ref.url)) { + frames[id] = api.getStoryHrefs(storyId, { + queryParams: { ...queryParams, ...(version && { version }) }, + refId: ref.id, + viewMode, + }).previewHref; } }); diff --git a/code/core/src/manager/components/preview/Iframe.stories.tsx b/code/core/src/manager/components/preview/Iframe.stories.tsx index 2390ca501774..319fde0986bb 100644 --- a/code/core/src/manager/components/preview/Iframe.stories.tsx +++ b/code/core/src/manager/components/preview/Iframe.stories.tsx @@ -30,34 +30,38 @@ const style: CSSProperties = { height: '700px', }; -export const WorkingStory = () => ( -