Skip to content

Commit

Permalink
remove nested toasters (#2153)
Browse files Browse the repository at this point in the history
  • Loading branch information
mayank99 authored Jul 30, 2024
1 parent 4d57f9f commit 7d368ac
Show file tree
Hide file tree
Showing 14 changed files with 120 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/flat-apes-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@itwin/itwinui-react': minor
---

Nested `ThemeProvider`s will now reuse the same toaster instead of creating new ones.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
68 changes: 34 additions & 34 deletions packages/itwinui-react/src/core/ThemeProvider/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
ScopeProvider,
portalContainerAtom,
useScopedAtom,
useScopedSetAtom,
useId,
} from '../../utils/index.js';
import type { PolymorphicForwardRefComponent } from '../../utils/index.js';
Expand Down Expand Up @@ -180,7 +179,9 @@ export const ThemeProvider = React.forwardRef((props, forwardedRef) => {
<ScopeProvider>
<HydrationProvider>
<ThemeContext.Provider value={contextValue}>
<ToastProvider>
<ToastProvider
inherit={themeProp === 'inherit' && !portalContainerProp}
>
{includeCss && rootElement ? (
<FallbackStyles root={rootElement} />
) : null}
Expand Down Expand Up @@ -328,59 +329,58 @@ const PortalContainer = React.memo(
isInheritingTheme: boolean;
}) => {
const [ownerDocument] = useScopedAtom(ownerDocumentAtom);
const setPortalContainer = useScopedSetAtom(portalContainerAtom);
const [portalContainer, setPortalContainer] =
useScopedAtom(portalContainerAtom);

// Create a new portal container only if necessary:
// - no explicit portalContainer prop
// - not inheriting theme
// - no parent portal container to portal into
// - parent portal container is in a different window (#2006)
const shouldSetupPortalContainer =
!portalContainerProp &&
(!isInheritingTheme ||
!portalContainerFromParent ||
(!!ownerDocument &&
portalContainerFromParent.ownerDocument !== ownerDocument));

const id = useId();

// Synchronize atom with the correct portal target if necessary.
React.useEffect(() => {
if (shouldSetupPortalContainer) {
return;
}

const portalTarget = portalContainerProp || portalContainerFromParent;

if (portalTarget && portalTarget !== portalContainer) {
setPortalContainer(portalTarget);
}
});

// bail if not hydrated, because portals don't work on server
const isHydrated = useHydration() === 'hydrated';
if (!isHydrated) {
return null;
}

if (portalContainerProp) {
return <PortaledToaster target={portalContainerProp} />;
}

// Create a new portal container only if necessary:
// - not inheriting theme
// - no parent portal container to portal into
// - parent portal container is in a different window (#2006)
if (
!isInheritingTheme ||
!portalContainerFromParent ||
(!!ownerDocument &&
portalContainerFromParent.ownerDocument !== ownerDocument)
) {
if (shouldSetupPortalContainer) {
return (
<div style={{ display: 'contents' }} ref={setPortalContainer} id={id}>
<Toaster />
</div>
);
} else if (portalContainerProp) {
return ReactDOM.createPortal(<Toaster />, portalContainerProp);
}

return <PortaledToaster target={portalContainerFromParent} />;
return null;
},
);

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

const PortaledToaster = ({ target }: { target?: HTMLElement }) => {
const [portalContainer, setPortalContainer] =
useScopedAtom(portalContainerAtom);

// Synchronize atom with the correct portal target if necessary.
React.useEffect(() => {
if (target && target !== portalContainer) {
setPortalContainer(target);
}
});

return target ? ReactDOM.createPortal(<Toaster />, target) : null;
};

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

/**
* When `@itwin/itwinui-react/styles.css` is not imported, we will attempt to
* dynamically import it (if possible) and fallback to loading it from a CDN.
Expand Down
13 changes: 12 additions & 1 deletion packages/itwinui-react/src/core/Toast/Toaster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,13 @@ export const Toaster = () => {

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

export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
export const ToastProvider = ({
children,
inherit = false,
}: {
children: React.ReactNode;
inherit?: boolean;
}) => {
const [toasterState, dispatch] = React.useReducer(toastReducer, {
toasts: [],
settings: {
Expand All @@ -98,6 +104,11 @@ export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
},
});

// Re-use existing ToastProvider if found
if (React.useContext(ToasterStateContext) && inherit) {
return children;
}

return (
<ToasterDispatchContext.Provider value={dispatch}>
<ToasterStateContext.Provider value={toasterState}>
Expand Down
45 changes: 45 additions & 0 deletions testing/e2e/app/routes/Toaster/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as React from 'react';
import { useSearchParams } from '@remix-run/react';
import { ThemeProvider, useToaster } from '@itwin/itwinui-react';

export default function Page() {
const [searchParams] = useSearchParams();
const shouldPortal = searchParams.get('portal') === 'true';
const isClient = useIsClient();

return (
<>
<Toast text='Toast (root)' />

<ThemeProvider data-container='nested'>
<Toast text='Toast (nested)' />
</ThemeProvider>

{isClient && shouldPortal && (
<ThemeProvider portalContainer={document.body}>
<Toast text='Toast (portal)' />
</ThemeProvider>
)}
</>
);
}

function Toast({ text = 'Toast' }) {
const toaster = useToaster();

React.useEffect(() => {
toaster.informational(text, { type: 'persisting' });
}, [toaster]);

return null;
}

function useIsClient() {
const [isClient, setIsClient] = React.useState(false);

React.useEffect(() => {
setIsClient(true);
}, []);

return isClient;
}
24 changes: 24 additions & 0 deletions testing/e2e/app/routes/Toaster/spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { test, expect } from '@playwright/test';

test.describe('Toaster', () => {
test('should not create nested toast containers', async ({ page }) => {
await page.goto('/Toaster');

const rootToast = page.getByText('Toast (root)');
await expect(rootToast).toBeVisible();

const nestedToast = page.getByText('Toast (nested)');
await expect(nestedToast).toBeVisible();

await expect(page.locator('[data-container="nested"]')).not.toContainText(
'Toast',
);
});

test('should work when using portalContainer prop', async ({ page }) => {
await page.goto('/Toaster?portal=true');

const portalToast = page.getByText('Toast (portal)');
await expect(portalToast).toBeVisible();
});
});

0 comments on commit 7d368ac

Please sign in to comment.