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);
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user