Compare commits

..

1 Commits

Author SHA1 Message Date
cb8554e904 feat: crisis overlay full Tab cycle + Escape dismiss (#95)
All checks were successful
Sanity Checks / sanity-test (pull_request) Successful in 8s
Smoke Test / smoke (pull_request) Successful in 18s
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
2026-04-15 03:22:31 +00:00
2 changed files with 47 additions and 47 deletions

View File

@@ -680,7 +680,7 @@ html, body {
<!-- Footer -->
<footer id="footer">
<a href="/about.html" aria-label="About The Door">about</a>
<a href="/about" aria-label="About The Door">about</a>
<button id="safety-plan-btn" aria-label="Open My Safety Plan">my safety plan</button>
<button id="clear-chat-btn" aria-label="Clear chat history">clear chat</button>
</footer>
@@ -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');

View File

@@ -1,24 +0,0 @@
import pathlib
import unittest
ROOT = pathlib.Path(__file__).resolve().parents[1]
INDEX_HTML = ROOT / 'index.html'
ABOUT_HTML = ROOT / 'about.html'
class TestAboutLink(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.html = INDEX_HTML.read_text(encoding='utf-8')
def test_about_page_exists(self):
self.assertTrue(ABOUT_HTML.exists(), 'about.html should exist for static serving')
def test_footer_about_link_targets_static_about_html(self):
self.assertIn('href="/about.html"', self.html)
self.assertNotIn('href="/about"', self.html)
if __name__ == '__main__':
unittest.main()