@zag-js/dismissable
Version:
Dismissable layer utilities for the DOM
112 lines (111 loc) • 3.98 kB
JavaScript
// src/dismissable-layer.ts
import { contains, getEventTarget, isHTMLElement, raf } from "@zag-js/dom-query";
import {
trackInteractOutside
} from "@zag-js/interact-outside";
import { isFunction, warn } from "@zag-js/utils";
import { trackEscapeKeydown } from "./escape-keydown.mjs";
import { layerStack } from "./layer-stack.mjs";
import { assignPointerEventToLayers, clearPointerEvent, disablePointerEventsOutside } from "./pointer-event-outside.mjs";
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, onRequestDismiss, pointerBlocking, exclude: excludeContainers, debug, type = "dialog" } = options;
const layer = { dismiss: onDismiss, node, type, pointerBlocking, requestDismiss: onRequestDismiss };
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
};