diff --git a/.changeset/shy-mangos-arrive.md b/.changeset/shy-mangos-arrive.md new file mode 100644 index 00000000000..545197158d6 --- /dev/null +++ b/.changeset/shy-mangos-arrive.md @@ -0,0 +1,5 @@ +--- +'@itwin/itwinui-react': patch +--- + +Fixed an issue where some components (e.g. `VisuallyHidden` inside `ProgressRadial`) were losing their styles when reparented into a different window. diff --git a/packages/itwinui-react/src/utils/components/ShadowRoot.tsx b/packages/itwinui-react/src/utils/components/ShadowRoot.tsx index 22de2adf9e8..7c5e4a74935 100644 --- a/packages/itwinui-react/src/utils/components/ShadowRoot.tsx +++ b/packages/itwinui-react/src/utils/components/ShadowRoot.tsx @@ -81,6 +81,29 @@ function useShadowRoot( const latestCss = useLatestRef(css); const latestShadowRoot = useLatestRef(shadowRoot); + const createStyleSheet = React.useCallback( + (shadow: ShadowRoot | null) => { + if (shadow && supportsAdoptedStylesheets) { + const currentWindow = shadow.ownerDocument.defaultView || globalThis; + + // bail if stylesheet already exists in the current window + if (styleSheet.current instanceof currentWindow.CSSStyleSheet) { + return; + } + + // create an empty stylesheet and add it to the shadowRoot + styleSheet.current = new currentWindow.CSSStyleSheet(); + shadow.adoptedStyleSheets.push(styleSheet.current); + + // add the CSS immediately to avoid FOUC (one-time) + if (latestCss.current) { + styleSheet.current.replaceSync(latestCss.current); + } + } + }, + [latestCss], + ); + useLayoutEffect(() => { const parent = templateRef.current?.parentElement; if (!parent) { @@ -93,18 +116,7 @@ function useShadowRoot( } const shadow = parent.shadowRoot || parent.attachShadow({ mode: 'open' }); - - if (supportsAdoptedStylesheets) { - // create an empty stylesheet and add it to the shadowRoot - const currentWindow = shadow.ownerDocument.defaultView || globalThis; - styleSheet.current = new currentWindow.CSSStyleSheet(); - shadow.adoptedStyleSheets = [styleSheet.current]; - - // add the CSS immediately to avoid FOUC (one-time) - if (latestCss.current) { - styleSheet.current.replaceSync(latestCss.current); - } - } + createStyleSheet(shadow); // Flush the state immediately after shadow-root is attached, to ensure that layout // measurements in parent component are correct. @@ -118,7 +130,7 @@ function useShadowRoot( }); return () => void setShadowRoot(null); - }, [templateRef, latestCss, latestShadowRoot]); + }, [templateRef, createStyleSheet, latestShadowRoot]); // Synchronize `css` with contents of the existing stylesheet useLayoutEffect(() => { @@ -127,5 +139,16 @@ function useShadowRoot( } }, [css]); + // Re-create stylesheet if the element is moved to a different window (by AppUI) + React.useEffect(() => { + const listener = () => createStyleSheet(latestShadowRoot.current); + + // See https://github.com/iTwin/appui/blob/0a4cc7d127b50146e003071320d06064a09a06ae/ui/appui-react/src/appui-react/layout/widget/ContentRenderer.tsx#L74-L80 + window.addEventListener('appui:reparent', listener); + return () => { + window.removeEventListener('appui:reparent', listener); + }; + }, [createStyleSheet, latestShadowRoot]); + return shadowRoot; } diff --git a/testing/e2e/app/routes/VisuallyHidden/route.tsx b/testing/e2e/app/routes/VisuallyHidden/route.tsx new file mode 100644 index 00000000000..78b748d0c30 --- /dev/null +++ b/testing/e2e/app/routes/VisuallyHidden/route.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { VisuallyHidden, ThemeProvider } from '@itwin/itwinui-react'; +import { useSearchParams } from '@remix-run/react'; + +export default function Page() { + const [searchParams] = useSearchParams(); + + if (searchParams.get('popout') === 'true') { + return ; + } + + return null; +} + +/** https://github.com/iTwin/iTwinUI/pull/2252#discussion_r1766676900 */ +function PopoutTest() { + const popout = usePopout(); + + return ( + <> + + {popout.popout && + ReactDOM.render( + + Hello + , + popout.popout.document.body, + )} + + ); +} + +// ---------------------------------------------------------------------------- + +function usePopout() { + const [popout, setPopout] = React.useState(null); + + const open = React.useCallback(() => { + const popout = window.open('', 'popout', 'width=400,height=400'); + setPopout(popout); + }, []); + + return React.useMemo(() => ({ open, popout }), [open, popout]); +} diff --git a/testing/e2e/app/routes/VisuallyHidden/spec.ts b/testing/e2e/app/routes/VisuallyHidden/spec.ts new file mode 100644 index 00000000000..1b075ae3b85 --- /dev/null +++ b/testing/e2e/app/routes/VisuallyHidden/spec.ts @@ -0,0 +1,11 @@ +import { test, expect } from '@playwright/test'; + +test('styles should exist in popout window', async ({ page }) => { + const popoutPromise = page.waitForEvent('popup'); + await page.goto('/VisuallyHidden?popout=true'); + await page.click('button'); + const popout = await popoutPromise; + + const visuallyHidden = popout.getByText('Hello'); + await expect(visuallyHidden).toHaveCSS('position', 'absolute'); +});