Add focus trapping for modal dialogs

- Focus moves to first focusable element when modal opens
- Tab/Shift+Tab cycles within modal only
- Escape key closes modal
- Focus returns to previously focused element on close

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2025-12-24 10:34:22 -05:00
parent fa9e24763f
commit 44d0f7a257

View File

@@ -1142,12 +1142,64 @@
}
}
let previouslyFocusedElement = null;
let activeModalId = null;
function openModal(id) {
document.getElementById(id).classList.add('active');
previouslyFocusedElement = document.activeElement;
activeModalId = id;
const modal = document.getElementById(id);
modal.classList.add('active');
// Focus first focusable element in modal
const focusable = modal.querySelectorAll('button, input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (focusable.length > 0) {
setTimeout(() => focusable[0].focus(), 50);
}
// Add event listeners for focus trap and Escape
document.addEventListener('keydown', handleModalKeydown);
}
function closeModal(id) {
document.getElementById(id).classList.remove('active');
document.removeEventListener('keydown', handleModalKeydown);
activeModalId = null;
// Restore focus to previously focused element
if (previouslyFocusedElement) {
previouslyFocusedElement.focus();
previouslyFocusedElement = null;
}
}
function handleModalKeydown(e) {
if (!activeModalId) return;
const modal = document.getElementById(activeModalId);
// Close on Escape
if (e.key === 'Escape') {
e.preventDefault();
closeModal(activeModalId);
return;
}
// Trap focus on Tab
if (e.key === 'Tab') {
const focusable = modal.querySelectorAll('button, input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
}
async function verifyPin() {