import invariant from 'invariant';
import * as React from 'react';
import { ModalsContext } from './context';
import type { ModalComponent, OpenModal, OpenModalOptions } from './use-modal';
type ModalStackEntry
= {
key: string;
open: boolean;
promise: Promise;
Component: ModalComponent;
payload: P;
onClose: (result: R) => Promise;
resolve: (result: R) => void;
};
export type ModalProviderProps = {
children?: React.ReactNode;
unmountAfter?: number;
};
/**
* Provider for Modals stacks. The subtree of this component can use the `useDialogs` hook to
* access the modals API. The modals are rendered in the order they are requested.
*
*/
const ModalsProvider = (props: ModalProviderProps) => {
const { children, unmountAfter = 1000 } = props;
const [stack, setStack] = React.useState[]>([]);
const keyPrefix = React.useId();
const nextId = React.useRef(0);
const requestModal = React.useCallback(
function open(
Component: ModalComponent
,
payload: P,
options: OpenModalOptions = {}
) {
const { onClose = async () => {} } = options;
let resolve: ((result: R) => void) | undefined;
const promise = new Promise((resolveImpl) => {
resolve = resolveImpl;
});
invariant(resolve, 'resolve not set');
const key = `${keyPrefix}-${nextId.current}`;
nextId.current += 1;
const newEntry: ModalStackEntry = {
key,
open: true,
promise,
Component,
payload,
onClose,
resolve,
};
setStack((prevStack) => [...prevStack, newEntry]);
return promise;
},
[keyPrefix]
);
const closeModalUi = React.useCallback(
function closeDialogUi(modal: Promise) {
const updateStack = (prevStack: typeof stack) =>
prevStack.map((entry) =>
entry.promise === modal ? { ...entry, open: false } : entry
);
const filterStack = (prevStack: typeof stack) =>
prevStack.filter((entry) => entry.promise !== modal);
setStack(updateStack);
setTimeout(() => {
setStack(filterStack);
}, unmountAfter);
},
[unmountAfter]
);
const closeModal = React.useCallback(
async function closeModal(modal: Promise, result: R): Promise {
const entryToClose = stack.find((entry) => entry.promise === modal);
invariant(entryToClose, 'Dialog not found');
const { onClose, resolve } = entryToClose;
await onClose(result);
resolve(result);
closeModalUi(modal);
return modal;
},
[stack, closeModalUi]
);
const contextValue = React.useMemo(
() => ({ open: requestModal, close: closeModal }),
[requestModal, closeModal]
);
return (
{children}
{stack.map(({ key, open, Component, payload, promise }) => (
{
await closeModal(promise, result);
}}
/>
))}
);
};
export { ModalsProvider };