Skip to content

Commit

Permalink
re-add shadow DOM styles when reparented (#2252)
Browse files Browse the repository at this point in the history
  • Loading branch information
mayank99 authored Sep 20, 2024
1 parent 42a106c commit 3df74c0
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 13 deletions.
5 changes: 5 additions & 0 deletions .changeset/shy-mangos-arrive.md
Original file line number Diff line number Diff line change
@@ -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.
49 changes: 36 additions & 13 deletions packages/itwinui-react/src/utils/components/ShadowRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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.
Expand All @@ -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(() => {
Expand All @@ -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;
}
45 changes: 45 additions & 0 deletions testing/e2e/app/routes/VisuallyHidden/route.tsx
Original file line number Diff line number Diff line change
@@ -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 <PopoutTest />;
}

return null;
}

/** https://github.com/iTwin/iTwinUI/pull/2252#discussion_r1766676900 */
function PopoutTest() {
const popout = usePopout();

return (
<>
<button onClick={popout.open}>Open popout</button>
{popout.popout &&
ReactDOM.render(
<ThemeProvider>
<VisuallyHidden>Hello</VisuallyHidden>
</ThemeProvider>,
popout.popout.document.body,
)}
</>
);
}

// ----------------------------------------------------------------------------

function usePopout() {
const [popout, setPopout] = React.useState<Window | null>(null);

const open = React.useCallback(() => {
const popout = window.open('', 'popout', 'width=400,height=400');
setPopout(popout);
}, []);

return React.useMemo(() => ({ open, popout }), [open, popout]);
}
11 changes: 11 additions & 0 deletions testing/e2e/app/routes/VisuallyHidden/spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});

0 comments on commit 3df74c0

Please sign in to comment.