import React, { createContext, createElement, Fragment, FunctionComponent, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState, } from "react"; import { DeleteConfirmationModal, DeleteConfirmationModalProps, } from ":/components/Modal/DeleteConfirmationModal"; import { randomString } from ":/utils"; import { ModalProps, useModal } from ":/components/Modal/index"; import { ConfirmationModal, ConfirmationModalProps, } from ":/components/Modal/ConfirmationModal"; import { WithOptional } from ":/types"; import { MessageModal, MessageModalProps, } from ":/components/Modal/MessageModal"; export const NOSCROLL_CLASS = "c__noscroll"; export type Decision = string | null | undefined; export type DecisionModalProps = WithOptional & { onDecide: (decision?: Decision) => void; }; // TODO: I don't really like this "& any", but it's the only way I found to make it work for MessageModal. type DecisionModalComponent = FunctionComponent; interface ModalContextType { deleteConfirmationModal: ( props?: Partial, ) => Promise; confirmationModal: ( props?: Partial, ) => Promise; messageModal: (props?: Partial) => Promise; } const ModalContext = createContext(undefined); export const useModals = () => { const context = useContext(ModalContext); if (context === undefined) { throw new Error("useModals must be used within a ModalProvider."); } return context; }; /** * This component is used to wrap a modal component and add the modal context to it. * It handles creating a generic `component` modal, and closing it when the `onClose` callback is called. * * @param component * @param onClose * @param props * @constructor */ const ModalContainer = ({ component, onClose, props, }: { component: DecisionModalComponent; onClose: Function; props: Partial; }) => { const modal = useModal({ isOpenDefault: true }); const close = () => { modal.onClose(); onClose(); }; const onCloseLocal = () => { close(); // Closing sends a "undefined" decision. props.onDecide?.(); }; const onDecide = (decision?: Decision) => { close(); props.onDecide?.(decision); }; return createElement(component, { ...modal, ...props, onClose: onCloseLocal, onDecide, }); }; type ModalMap = Map; export const ModalProvider = ({ children }: PropsWithChildren) => { const [modals, setModals] = useState({} as ModalMap); const addModal = ( component: FunctionComponent, props: Partial, ) => { const key = randomString(32); const container = ( { setModals((modals_) => { // @ts-ignore delete modals_[key]; return modals_; }); }} /> ); setModals((modals_) => ({ ...modals_, [key]: container, })); }; const ModalHelper = (component: DecisionModalComponent) => { return (props: Partial = {}) => { return new Promise((resolve) => { addModal(component, { onDecide: (decision) => { resolve(decision); }, ...props, }); }); }; }; const context: ModalContextType = useMemo( () => ({ deleteConfirmationModal: ModalHelper(DeleteConfirmationModal), confirmationModal: ModalHelper(ConfirmationModal), messageModal: ModalHelper(MessageModal), }), [], ); useEffect(() => { const portalElement = document.getElementById("c__modals-portal")!; // Create an observer instance linked to the callback function const observer = new MutationObserver(() => { const dialogs = portalElement.querySelectorAll("dialog"); if (dialogs.length > 0) { document.querySelector("body")!.classList.add(NOSCROLL_CLASS); } else { document.querySelector("body")!.classList.remove(NOSCROLL_CLASS); } }); // Start observing the target node for configured mutations observer.observe(portalElement, { childList: true, }); return () => { observer.disconnect(); }; }, []); return ( {children}
{Object.entries(modals).map(([key, modal]) => ( {modal} ))} ); };