Skip to content

Commit

Permalink
render a new Root as the portal container (#2173)
Browse files Browse the repository at this point in the history
  • Loading branch information
mayank99 authored Aug 1, 2024
1 parent 7d368ac commit 89243f5
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 21 deletions.
56 changes: 37 additions & 19 deletions packages/itwinui-react/src/core/ThemeProvider/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,6 @@ type RootProps = {
*/
applyBackground?: boolean;
};
/**
* This will be used to determine if background will be applied.
*/
shouldApplyBackground?: boolean;
};

type ThemeProviderOwnProps = Pick<RootProps, 'theme'> & {
Expand Down Expand Up @@ -186,7 +182,7 @@ export const ThemeProvider = React.forwardRef((props, forwardedRef) => {
<FallbackStyles root={rootElement} />
) : null}

<Root
<MainRoot
theme={theme}
themeOptions={themeOptions}
ref={useMergedRefs(forwardedRef, setRootElement, useIuiDebugRef)}
Expand All @@ -195,11 +191,13 @@ export const ThemeProvider = React.forwardRef((props, forwardedRef) => {
{children}

<PortalContainer
theme={theme}
themeOptions={themeOptions}
portalContainerProp={portalContainerProp}
portalContainerFromParent={portalContainerFromParent}
isInheritingTheme={themeProp === 'inherit'}
/>
</Root>
</MainRoot>
</ToastProvider>
</ThemeContext.Provider>
</HydrationProvider>
Expand All @@ -212,15 +210,7 @@ if (process.env.NODE_ENV === 'development') {

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

const Root = React.forwardRef((props, forwardedRef) => {
const { theme, children, themeOptions, className, ...rest } = props;

const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');
const prefersHighContrast = useMediaQuery('(prefers-contrast: more)');
const shouldApplyDark = theme === 'dark' || (theme === 'os' && prefersDark);
const shouldApplyHC = themeOptions?.highContrast ?? prefersHighContrast;
const shouldApplyBackground = themeOptions?.applyBackground;

const MainRoot = React.forwardRef((props, forwardedRef) => {
const [ownerDocument, setOwnerDocument] = useScopedAtom(ownerDocumentAtom);
const findOwnerDocumentFromRef = React.useCallback(
(el: HTMLElement | null): void => {
Expand All @@ -231,6 +221,25 @@ const Root = React.forwardRef((props, forwardedRef) => {
[ownerDocument, setOwnerDocument],
);

return (
<Root
{...props}
ref={useMergedRefs(findOwnerDocumentFromRef, forwardedRef)}
/>
);
}) as PolymorphicForwardRefComponent<'div', RootProps>;

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

const Root = React.forwardRef((props, forwardedRef) => {
const { theme, children, themeOptions, className, ...rest } = props;

const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');
const prefersHighContrast = useMediaQuery('(prefers-contrast: more)');
const shouldApplyDark = theme === 'dark' || (theme === 'os' && prefersDark);
const shouldApplyHC = themeOptions?.highContrast ?? prefersHighContrast;
const shouldApplyBackground = themeOptions?.applyBackground;

return (
<Box
className={cx(
Expand All @@ -240,7 +249,7 @@ const Root = React.forwardRef((props, forwardedRef) => {
)}
data-iui-theme={shouldApplyDark ? 'dark' : 'light'}
data-iui-contrast={shouldApplyHC ? 'high' : 'default'}
ref={useMergedRefs(forwardedRef, findOwnerDocumentFromRef)}
ref={forwardedRef}
{...rest}
>
{children}
Expand Down Expand Up @@ -323,11 +332,13 @@ const PortalContainer = React.memo(
portalContainerProp,
portalContainerFromParent,
isInheritingTheme,
theme,
themeOptions,
}: {
portalContainerProp: HTMLElement | undefined;
portalContainerFromParent: HTMLElement | undefined;
isInheritingTheme: boolean;
}) => {
} & RootProps) => {
const [ownerDocument] = useScopedAtom(ownerDocumentAtom);
const [portalContainer, setPortalContainer] =
useScopedAtom(portalContainerAtom);
Expand Down Expand Up @@ -367,9 +378,16 @@ const PortalContainer = React.memo(

if (shouldSetupPortalContainer) {
return (
<div style={{ display: 'contents' }} ref={setPortalContainer} id={id}>
<Root
theme={theme}
themeOptions={{ ...themeOptions, applyBackground: false }}
data-iui-portal
style={{ display: 'contents' }}
ref={setPortalContainer}
id={id}
>
<Toaster />
</div>
</Root>
);
} else if (portalContainerProp) {
return ReactDOM.createPortal(<Toaster />, portalContainerProp);
Expand Down
48 changes: 46 additions & 2 deletions testing/e2e/app/routes/ThemeProvider/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@ import * as ReactDOM from 'react-dom';
import { useSearchParams } from '@remix-run/react';
import { ThemeProvider, Tooltip } from '@itwin/itwinui-react';

export default function ThemeProviderExample() {
export default function Page() {
const [searchParams] = useSearchParams();
const themeSwitch = searchParams.get('themeSwitch') === 'true';

if (themeSwitch) {
return <ThemeSwitchTest />;
}

return <ThemeProviderTests />;
}

const ThemeProviderTests = () => {
const [searchParams] = useSearchParams();

const nested = searchParams.get('nested') === 'true';
Expand Down Expand Up @@ -79,7 +90,7 @@ export default function ThemeProviderExample() {
{portaled && <PortaledTest />}
</ThemeProvider>
);
}
};

/** https://github.com/iTwin/iTwinUI/issues/2082 */
const PortaledTest = () => {
Expand All @@ -101,3 +112,36 @@ const PortaledTest = () => {
</>
);
};

const ThemeSwitchTest = () => {
const [theme, setTheme] = React.useState<'dark' | 'light'>('dark');
const [highContrast, setHighContrast] = React.useState(false);

return (
<>
<button
onClick={() =>
setTheme((theme) => (theme === 'dark' ? 'light' : 'dark'))
}
>
Toggle theme
</button>

<button onClick={() => setHighContrast((hc) => !hc)}>
Toggle high contrast
</button>

<hr />

<ThemeProvider
className='MainRoot'
theme={theme}
themeOptions={{ highContrast }}
>
<Tooltip content='tooltip' visible placement='bottom'>
<button>hello</button>
</Tooltip>
</ThemeProvider>
</>
);
};
42 changes: 42 additions & 0 deletions testing/e2e/app/routes/ThemeProvider/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,45 @@ test('should not cause an infinite loop when portaled', async ({ page }) => {
'hello (portaled)',
);
});

test('should use the same theme for the main root and the portaled root', async ({
page,
}) => {
await page.goto('/ThemeProvider?themeSwitch=true');

const mainRoot = page.locator('.MainRoot');
const portaledRoot = page.locator('[data-iui-portal]', {
hasText: 'tooltip',
});
const themeSwitchButton = page.getByRole('button', { name: 'Toggle theme' });
const contrastSwitchButton = page.getByRole('button', {
name: 'Toggle high contrast',
});

// dark, default contrast
await expect(mainRoot).toHaveAttribute('data-iui-theme', 'dark');
await expect(mainRoot).toHaveAttribute('data-iui-contrast', 'default');
await expect(portaledRoot).toHaveAttribute('data-iui-theme', 'dark');
await expect(portaledRoot).toHaveAttribute('data-iui-contrast', 'default');

// light, default contrast
await themeSwitchButton.click();
await expect(mainRoot).toHaveAttribute('data-iui-theme', 'light');
await expect(mainRoot).toHaveAttribute('data-iui-contrast', 'default');
await expect(portaledRoot).toHaveAttribute('data-iui-theme', 'light');
await expect(portaledRoot).toHaveAttribute('data-iui-contrast', 'default');

// light, high contrast
await contrastSwitchButton.click();
await expect(mainRoot).toHaveAttribute('data-iui-theme', 'light');
await expect(mainRoot).toHaveAttribute('data-iui-contrast', 'high');
await expect(portaledRoot).toHaveAttribute('data-iui-theme', 'light');
await expect(portaledRoot).toHaveAttribute('data-iui-contrast', 'high');

// dark, high contrast
await themeSwitchButton.click();
await expect(mainRoot).toHaveAttribute('data-iui-theme', 'dark');
await expect(mainRoot).toHaveAttribute('data-iui-contrast', 'high');
await expect(portaledRoot).toHaveAttribute('data-iui-theme', 'dark');
await expect(portaledRoot).toHaveAttribute('data-iui-contrast', 'high');
});

0 comments on commit 89243f5

Please sign in to comment.