Skip to content

Commit

Permalink
add adoptedStyleSheets fallback for older safari (#1954)
Browse files Browse the repository at this point in the history
Slight refactor of ShadowRoot, with the main change being that when adoptedStyleSheets is not supported, an inline <style> tag will be used instead. Needed for iOS 15.
  • Loading branch information
mayank99 authored Mar 26, 2024
1 parent e3bdda1 commit 0a071e9
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 42 deletions.
5 changes: 5 additions & 0 deletions .changeset/nine-fishes-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@itwin/itwinui-react": patch
---

Fixed an issue in older Safari versions where visually-hidden styles inside `ProgressRadial` were not being applied.
126 changes: 84 additions & 42 deletions packages/itwinui-react/src/core/utils/components/ShadowRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,24 @@
*--------------------------------------------------------------------------------------------*/
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { useLatestRef, useLayoutEffect } from '../hooks/index.js';

const isBrowser = typeof document !== 'undefined';
const supportsDSD =
isBrowser && 'shadowRootMode' in HTMLTemplateElement.prototype;
const supportsAdoptedStylesheets =
isBrowser && 'adoptedStyleSheets' in Document.prototype;

type ShadowRootProps = { children: React.ReactNode; css?: string };

/**
* Wrapper around `<template>` element that attaches shadow root to its parent
* and moves its children into the shadow root.
*
* @private
*/
export const ShadowRoot = ({
children,
css,
}: {
children: React.ReactNode;
css?: string;
}) => {
const [shadowRoot, setShadowRoot] = React.useState<ShadowRoot>();
export const ShadowRoot = ({ children, css }: ShadowRootProps) => {
const isFirstRender = useIsFirstRender();
const styleSheet = React.useRef<CSSStyleSheet>();

const attachShadowRef = React.useCallback(
(template: HTMLTemplateElement | null) => {
const parent = template?.parentElement;
if (!template || !parent) {
return;
}
if (parent.shadowRoot) {
parent.shadowRoot.replaceChildren(); // Remove previous shadowroot content
}
queueMicrotask(() => {
const shadow =
parent.shadowRoot || parent.attachShadow({ mode: 'open' });

if (css && supportsAdoptedStylesheets) {
styleSheet.current ||= new CSSStyleSheet();
styleSheet.current.replaceSync(css);
shadow.adoptedStyleSheets = [styleSheet.current];
}

ReactDOM.flushSync(() => setShadowRoot(shadow));
});
},
[css],
);

if (!isBrowser) {
return (
Expand All @@ -68,19 +38,91 @@ export const ShadowRoot = ({
return null;
}

return (
<>
{shadowRoot ? (
ReactDOM.createPortal(children, shadowRoot)
) : (
<template ref={attachShadowRef} />
)}
</>
return <ClientShadowRoot css={css}>{children}</ClientShadowRoot>;
};

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

const ClientShadowRoot = ({ children, css }: ShadowRootProps) => {
const templateRef = React.useRef<HTMLTemplateElement>(null);
const shadowRoot = useShadowRoot(templateRef, { css });

// fallback to <style> tag if adoptedStyleSheets is not supported
const fallbackCss =
!supportsAdoptedStylesheets && css ? <style>{css}</style> : null;

return shadowRoot ? (
ReactDOM.createPortal(
<>
{fallbackCss}
{children}
</>,
shadowRoot,
)
) : (
<template ref={templateRef} />
);
};

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

/**
* Given a ref, this hook will return a shadowRoot attached to its parent element.
*
* The css will be added to the shadowRoot using `adoptedStyleSheets` (if supported).
*/
function useShadowRoot(
templateRef: React.RefObject<HTMLElement>,
{ css = '' },
) {
const [shadowRoot, setShadowRoot] = React.useState<ShadowRoot | null>(null);
const styleSheet = React.useRef<CSSStyleSheet>();
const latestCss = useLatestRef(css);

useLayoutEffect(() => {
const parent = templateRef.current?.parentElement;
if (!parent) {
return;
}

if (parent.shadowRoot) {
parent.shadowRoot.replaceChildren(); // Remove previous shadowroot content
}

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

queueMicrotask(() => {
// Flush the state immediately to ensure layout measurements in parent component are correct
ReactDOM.flushSync(() => setShadowRoot(shadow));
});

return () => void setShadowRoot(null);
}, [templateRef, latestCss]);

// Synchronize `css` with contents of the existing stylesheet
useLayoutEffect(() => {
if (css && supportsAdoptedStylesheets) {
styleSheet.current?.replaceSync(css);
}
}, [css]);

return shadowRoot;
}

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

function useIsFirstRender() {
const [isFirstRender, setIsFirstRender] = React.useState(true);
React.useEffect(() => setIsFirstRender(false), []);
Expand Down

0 comments on commit 0a071e9

Please sign in to comment.