From aa052cde3fd5b3f6115c0d82c93708be59936b26 Mon Sep 17 00:00:00 2001 From: Arpit Jain Date: Wed, 24 Jun 2026 11:22:53 +0900 Subject: [PATCH] Prevent Storybook links from navigating away Signed-off-by: Arpit Jain --- .storybook/preview.js | 64 ++++++++++++++++++++++++++++ src/components/Link/link.stories.tsx | 6 ++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/.storybook/preview.js b/.storybook/preview.js index a709f1a0da..c2c9d0fc7a 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -315,6 +315,69 @@ const withExplicitFullscreenStoryCanvas = (Story, context) => { return React.createElement(Story); }; +const shouldBlockStorybookLinkNavigation = (anchor) => { + const href = anchor.getAttribute('href'); + + if (!href) { + return false; + } + + if (href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) { + return false; + } + + return true; +}; + +/** + * Prevent links rendered inside stories from navigating away from Storybook. + * + * @type {(Story: any, context: any) => import('react').ReactElement} + */ +const withStorybookLinkNavigationGuard = (Story, context) => { + React.useEffect(() => { + if (context.viewMode !== 'story' && context.viewMode !== 'docs') { + return undefined; + } + + const handleAnchorClick = (event) => { + if ( + event.defaultPrevented || + event.button !== 0 || + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey + ) { + return; + } + + const target = event.target; + if (!(target instanceof Element)) { + return; + } + + const anchor = target.closest('a[href]'); + if (!(anchor instanceof HTMLAnchorElement)) { + return; + } + + if (!shouldBlockStorybookLinkNavigation(anchor)) { + return; + } + + event.preventDefault(); + }; + + document.addEventListener('click', handleAnchorClick, true); + return () => { + document.removeEventListener('click', handleAnchorClick, true); + }; + }, [context.id, context.viewMode]); + + return React.createElement(Story); +}; + export const globalTypes = { responsivePreview: { name: 'Responsive preview', @@ -338,6 +401,7 @@ export const initialGlobals = { export const decorators = [ renderResponsivePreviews, withExplicitFullscreenStoryCanvas, + withStorybookLinkNavigationGuard, ]; export const preview = { diff --git a/src/components/Link/link.stories.tsx b/src/components/Link/link.stories.tsx index f819e26ac6..92bd87018d 100644 --- a/src/components/Link/link.stories.tsx +++ b/src/components/Link/link.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { BrowserRouter } from 'react-router'; -import { expect, within } from 'storybook/test'; +import { expect, userEvent, within } from 'storybook/test'; import { Heading, Link, List, ListLink } from '~/src/index'; const meta: Meta = { @@ -36,6 +36,10 @@ export const Standalone: Story = { const canvas = within(canvasElement); const link = canvas.getByRole('link', { name: /standalone link/i }); await expect(link).toHaveAttribute('href', '/#'); + + const initialHref = globalThis.location.href; + await userEvent.click(link); + await expect(globalThis.location.href).toBe(initialHref); }, };