export function debounce( node: HTMLInputElement, { callback, timeout = 500 }: { callback: () => void; timeout?: number } ) { let timer: NodeJS.Timeout; function trigger(event: KeyboardEvent) { clearTimeout(timer); if (event.key !== 'Enter') { timer = setTimeout(callback, timeout); } else { callback(); } } node.addEventListener('keyup', trigger); return { destroy() { clearTimeout(timer); node.removeEventListener('keyup', trigger); } }; } export function clickOutside( node: HTMLElement, { handler, ignore }: { handler: () => void; ignore?: HTMLElement } ) { const handle = (event: Event) => { const target = event.target as HTMLElement; if (!target || target === ignore) return; if (node && !node.contains(target) && !event.defaultPrevented) { handler(); } }; document.addEventListener('click', handle, true); return { destroy() { document.removeEventListener('click', handle, true); } }; } export const focusableElements = [ 'a[href]', 'area[href]', 'input:not([disabled]):not([type="hidden"]):not([aria-hidden])', 'select:not([disabled]):not([aria-hidden])', 'textarea:not([disabled]):not([aria-hidden])', 'button:not([disabled]):not([aria-hidden])', 'iframe', 'object', 'embed', '[contenteditable]', '[tabindex]:not([tabindex^="-"])' ]; let trapped: HTMLElement[] = []; export function trapFocus(node: HTMLElement) { function handler(event: KeyboardEvent) { if (event.target === window) return; // return if we're not the topmost node to handle if (trapped.at(0) !== node) return; const focusable = node.querySelectorAll(focusableElements.join()); const first = focusable[0]; const last = focusable[focusable.length - 1]; if (event.key === 'Tab') { if (!node.contains(document.activeElement)) { first.focus(); event.preventDefault(); } if (event.shiftKey && event.target === first) { last.focus(); event.preventDefault(); } else if (!event.shiftKey && event.target === last) { first.focus(); event.preventDefault(); } } } if (document.activeElement instanceof HTMLElement) { // if we trap focus, make sure to blur any previously selected external // item such that focus does not remain outside of the node if (!node.contains(document.activeElement)) { document.activeElement.blur(); } } document.addEventListener('keydown', handler); trapped.unshift(node); return { destroy() { document.removeEventListener('keydown', handler); trapped = trapped.filter((i) => i !== node); } }; }