Follow-up to #2009. This attempts to reset portalContainer when it is in a different (popout) window than the ThemeProvider itself.
This is how it should work:
When portalContainerprop is passed, use it as-is.
When inheriting theme, try to find the topmost portal container (recursively inheriting).
If the inherited portal container is in a different window from the ThemeProvider, create a DOM element. (This PR)
If none of the above apply, create a new DOM element instead of createPortaling into an existing element.
Store the portal container for use in descendants.
Implementation notes
After some iteration, I ultimately decided to add jotai, which is something we had already planned on adding ever since v3 was released. I created a thin wrapper over useAtom called useScopedAtom that falls back to reading the value from the nearest context if it hasn't been set in the current context. This unlocks the main inheritance functionality in this PR, while avoiding conflicts with global/application state. This wrapper is completely abstracted away in ScopeProvider, so the most interesting changes are in other files:
portalContainerAtom now lives in and is exported from Portal.tsx and does not need to be stored in ThemeContext. (This is a great example of how jotai+ScopeProvider allows colocating code where it makes the most sense)
PortalContainer is a helper component in ThemeProvider.tsx that handles everything, with the exception of portalContainerFromParent, which needs to be passed by ThemeProvider.
This exception is because useScopedAtom uses the parent scope in ThemeProvider but uses the current scope in PortalContainer (since it is a child of ThemeProvider's own ScopeProvider).
ownerDocumentAtom is a private atom only used in ThemeProvider.tsx. The read/write are separated using useScopedAtom/useScopedSetAtom in different components.
Testing
Added an e2e test, and moved some existing unit tests to e2e for more reliability.
Manually tested in vite-playground using this code:
App.tsx
```tsx
import { useState } from 'react';
import {
Button,
Table,
ThemeProvider,
tableFilters,
} from '@itwin/itwinui-react';
import type { Column } from '@itwin/itwinui-react/react-table';
import { createPortal } from 'react-dom';
export default function App() {
const [popoutDocumentBody, setPopoutDocumentBody] =
useState(null);
const showPopOutWindow = () => {
const childWindow = window.open(
'',
'_blank',
'width=1280,height=720,menubar=no,resizable=yes,scrollbars=no,status=no,location=no',
);
setTimeout(() => {
if (!childWindow) {
console.log('childWindow is null??');
return;
}
childWindow.document.title = 'Popout window!';
childWindow.document.write(htmlShell);
copyStyles(childWindow.document);
setPopoutDocumentBody(childWindow.document.body);
});
};
return (
Changes
Follow-up to #2009. This attempts to reset
portalContainer
when it is in a different (popout) window than theThemeProvider
itself.This is how it should work:
portalContainer
prop is passed, use it as-is.ThemeProvider
, create a DOM element. (This PR)createPortal
ing into an existing element.Implementation notes
After some iteration, I ultimately decided to add
jotai
, which is something we had already planned on adding ever since v3 was released. I created a thin wrapper overuseAtom
calleduseScopedAtom
that falls back to reading the value from the nearest context if it hasn't been set in the current context. This unlocks the main inheritance functionality in this PR, while avoiding conflicts with global/application state. This wrapper is completely abstracted away inScopeProvider
, so the most interesting changes are in other files:portalContainerAtom
now lives in and is exported fromPortal.tsx
and does not need to be stored inThemeContext
. (This is a great example of howjotai
+ScopeProvider
allows colocating code where it makes the most sense)PortalContainer
is a helper component inThemeProvider.tsx
that handles everything, with the exception ofportalContainerFromParent
, which needs to be passed byThemeProvider
.useScopedAtom
uses the parent scope inThemeProvider
but uses the current scope inPortalContainer
(since it is a child ofThemeProvider
's ownScopeProvider
).ownerDocumentAtom
is a private atom only used inThemeProvider.tsx
. The read/write are separated usinguseScopedAtom
/useScopedSetAtom
in different components.Testing
Added an e2e test, and moved some existing unit tests to e2e for more reliability.
Manually tested in vite-playground using this code:
App.tsx
```tsx import { useState } from 'react'; import { Button, Table, ThemeProvider, tableFilters, } from '@itwin/itwinui-react'; import type { Column } from '@itwin/itwinui-react/react-table'; import { createPortal } from 'react-dom'; export default function App() { const [popoutDocumentBody, setPopoutDocumentBody] = useStateThe most important difference between this and the playground for the previous PR (#2009) is this:
(The user doesn't need to specify
portalContainer
anymore because this PR makes it so the correct window is used automatically)Docs
Added changesets.