From 39b1ff7bc027bba31999dae711314218a1cb67e6 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Wed, 24 Dec 2025 10:56:01 -0500 Subject: [PATCH] Fix modal focus trapping and add focus indicators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- index.html | 64 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/index.html b/index.html index c341616..f7ff57d 100644 --- a/index.html +++ b/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(); } }