c7644ade69
@robonen/vue (toolkit): migrate to eslint flat config + composite tsconfig; fix composable + test type errors (writable computed returns, null guards, overload-compatible signatures, typed test helpers) — all type-level.
61 lines
1.7 KiB
TypeScript
61 lines
1.7 KiB
TypeScript
import type { VoidFunction } from '@robonen/stdlib';
|
|
import { noop } from '@robonen/stdlib';
|
|
import { useEventListener } from '@/composables/browser/useEventListener';
|
|
import { tryOnScopeDispose } from '@/composables/lifecycle/tryOnScopeDispose';
|
|
import { defaultWindow } from '@/types';
|
|
|
|
type EscapeListener = (event: KeyboardEvent) => void;
|
|
|
|
// Module-scoped stack: only the topmost non-paused layer handles Escape so that
|
|
// nested dismissables behave correctly (top-most dialog closes first).
|
|
const stack: EscapeListener[] = [];
|
|
let installed = false;
|
|
let cleanup: VoidFunction = noop;
|
|
|
|
function install() {
|
|
if (installed || !defaultWindow) return;
|
|
installed = true;
|
|
|
|
cleanup = useEventListener(defaultWindow, 'keydown', (event: KeyboardEvent) => {
|
|
if (event.key !== 'Escape') return;
|
|
|
|
const top = stack.at(-1);
|
|
top?.(event);
|
|
}, { capture: true });
|
|
}
|
|
|
|
function uninstall() {
|
|
if (!installed || stack.length > 0) return;
|
|
installed = false;
|
|
cleanup();
|
|
cleanup = noop;
|
|
}
|
|
|
|
/**
|
|
* @name useEscapeKey
|
|
* @category Browser
|
|
* @description Register a callback for the topmost Escape keydown. Uses an internal
|
|
* stack so that nested layers (e.g. nested Dialogs) dismiss in the correct order —
|
|
* only the most recently-registered listener fires for a given keydown.
|
|
*
|
|
* @param {(event: KeyboardEvent) => void} handler Callback invoked on the topmost Escape
|
|
* @returns {VoidFunction} Stop handle that removes the subscription
|
|
*
|
|
* @since 0.0.14
|
|
*/
|
|
export function useEscapeKey(handler: EscapeListener): VoidFunction {
|
|
if (!defaultWindow) return noop;
|
|
|
|
install();
|
|
stack.push(handler);
|
|
|
|
const stop = () => {
|
|
const i = stack.lastIndexOf(handler);
|
|
if (i !== -1) stack.splice(i, 1);
|
|
uninstall();
|
|
};
|
|
|
|
tryOnScopeDispose(stop);
|
|
return stop;
|
|
}
|