Core/src/Components/Curtains/Curtains.tsx

137 lines
3.3 KiB
TypeScript
Raw Normal View History

2024-07-14 14:21:51 +02:00
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<CurtainsContextState>({
// 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<Record<CurtainUuidType, React.ReactNode>>({});
// Initialize open curtain function.
const open = useRef<OpenCurtainFunction>();
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<CloseCurtainFunction>();
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 (
<CurtainsContext.Provider value={contextState}>
{children}
<CurtainsPortal curtains={curtains} />
</CurtainsContext.Provider>
);
}
/**
* Curtains portal manager.
*/
function CurtainsPortal({curtains}: {
curtains: Record<CurtainUuidType, React.ReactNode>;
})
{
return ReactDOM.createPortal(Object.entries(curtains).map(([uuid, curtainContent]) => (
<CurtainInstance key={uuid}>
{curtainContent}
</CurtainInstance>
)), document.body);
}
/**
* Component of an opened curtain instance.
*/
function CurtainInstance({children}: React.PropsWithChildren<{}>)
{
return (
<div className={"curtain"}>
{children}
</div>
);
}