41 lines
1.7 KiB
JavaScript
41 lines
1.7 KiB
JavaScript
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);
|