UNPKG

@zag-js/dismissable

Version:

Dismissable layer utilities for the DOM

218 lines (213 loc) • 7.28 kB
'use strict'; var domQuery = require('@zag-js/dom-query'); var interactOutside = require('@zag-js/interact-outside'); var utils = require('@zag-js/utils'); // src/dismissable-layer.ts function trackEscapeKeydown(node, fn) { const handleKeyDown = (event) => { if (event.key !== "Escape") return; if (event.isComposing) return; fn?.(event); }; return domQuery.addDomEvent(domQuery.getDocument(node), "keydown", handleKeyDown, { capture: true }); } var layerStack = { layers: [], branches: [], count() { return this.layers.length; }, pointerBlockingLayers() { return this.layers.filter((layer) => layer.pointerBlocking); }, topMostPointerBlockingLayer() { return [...this.pointerBlockingLayers()].slice(-1)[0]; }, hasPointerBlockingLayer() { return this.pointerBlockingLayers().length > 0; }, isBelowPointerBlockingLayer(node) { const index = this.indexOf(node); const highestBlockingIndex = this.topMostPointerBlockingLayer() ? this.indexOf(this.topMostPointerBlockingLayer()?.node) : -1; return index < highestBlockingIndex; }, isTopMost(node) { const layer = this.layers[this.count() - 1]; return layer?.node === node; }, getNestedLayers(node) { return Array.from(this.layers).slice(this.indexOf(node) + 1); }, isInNestedLayer(node, target) { return this.getNestedLayers(node).some((layer) => domQuery.contains(layer.node, target)); }, isInBranch(target) { return Array.from(this.branches).some((branch) => domQuery.contains(branch, target)); }, add(layer) { const num = this.layers.push(layer); layer.node.style.setProperty("--layer-index", `${num}`); }, addBranch(node) { this.branches.push(node); }, remove(node) { const index = this.indexOf(node); if (index < 0) return; if (index < this.count() - 1) { const _layers = this.getNestedLayers(node); _layers.forEach((layer) => layer.dismiss()); } this.layers.splice(index, 1); node.style.removeProperty("--layer-index"); }, removeBranch(node) { const index = this.branches.indexOf(node); if (index >= 0) this.branches.splice(index, 1); }, indexOf(node) { return this.layers.findIndex((layer) => layer.node === node); }, dismiss(node) { this.layers[this.indexOf(node)]?.dismiss(); }, clear() { this.remove(this.layers[0].node); } }; var originalBodyPointerEvents; function assignPointerEventToLayers() { layerStack.layers.forEach(({ node }) => { node.style.pointerEvents = layerStack.isBelowPointerBlockingLayer(node) ? "none" : "auto"; }); } function clearPointerEvent(node) { node.style.pointerEvents = ""; } function disablePointerEventsOutside(node, persistentElements) { const doc = domQuery.getDocument(node); const cleanups = []; if (layerStack.hasPointerBlockingLayer() && !doc.body.hasAttribute("data-inert")) { originalBodyPointerEvents = document.body.style.pointerEvents; queueMicrotask(() => { doc.body.style.pointerEvents = "none"; doc.body.setAttribute("data-inert", ""); }); } if (persistentElements) { const persistedCleanup = domQuery.waitForElements(persistentElements, (el) => { cleanups.push(domQuery.setStyle(el, { pointerEvents: "auto" })); }); cleanups.push(persistedCleanup); } return () => { if (layerStack.hasPointerBlockingLayer()) return; queueMicrotask(() => { doc.body.style.pointerEvents = originalBodyPointerEvents; doc.body.removeAttribute("data-inert"); if (doc.body.style.length === 0) doc.body.removeAttribute("style"); }); cleanups.forEach((fn) => fn()); }; } // src/dismissable-layer.ts function trackDismissableElementImpl(node, options) { const { warnOnMissingNode = true } = options; if (warnOnMissingNode && !node) { utils.warn("[@zag-js/dismissable] node is `null` or `undefined`"); return; } if (!node) { return; } const { onDismiss, pointerBlocking, exclude: excludeContainers, debug } = options; const layer = { dismiss: onDismiss, node, pointerBlocking }; layerStack.add(layer); assignPointerEventToLayers(); function onPointerDownOutside(event) { const target = domQuery.getEventTarget(event.detail.originalEvent); if (layerStack.isBelowPointerBlockingLayer(node) || layerStack.isInBranch(target)) return; options.onPointerDownOutside?.(event); options.onInteractOutside?.(event); if (event.defaultPrevented) return; if (debug) { console.log("onPointerDownOutside:", event.detail.originalEvent); } onDismiss?.(); } function onFocusOutside(event) { const target = domQuery.getEventTarget(event.detail.originalEvent); if (layerStack.isInBranch(target)) return; options.onFocusOutside?.(event); options.onInteractOutside?.(event); if (event.defaultPrevented) return; if (debug) { console.log("onFocusOutside:", event.detail.originalEvent); } onDismiss?.(); } function onEscapeKeyDown(event) { if (!layerStack.isTopMost(node)) return; options.onEscapeKeyDown?.(event); if (!event.defaultPrevented && onDismiss) { event.preventDefault(); onDismiss(); } } function exclude(target) { if (!node) return false; const containers = typeof excludeContainers === "function" ? excludeContainers() : excludeContainers; const _containers = Array.isArray(containers) ? containers : [containers]; const persistentElements = options.persistentElements?.map((fn) => fn()).filter(domQuery.isHTMLElement); if (persistentElements) _containers.push(...persistentElements); return _containers.some((node2) => domQuery.contains(node2, target)) || layerStack.isInNestedLayer(node, target); } const cleanups = [ pointerBlocking ? disablePointerEventsOutside(node, options.persistentElements) : void 0, trackEscapeKeydown(node, onEscapeKeyDown), interactOutside.trackInteractOutside(node, { exclude, onFocusOutside, onPointerDownOutside, defer: options.defer }) ]; return () => { layerStack.remove(node); assignPointerEventToLayers(); clearPointerEvent(node); cleanups.forEach((fn) => fn?.()); }; } function trackDismissableElement(nodeOrFn, options) { const { defer } = options; const func = defer ? domQuery.raf : (v) => v(); const cleanups = []; cleanups.push( func(() => { const node = utils.isFunction(nodeOrFn) ? nodeOrFn() : nodeOrFn; cleanups.push(trackDismissableElementImpl(node, options)); }) ); return () => { cleanups.forEach((fn) => fn?.()); }; } function trackDismissableBranch(nodeOrFn, options = {}) { const { defer } = options; const func = defer ? domQuery.raf : (v) => v(); const cleanups = []; cleanups.push( func(() => { const node = utils.isFunction(nodeOrFn) ? nodeOrFn() : nodeOrFn; if (!node) { utils.warn("[@zag-js/dismissable] branch node is `null` or `undefined`"); return; } layerStack.addBranch(node); cleanups.push(() => { layerStack.removeBranch(node); }); }) ); return () => { cleanups.forEach((fn) => fn?.()); }; } exports.trackDismissableBranch = trackDismissableBranch; exports.trackDismissableElement = trackDismissableElement;