UNPKG

@blocdigital/usetoplayerelement

Version:
101 lines (100 loc) 4.28 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = useTopLayerElements; const react_1 = require("react"); const topLayerElements = new Set(); // Custom event to notify elements when they are added/removed from the top layer const topLayerEvent = (inTopLayer) => new CustomEvent('topLayer', { bubbles: true, detail: { inTopLayer } }); if (typeof document !== 'undefined') { // Listen for elements being added/removed from the top layer document.addEventListener('toggle', ({ target }) => { if (!target) return; const el = target; if (!(el instanceof HTMLDialogElement || el.hasAttribute('popover'))) return; if (el.matches(':modal, :popover-open') && document.contains(el)) { topLayerElements.add(el); el.dispatchEvent(topLayerEvent(true)); } else { if (topLayerElements.delete(el)) el.dispatchEvent(topLayerEvent(false)); } }, { capture: true }); // MutationObserver for automatic cleanup const observer = new MutationObserver((mutations) => { const nodes = mutations.flatMap(({ removedNodes }) => Array.from(removedNodes).filter((node) => node instanceof HTMLElement)); for (const node of nodes) if (topLayerElements.delete(node)) document.dispatchEvent(topLayerEvent(false)); }); observer.observe(document.body, { childList: true, subtree: true }); } /** * React hook to track and interact with elements in the "top layer" (such as dialogs and popovers). * * This hook provides a ref to attach to your element, and returns information about its position * and presence in the top layer, as well as the current list of top layer elements. * * The top layer is determined by listening for the `toggle` event on dialogs and popovers, * and by observing DOM mutations for automatic cleanup. * * @template T - The type of HTMLElement to track. * @returns {useTopLayerElementsReturn<T>} An object containing: * - `ref`: React ref to attach to your element. * - `topElement`: The topmost element in the top layer. * - `topDialog`: The topmost dialog element in the top layer. * - `isInTopLayer`: Whether your element is currently in the top layer. * - `isTopElement`: Whether your element is the topmost element in the top layer. * - `topLayerList`: Array of all elements currently in the top layer. * * @example * ```tsx * import useTopLayerElements from './useTopLayerElement'; * * function MyDialog() { * const { ref, isInTopLayer, isTopElement } = useTopLayerElements<HTMLDialogElement>(); * * return ( * <dialog ref={ref} open> * {isInTopLayer && <span>I'm in the top layer!</span>} * {isTopElement && <span>I'm the topmost dialog!</span>} * </dialog> * ); * } * ``` * * @example * ```tsx * import useTopLayerElements from './useTopLayerElement'; * * function MyPopover() { * const { ref, topLayerList } = useTopLayerElements<HTMLDivElement>(); * * return ( * <div ref={ref} popover="auto"> * <span>Current top layer count: {topLayerList.length}</span> * </div> * ); * } * ``` */ function useTopLayerElements() { const ref = (0, react_1.useRef)(null); const [topLayerList, setTopLayerList] = (0, react_1.useState)([...topLayerElements]); const derivedState = (0, react_1.useMemo)(() => { const topElement = topLayerList.length ? topLayerList.at(-1) || null : null; const topDialog = topLayerList.findLast((el) => el.tagName === 'DIALOG') || null; const isTopElement = ref.current ? topElement === ref.current : false; const isInTopLayer = ref.current ? topLayerList.includes(ref.current) : false; return { topElement, topDialog, isTopElement, isInTopLayer }; }, [topLayerList]); // Listen for top layer changes (0, react_1.useEffect)(() => { const ac = new AbortController(); document.addEventListener('topLayer', () => setTopLayerList([...topLayerElements]), { signal: ac.signal }); return () => ac.abort(); }, []); return (0, react_1.useMemo)(() => ({ ref, ...derivedState, topLayerList }), [derivedState, topLayerList]); }