From 310b66745e34378ab08a74fa698939488fcdff2e Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Tue, 1 Sep 2026 10:25:39 +1000 Subject: [PATCH 1/4] fix: storybook stories that render poorly --- .../s2/stories/ShadowDOM.stories.tsx | 84 ++++++++++++------- 1 file changed, 55 insertions(+), 29 deletions(-) diff --git a/packages/@react-spectrum/s2/stories/ShadowDOM.stories.tsx b/packages/@react-spectrum/s2/stories/ShadowDOM.stories.tsx index 6dc42b1269f..5d3208fe6db 100644 --- a/packages/@react-spectrum/s2/stories/ShadowDOM.stories.tsx +++ b/packages/@react-spectrum/s2/stories/ShadowDOM.stories.tsx @@ -122,7 +122,7 @@ import Server from '../spectrum-illustrations/linear/Server'; import StarFilled1 from '../spectrum-illustrations/linear/Star'; import {style} from '../style' with {type: 'macro'}; import {UNSAFE_PortalProvider} from 'react-aria'; -import {useEffect, useRef} from 'react'; +import {useEffect, useRef, useState} from 'react'; enableShadowDOM(); @@ -165,6 +165,54 @@ function unmountRootDeferred(root: ReturnType): void { }); } +type Scheme = 'light' | 'dark'; + +function getDocumentColorScheme(): Scheme { + return document.documentElement.getAttribute('data-color-scheme') === 'dark' ? 'dark' : 'light'; +} + +function useStorybookColorScheme(): Scheme { + const [colorScheme, setColorScheme] = useState(getDocumentColorScheme); + useEffect(() => { + const docEl = document.documentElement; + const update = () => setColorScheme(getDocumentColorScheme()); + update(); + const observer = new MutationObserver(update); + observer.observe(docEl, {attributes: true, attributeFilter: ['data-color-scheme']}); + return () => observer.disconnect(); + }, []); + return colorScheme; +} + +/** + * The app rendered into the nested shadow root. It tracks Storybook's color scheme itself and + * passes it to ``, so the scheme updates through React state within + * this root. `data-color-scheme` is also mirrored onto the shadow host(s) so page.css themes the + * background. + */ +function ShadowApp({ + hosts, + portalContainerRef +}: { + hosts: HTMLElement[]; + portalContainerRef: {current: HTMLDivElement | null}; +}) { + const colorScheme = useStorybookColorScheme(); + useEffect(() => { + for (const host of hosts) { + host.setAttribute('data-color-scheme', colorScheme); + } + }, [colorScheme, hosts]); + + return ( + + portalContainerRef.current}> + + + + ); +} + function ShadowDOMContained() { const hostRef = useRef(null); const portalContainerRef = useRef(null); @@ -178,12 +226,6 @@ function ShadowDOMContained() { const shadowRoot = host.attachShadow({mode: 'open'}); - // So S2 theme variables apply: :host in the copied CSS targets the shadow host. - const scheme = document.documentElement.getAttribute('data-color-scheme'); - if (scheme) { - host.setAttribute('data-color-scheme', scheme); - } - // Copy all styles from the document into the shadow root so S2 (and Storybook) styles apply. // Shadow DOM does not inherit styles; we must duplicate every stylesheet. shadowRoot.appendChild(createClonedDocumentStyleRoot()); @@ -199,13 +241,7 @@ function ShadowDOMContained() { const root = createRoot(appContainer); rootRef.current = root; - root.render( - - portalContainerRef.current}> - - - - ); + root.render(); return () => { rootRef.current = null; @@ -233,13 +269,6 @@ function ShadowDOMPortalToBody() { const shadowRoot = host.attachShadow({mode: 'open'}); const shadowPortal = portalHost.attachShadow({mode: 'open'}); - // So S2 theme variables apply: :host in the copied CSS targets the shadow host. - const scheme = document.documentElement.getAttribute('data-color-scheme'); - if (scheme) { - host.setAttribute('data-color-scheme', scheme); - portalHost.setAttribute('data-color-scheme', scheme); - } - // Each shadow root needs its own style clone — reusing one node only leaves styles in the last root. shadowRoot.appendChild(createClonedDocumentStyleRoot()); shadowPortal.appendChild(createClonedDocumentStyleRoot()); @@ -255,13 +284,8 @@ function ShadowDOMPortalToBody() { const root = createRoot(appContainer); rootRef.current = root; - root.render( - - portalContainerRef.current}> - - - - ); + // The portaled overlays inherit colorScheme through the Provider context, so both hosts get themed. + root.render(); return () => { rootRef.current = null; @@ -557,7 +581,9 @@ function AllComponents() { )}> - Name + + Name + Value From 3a23d8ddaa1105b6c1411ebf59b70dd363f7d5f3 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Tue, 1 Sep 2026 10:45:46 +1000 Subject: [PATCH 2/4] fix mobile size --- .../ai/stories/ResponseStatus.stories.tsx | 6 ++--- .../s2/stories/ShadowDOM.stories.tsx | 27 +++++-------------- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/packages/@react-spectrum/ai/stories/ResponseStatus.stories.tsx b/packages/@react-spectrum/ai/stories/ResponseStatus.stories.tsx index a5dac4e2e95..b4e21cbf1de 100644 --- a/packages/@react-spectrum/ai/stories/ResponseStatus.stories.tsx +++ b/packages/@react-spectrum/ai/stories/ResponseStatus.stories.tsx @@ -66,7 +66,7 @@ type Story = StoryObj; export const Example: Story = { render: args => ( -
+
{args.status === 'pending' @@ -86,7 +86,7 @@ export const Example: Story = { export const NoResponseContent: Story = { render: args => ( -
+
{args.status === 'pending' @@ -228,7 +228,7 @@ function WithExecutionTraceRender(args) { return (
`, so the scheme updates through React state within - * this root. `data-color-scheme` is also mirrored onto the shadow host(s) so page.css themes the - * background. + * passes it to ``, so the scheme updates through React state within this + * root. */ -function ShadowApp({ - hosts, - portalContainerRef -}: { - hosts: HTMLElement[]; - portalContainerRef: {current: HTMLDivElement | null}; -}) { +function ShadowApp({portalContainerRef}: {portalContainerRef: {current: HTMLDivElement | null}}) { const colorScheme = useStorybookColorScheme(); - useEffect(() => { - for (const host of hosts) { - host.setAttribute('data-color-scheme', colorScheme); - } - }, [colorScheme, hosts]); - return ( - + portalContainerRef.current}> @@ -241,7 +228,7 @@ function ShadowDOMContained() { const root = createRoot(appContainer); rootRef.current = root; - root.render(); + root.render(); return () => { rootRef.current = null; @@ -284,8 +271,8 @@ function ShadowDOMPortalToBody() { const root = createRoot(appContainer); rootRef.current = root; - // The portaled overlays inherit colorScheme through the Provider context, so both hosts get themed. - root.render(); + // The portaled overlays inherit colorScheme through the Provider context, so both shadows theme. + root.render(); return () => { rootRef.current = null; From fb483e1fdde8b5dba8d8de8bb5e44aacf6f8d838 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Tue, 1 Sep 2026 13:22:22 +1000 Subject: [PATCH 3/4] fix alerts so they fit in mobile screens --- packages/@react-spectrum/ai/stories/Alert.stories.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/@react-spectrum/ai/stories/Alert.stories.tsx b/packages/@react-spectrum/ai/stories/Alert.stories.tsx index 1ba553a12f4..e7cfc08ad03 100644 --- a/packages/@react-spectrum/ai/stories/Alert.stories.tsx +++ b/packages/@react-spectrum/ai/stories/Alert.stories.tsx @@ -42,7 +42,8 @@ export default meta; type Story = StoryObj; export const Example: Story = { - render: args => + // Storybook uses 16px padding left and right + render: args => }; const VARIANTS = ['informative', 'positive', 'notice', 'negative', 'neutral'] as const; @@ -52,7 +53,12 @@ export const AllVariants: Story = { render: args => (
{VARIANTS.map(variant => ( - + ))}
), From ad90b2c4ee8a8dc294f7cb23370631f518865966 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Tue, 1 Sep 2026 13:24:12 +1000 Subject: [PATCH 4/4] couple more fixes --- .../@react-spectrum/ai/stories/ResponseStatus.stories.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@react-spectrum/ai/stories/ResponseStatus.stories.tsx b/packages/@react-spectrum/ai/stories/ResponseStatus.stories.tsx index b4e21cbf1de..fa766be60ca 100644 --- a/packages/@react-spectrum/ai/stories/ResponseStatus.stories.tsx +++ b/packages/@react-spectrum/ai/stories/ResponseStatus.stories.tsx @@ -66,7 +66,7 @@ type Story = StoryObj; export const Example: Story = { render: args => ( -
+
{args.status === 'pending' @@ -86,7 +86,7 @@ export const Example: Story = { export const NoResponseContent: Story = { render: args => ( -
+
{args.status === 'pending' @@ -228,7 +228,7 @@ function WithExecutionTraceRender(args) { return (