UNPKG

@melt-ui/svelte

Version:
78 lines (77 loc) 2.96 kB
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'; };