Fix modal focus trapping and add focus indicators

Focus trapping:
- Manually handle all Tab key navigation within modals
- Use getComputedStyle and offsetWidth/Height to detect visible elements
- Focus appropriate input field when modal opens (PIN or Name)

Focus indicators:
- Add visible outline on buttons (:focus)
- Add visible outline on checkbox (:focus)
- Add visible outline on modal close button (:focus)

🤖 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:56:01 -05:00
parent 7d39ea4dd8
commit 39b1ff7bc0

View File

@@ -331,6 +331,11 @@
background: var(--border); background: var(--border);
} }
.modal-close:focus {
outline: 2px solid var(--forest);
outline-offset: 2px;
}
.modal-body { .modal-body {
padding: 24px; padding: 24px;
} }
@@ -405,6 +410,11 @@
background: var(--terracotta-light); background: var(--terracotta-light);
} }
.btn:focus {
outline: 3px solid var(--oak);
outline-offset: 2px;
}
.btn-secondary { .btn-secondary {
background: var(--cream); background: var(--cream);
color: var(--charcoal); color: var(--charcoal);
@@ -427,6 +437,11 @@
accent-color: var(--forest); accent-color: var(--forest);
} }
.checkbox-group input:focus {
outline: 2px solid var(--forest);
outline-offset: 2px;
}
.pin-input { .pin-input {
letter-spacing: 8px; letter-spacing: 8px;
text-align: center; text-align: center;
@@ -1156,11 +1171,23 @@
// Make background content inert (unfocusable) // Make background content inert (unfocusable)
document.getElementById('mainContent').setAttribute('inert', ''); document.getElementById('mainContent').setAttribute('inert', '');
// Focus first focusable element in modal // Focus the primary input field in the modal
setTimeout(() => { setTimeout(() => {
const focusable = getFocusableElements(modal); let focusTarget = null;
if (focusable.length > 0) { if (id === 'pinModal') {
focusable[0].focus(); focusTarget = document.getElementById('pinInput');
} else if (id === 'setPinModal') {
focusTarget = document.getElementById('newPinInput');
} else if (id === 'itemModal') {
focusTarget = document.getElementById('itemName');
}
if (focusTarget) {
focusTarget.focus();
} else {
const focusable = getFocusableElements(modal);
if (focusable.length > 0) {
focusable[0].focus();
}
} }
}, 50); }, 50);
@@ -1184,11 +1211,15 @@
} }
function getFocusableElements(container) { function getFocusableElements(container) {
const elements = container.querySelectorAll('button, input:not([type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])'); const selector = 'button, input:not([type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])';
return Array.from(elements).filter(el => { const elements = Array.from(container.querySelectorAll(selector));
return elements.filter(el => {
if (el.disabled) return false; if (el.disabled) return false;
// Check if element is visible
if (el.offsetWidth === 0 && el.offsetHeight === 0) return false;
const style = window.getComputedStyle(el); const style = window.getComputedStyle(el);
return style.display !== 'none' && style.visibility !== 'hidden'; if (style.display === 'none' || style.visibility === 'hidden') return false;
return true;
}); });
} }
@@ -1203,21 +1234,22 @@
return; return;
} }
// Trap focus on Tab // Handle all Tab navigation manually
if (e.key === 'Tab') { if (e.key === 'Tab') {
e.preventDefault();
const focusable = getFocusableElements(modal); const focusable = getFocusableElements(modal);
if (focusable.length === 0) return; if (focusable.length === 0) return;
const first = focusable[0]; const currentIndex = focusable.indexOf(document.activeElement);
const last = focusable[focusable.length - 1]; let nextIndex;
if (e.shiftKey && document.activeElement === first) { if (e.shiftKey) {
e.preventDefault(); nextIndex = currentIndex <= 0 ? focusable.length - 1 : currentIndex - 1;
last.focus(); } else {
} else if (!e.shiftKey && document.activeElement === last) { nextIndex = currentIndex >= focusable.length - 1 ? 0 : currentIndex + 1;
e.preventDefault();
first.focus();
} }
focusable[nextIndex].focus();
} }
} }