@zag-js/dismissable
Version:
Dismissable layer utilities for the DOM
215 lines (211 loc) • 7.16 kB
JavaScript
import { raf, contains, getDocument, waitForElements, setStyle, addDomEvent, getEventTarget, isHTMLElement } from '@zag-js/dom-query';
import { trackInteractOutside } from '@zag-js/interact-outside';
import { isFunction, warn } from '@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 addDomEvent(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) => contains(layer.node, target));
},
isInBranch(target) {
return Array.from(this.branches).some((branch) => 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 = 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 = waitForElements(persistentElements, (el) => {
cleanups.push(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) {
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 = 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 = 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(isHTMLElement);
if (persistentElements) _containers.push(...persistentElements);
return _containers.some((node2) => contains(node2, target)) || layerStack.isInNestedLayer(node, target);
}
const cleanups = [
pointerBlocking ? disablePointerEventsOutside(node, options.persistentElements) : void 0,
trackEscapeKeydown(node, onEscapeKeyDown),
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 ? raf : (v) => v();
const cleanups = [];
cleanups.push(
func(() => {
const node = isFunction(nodeOrFn) ? nodeOrFn() : nodeOrFn;
cleanups.push(trackDismissableElementImpl(node, options));
})
);
return () => {
cleanups.forEach((fn) => fn?.());
};
}
function trackDismissableBranch(nodeOrFn, options = {}) {
const { defer } = options;
const func = defer ? raf : (v) => v();
const cleanups = [];
cleanups.push(
func(() => {
const node = isFunction(nodeOrFn) ? nodeOrFn() : nodeOrFn;
if (!node) {
warn("[@zag-js/dismissable] branch node is `null` or `undefined`");
return;
}
layerStack.addBranch(node);
cleanups.push(() => {
layerStack.removeBranch(node);
});
})
);
return () => {
cleanups.forEach((fn) => fn?.());
};
}
export { trackDismissableBranch, trackDismissableElement };