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