import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from "react"; import ReactDOM from "react-dom"; import {v4 as uuidv4} from "uuid"; /** * Curtain UUID type. */ export type CurtainUuidType = string; /** * The function that opens a curtain. */ export type OpenCurtainFunction = (content: React.ReactNode) => CurtainUuidType; /** * The function that closes a curtain. */ export type CloseCurtainFunction = (uuid: CurtainUuidType) => void; /** * Interface of curtains state. */ export interface CurtainsContextState { /** * Open a new curtain. * @param content The curtain content. * @return UUID of the curtain. */ open: OpenCurtainFunction; /** * Close the given curtain. * @param uuid UUID of the curtain to close. */ close: CloseCurtainFunction; } const CurtainsContext = React.createContext({ // Empty functions. open() { return ""; }, close() {}, }); /** * Hook to interact with curtains (open or close). */ export function useCurtains(): CurtainsContextState { return useContext(CurtainsContext); } /** * Page curtains system. */ export function Curtains({children}: React.PropsWithChildren<{}>) { // Curtains state. const [curtains, setCurtains] = useState>({}); // Initialize open curtain function. const open = useRef(); open.current = useCallback((content: React.ReactNode) => { // Generate a new curtain UUID for the new curtain to open. const curtainUuid = uuidv4(); // Add the curtain to open to the list of curtains, with the generated UUID. setCurtains({ ...curtains, [curtainUuid]: content, }); // Return the curtain UUID. return curtainUuid; }, [curtains, setCurtains]); // Initialize close curtain function. const close = useRef(); close.current = useCallback((uuid: CurtainUuidType) => { // Copy the curtains list. const newCurtains = {...curtains}; // Remove the given curtain from the list. delete newCurtains[uuid]; // Set the new curtains list. setCurtains(newCurtains); }, [curtains, setCurtains]); // Initialize context state from action functions. const contextState = useMemo(() => ({ open: (content: React.ReactNode) => open.current(content), close: (uuid: CurtainUuidType) => close.current(uuid), }), [open, close]); // Show dimmed main content. useEffect(() => { if (Object.entries(curtains).length > 0 && !document.body.classList.contains("dimmed")) // We should dim content and it's currently not. document.body.classList.add("dimmed"); else // We shouldn't dim content. document.body.classList.remove("dimmed"); }, [curtains]); return ( {children} ); } /** * Curtains portal manager. */ function CurtainsPortal({curtains}: { curtains: Record; }) { return ReactDOM.createPortal(Object.entries(curtains).map(([uuid, curtainContent]) => ( {curtainContent} )), document.body); } /** * Component of an opened curtain instance. */ function CurtainInstance({children}: React.PropsWithChildren<{}>) { return (
{children}
); }