import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import DialogsContext from './DialogsContext'; import type { DialogComponent, OpenDialog, OpenDialogOptions } from './useDialogs'; interface DialogStackEntry { key: string; open: boolean; promise: Promise; Component: DialogComponent; payload: P; onClose: (result: R) => Promise; resolve: (result: R) => void; } export interface DialogProviderProps { children?: React.ReactNode; unmountAfter?: number; } /** * Provider for Dialog stacks. The subtree of this component can use the `useDialogs` hook to * access the dialogs API. The dialogs are rendered in the order they are requested. */ export default function DialogsProvider(props: DialogProviderProps) { const { children, unmountAfter = 1000 } = props; const [stack, setStack] = React.useState[]>([]); const keyPrefix = React.useId(); const nextId = React.useRef(0); const dialogMetadata = React.useRef( new WeakMap, DialogStackEntry>(), ); const requestDialog = useEventCallback(function open( Component: DialogComponent, payload: P, options: OpenDialogOptions = {}, ) { const { onClose = async () => {} } = options; let resolve: ((result: R) => void) | undefined; const promise = new Promise((resolveImpl) => { resolve = resolveImpl; }); if (!resolve) { throw new Error('resolve not set.'); } const key = `${keyPrefix}-${nextId.current}`; nextId.current += 1; const newEntry: DialogStackEntry = { key, open: true, promise, Component, payload, onClose, resolve, }; // Store metadata for reliable access during close dialogMetadata.current.set(promise, newEntry); setStack((prevStack) => [...prevStack, newEntry]); return promise; }); const closeDialogUi = useEventCallback(function closeDialogUi( dialog: Promise, ) { setStack((prevStack) => prevStack.map((entry) => entry.promise === dialog ? { ...entry, open: false } : entry, ), ); setTimeout(() => { // wait for closing animation setStack((prevStack) => prevStack.filter((entry) => entry.promise !== dialog)); // WeakMap automatically cleans up when promise is garbage collected }, unmountAfter); }); const closeDialog = useEventCallback(async function closeDialog( dialog: Promise, result: R, ) { const entryToClose = dialogMetadata.current.get(dialog); if (!entryToClose) { throw new Error('Dialog not found.'); } try { await entryToClose.onClose(result); } finally { entryToClose.resolve(result); closeDialogUi(dialog); } return dialog; }); const contextValue = React.useMemo( () => ({ open: requestDialog, close: closeDialog }), [requestDialog, closeDialog], ); return ( {children} {stack.map(({ key, open, Component, payload, promise }) => ( { await closeDialog(promise, result); }} /> ))} ); }