@melt-ui/svelte
Version:

78 lines (77 loc) • 2.96 kB
JavaScript
import { addEventListener } from '../../helpers/event.js';
import { isFunction, isHTMLElement, isReadable } from '../../helpers/is.js';
import { readable } from 'svelte/store';
import { effect, executeCallbacks, kbd, noop, withGet } from '../../helpers/index.js';
const layers = new Map();
export const useEscapeKeydown = ((node, config = {}) => {
let unsub = noop;
function update(config = {}) {
unsub();
const options = { behaviorType: 'close', ...config };
const behaviorType = isReadable(options.behaviorType)
? options.behaviorType
: withGet(readable(options.behaviorType));
layers.set(node, behaviorType);
const onKeyDown = (e) => {
if (e.key !== kbd.ESCAPE || !isResponsibleEscapeLayer(node))
return;
const target = e.target;
if (!isHTMLElement(target))
return;
e.preventDefault();
if (shouldIgnoreEvent(e, options.ignore))
return;
if (shouldInvokeResponsibleLayerHandler(behaviorType.get())) {
options.handler?.(e);
}
};
unsub = executeCallbacks(addEventListener(document, 'keydown', onKeyDown, { passive: false }), effect(behaviorType, ($behaviorType) => {
if ($behaviorType === 'close' ||
($behaviorType === 'defer-otherwise-close' && [...layers.keys()][0] === node)) {
node.dataset.escapee = '';
}
else {
delete node.dataset.escapee;
}
}), behaviorType.destroy || noop);
}
update(config);
return {
update,
destroy() {
layers.delete(node);
delete node.dataset.escapee;
unsub();
},
};
});
const isResponsibleEscapeLayer = (node) => {
const layersArr = [...layers];
/**
* We first check if we can find a top layer with `close` or `ignore`.
* If that top layer was found and matches the provided node, then the node is
* responsible for the escape. Otherwise, we know that all layers defer so
* the first layer is the responsible one.
*/
const topMostLayer = layersArr.findLast(([_, behaviorType]) => {
const $behaviorType = behaviorType.get();
return $behaviorType === 'close' || $behaviorType === 'ignore';
});
if (topMostLayer)
return topMostLayer[0] === node;
const [firstLayerNode] = layersArr[0];
return firstLayerNode === node;
};
const shouldIgnoreEvent = (e, ignore) => {
if (!ignore)
return false;
if (isFunction(ignore) && ignore(e))
return true;
if (Array.isArray(ignore) && ignore.some((ignoreEl) => e.target === ignoreEl)) {
return true;
}
return false;
};
const shouldInvokeResponsibleLayerHandler = (behaviorType) => {
return behaviorType === 'close' || behaviorType === 'defer-otherwise-close';
};