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');
+});