const focusableSelector = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; const stack = []; function focusables(modal) { return [...modal.querySelectorAll(focusableSelector)].filter(element => element.getClientRects().length && !element.closest('.hidden, [hidden]')); } export function openModal(modal, preferredFocus) { const previous = document.activeElement; modal.classList.remove('hidden'); stack.push({modal, previous}); (preferredFocus || focusables(modal)[0] || modal.querySelector('[role="dialog"]'))?.focus(); } export function closeModal(modal) { const index = stack.findLastIndex(entry => entry.modal === modal); if (index < 0) return; const [{previous}] = stack.splice(index, 1); modal.classList.add('hidden'); if (index === stack.length) { const target = stack.length ? focusables(stack.at(-1).modal)[0] : previous; if (target?.isConnected) target.focus(); } } export function topModal() { return stack.at(-1)?.modal || null; } document.addEventListener('keydown', event => { if (event.key !== 'Tab' || !stack.length) return; const modal = topModal(); const elements = focusables(modal); if (!elements.length) { event.preventDefault(); modal.querySelector('[role="dialog"]')?.focus(); return; } const first = elements[0], last = elements.at(-1); if (event.shiftKey && (document.activeElement === first || !modal.contains(document.activeElement))) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && (document.activeElement === last || !modal.contains(document.activeElement))) { event.preventDefault(); first.focus(); } }, true);