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:
64
index.html
64
index.html
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user