diff --git a/code/core/src/manager-api/modules/shortcuts.ts b/code/core/src/manager-api/modules/shortcuts.ts index 57233fca2eaa..2cbcd4ce9a0a 100644 --- a/code/core/src/manager-api/modules/shortcuts.ts +++ b/code/core/src/manager-api/modules/shortcuts.ts @@ -114,6 +114,7 @@ export interface API_Shortcuts { remount: API_KeyCollection; openInEditor: API_KeyCollection; copyStoryLink: API_KeyCollection; + openInIsolation: API_KeyCollection; // TODO: bring this back once we want to add shortcuts for this // copyStoryName: API_KeyCollection; } @@ -153,6 +154,7 @@ export const defaultShortcuts: API_Shortcuts = Object.freeze({ remount: ['alt', 'R'], openInEditor: ['alt', 'shift', 'E'], copyStoryLink: ['alt', 'shift', 'L'], + openInIsolation: ['alt', 'shift', 'I'], // TODO: bring this back once we want to add shortcuts for this // copyStoryName: ['alt', 'shift', 'C'], }); @@ -397,6 +399,11 @@ export const init: ModuleFn = ({ store, fullAPI, provider }) => { } break; } + case 'openInIsolation': { + const { refId, storyId } = store.getState(); + fullAPI.openInIsolation(storyId, refId); + break; + } // TODO: bring this back once we want to add shortcuts for this // case 'copyStoryName': { // const storyData = fullAPI.getCurrentStoryData(); diff --git a/code/core/src/manager-api/modules/stories.ts b/code/core/src/manager-api/modules/stories.ts index 21574e76ffd8..483675477d5a 100644 --- a/code/core/src/manager-api/modules/stories.ts +++ b/code/core/src/manager-api/modules/stories.ts @@ -1,4 +1,5 @@ import { logger } from 'storybook/internal/client-logger'; +import { getStoryHref } from 'storybook/internal/components'; import { CONFIG_ERROR, CURRENT_STORY_WAS_SET, @@ -291,6 +292,17 @@ export interface SubAPI { * @returns {Promise} A promise that resolves when the state has been updated. */ experimental_setFilter: (addonId: string, filterFunction: API_FilterFunction) => Promise; + /** + * Opens a story in isolation mode in a new tab/window. + * + * @param {string} storyId - The ID of the story to open. + * @param {string | null | undefined} refId - The ID of the ref for the story. Pass null/undefined + * for local stories. + * @param {'story' | 'docs'} viewMode - The view mode to open the story in. Defaults to current + * view mode. + * @returns {void} + */ + openInIsolation: (storyId: string, refId?: string | null, viewMode?: 'story' | 'docs') => void; } const removedOptions = ['enableShortcuts', 'theme', 'showRoots']; @@ -707,6 +719,32 @@ export const init: ModuleFn = ({ provider.channel?.emit(SET_FILTER, { id }); }, + + openInIsolation: (storyId, refId, viewMode) => { + const { location } = global.document; + const { refs, customQueryParams, viewMode: currentViewMode } = store.getState(); + + // Get the ref object from refs map using refId + const ref = refId ? refs[refId] : null; + + let baseUrl = `${location.origin}${location.pathname}`; + + if (!baseUrl.endsWith('/')) { + baseUrl += '/'; + } + + const iframeUrl = ref + ? `${ref.url}/iframe.html` + : (global.PREVIEW_URL as string) || `${baseUrl}iframe.html`; + + const storyViewMode = viewMode ?? currentViewMode; + + const href = getStoryHref(iframeUrl, storyId, { + ...customQueryParams, + ...(storyViewMode && { viewMode: storyViewMode }), + }); + global.window.open(href, '_blank', 'noopener,noreferrer'); + }, }; // On initial load, the local iframe will select the first story (or other "selection specifier") 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/stories.test.ts b/code/core/src/manager-api/tests/stories.test.ts index ea2e43e79b9f..9e55a780f101 100644 --- a/code/core/src/manager-api/tests/stories.test.ts +++ b/code/core/src/manager-api/tests/stories.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment happy-dom import type { Mocked } from 'vitest'; import { describe, expect, it, vi } from 'vitest'; diff --git a/code/core/src/manager/components/preview/tools/share.tsx b/code/core/src/manager/components/preview/tools/share.tsx index 5517dec0b54c..581c97f77e88 100644 --- a/code/core/src/manager/components/preview/tools/share.tsx +++ b/code/core/src/manager/components/preview/tools/share.tsx @@ -1,11 +1,6 @@ import React, { useMemo, useState } from 'react'; -import { - Button, - PopoverProvider, - TooltipLinkList, - getStoryHref, -} from 'storybook/internal/components'; +import { Button, PopoverProvider, TooltipLinkList } from 'storybook/internal/components'; import type { Addon_BaseType } from 'storybook/internal/types'; import { global } from '@storybook/global'; @@ -78,23 +73,22 @@ const QRDescription = styled.div(({ theme }) => ({ })); function ShareMenu({ - baseUrl, - storyId, - queryParams, qrUrl, isDevelopment, + refId, + storyId, }: { - baseUrl: string; - storyId: string; - queryParams: Record; qrUrl: string; isDevelopment: boolean; + refId: string | null | undefined; + storyId: string; }) { const api = useStorybookApi(); const shortcutKeys = api.getShortcutKeys(); const enableShortcuts = !!shortcutKeys; const [copied, setCopied] = useState(false); const copyStoryLink = shortcutKeys?.copyStoryLink; + const openInIsolation = shortcutKeys?.openInIsolation; const links = useMemo(() => { const copyTitle = copied ? 'Copied!' : 'Copy story link'; @@ -113,11 +107,11 @@ function ShareMenu({ }, { id: 'open-new-tab', - title: 'Open in isolation mode', + title: 'Open in isolation', icon: , + right: enableShortcuts ? : null, onClick: () => { - const href = getStoryHref(baseUrl, storyId, queryParams); - window.open(href, '_blank', 'noopener,noreferrer'); + api.openInIsolation(storyId, refId); }, }, ], @@ -144,7 +138,17 @@ function ShareMenu({ ]); return baseLinks; - }, [baseUrl, storyId, queryParams, copied, qrUrl, enableShortcuts, copyStoryLink, isDevelopment]); + }, [ + copied, + qrUrl, + enableShortcuts, + copyStoryLink, + isDevelopment, + api, + openInIsolation, + refId, + storyId, + ]); return ; } @@ -157,7 +161,7 @@ export const shareTool: Addon_BaseType = { render: () => { return ( - {({ baseUrl, storyId, queryParams }) => { + {({ storyId, refId }) => { const isDevelopment = global.CONFIG_TYPE === 'DEVELOPMENT'; const storyUrl = global.STORYBOOK_NETWORK_ADDRESS ? new URL(window.location.search, global.STORYBOOK_NETWORK_ADDRESS).href @@ -169,7 +173,12 @@ export const shareTool: Addon_BaseType = { placement="bottom" padding={0} popover={ - + } >