@blocdigital/usetoplayerelement
Version:
React hook for monitoring the top layer
101 lines (100 loc) • 4.28 kB
JavaScript
;
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]);
}