From cb8554e904fad1002d71d338ff9873a266fd5fe4 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Wed, 15 Apr 2026 03:22:31 +0000 Subject: [PATCH] feat: crisis overlay full Tab cycle + Escape dismiss (#95) Three fixes: 1. Focus lands on Call 988 link on open (not disabled dismiss button) 2. Focus trap catches escaped focus outside overlay 3. Escape key closes overlay, returns focus to chat input Closes #95 --- index.html | 68 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/index.html b/index.html index 06cff1c..88d4c8b 100644 --- a/index.html +++ b/index.html @@ -1001,6 +1001,13 @@ Sovereignty and service always.`; var first = focusable[0]; var last = focusable[focusable.length - 1]; + // If focus escaped outside the overlay (e.g. to body), redirect to first + if (!crisisOverlay.contains(document.activeElement)) { + e.preventDefault(); + first.focus(); + return; + } + if (e.shiftKey) { // Shift+Tab: if on first, wrap to last if (document.activeElement === first) { @@ -1050,38 +1057,55 @@ Sovereignty and service always.`; } }, 1000); - overlayDismissBtn.focus(); + // Focus the first focusable element (call link) — dismiss button is still disabled + var firstFocusable = crisisOverlay.querySelector('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])'); + if (firstFocusable) { + firstFocusable.focus(); + } } // Register focus trap on document (always listening, gated by class check) document.addEventListener('keydown', trapFocusInOverlay); + function dismissOverlay() { + crisisOverlay.classList.remove('active'); + if (overlayTimer) { + clearInterval(overlayTimer); + overlayTimer = null; + } + + // Re-enable background interaction + var mainApp = document.querySelector('.app'); + if (mainApp) mainApp.removeAttribute('inert'); + var chatSection = document.getElementById('chat'); + if (chatSection) chatSection.removeAttribute('aria-hidden'); + var footerEl = document.querySelector('footer'); + if (footerEl) footerEl.removeAttribute('aria-hidden'); + + // Restore focus to the element that had it before the overlay opened + if (_preOverlayFocusElement && typeof _preOverlayFocusElement.focus === 'function') { + _preOverlayFocusElement.focus(); + } else { + msgInput.focus(); + } + _preOverlayFocusElement = null; + } + overlayDismissBtn.addEventListener('click', function() { if (!overlayDismissBtn.disabled) { - crisisOverlay.classList.remove('active'); - if (overlayTimer) { - clearInterval(overlayTimer); - overlayTimer = null; - } - - // Re-enable background interaction - var mainApp = document.querySelector('.app'); - if (mainApp) mainApp.removeAttribute('inert'); - var chatSection = document.getElementById('chat'); - if (chatSection) chatSection.removeAttribute('aria-hidden'); - var footerEl = document.querySelector('footer'); - if (footerEl) footerEl.removeAttribute('aria-hidden'); - - // Restore focus to the element that had it before the overlay opened - if (_preOverlayFocusElement && typeof _preOverlayFocusElement.focus === 'function') { - _preOverlayFocusElement.focus(); - } else { - msgInput.focus(); - } - _preOverlayFocusElement = null; + dismissOverlay(); } }); + // Escape key closes crisis overlay (only after dismiss button is enabled) + document.addEventListener('keydown', function(e) { + if (e.key !== 'Escape') return; + if (!crisisOverlay.classList.contains('active')) return; + if (overlayDismissBtn.disabled) return; // Don't bypass countdown + e.preventDefault(); + dismissOverlay(); + }); + // ===== MESSAGE RENDERING ===== function addMessage(role, text, skipSave) { var div = document.createElement('div');