diff --git a/index.html b/index.html
index d049580..06cff1c 100644
--- a/index.html
+++ b/index.html
@@ -983,12 +983,60 @@ Sovereignty and service always.`;
// ===== OVERLAY =====
+
+ // Focus trap: cycle through focusable elements within the crisis overlay
+ function getOverlayFocusableElements() {
+ return crisisOverlay.querySelectorAll(
+ 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])'
+ );
+ }
+
+ function trapFocusInOverlay(e) {
+ if (!crisisOverlay.classList.contains('active')) return;
+ if (e.key !== 'Tab') return;
+
+ var focusable = getOverlayFocusableElements();
+ if (focusable.length === 0) return;
+
+ var first = focusable[0];
+ var last = focusable[focusable.length - 1];
+
+ if (e.shiftKey) {
+ // Shift+Tab: if on first, wrap to last
+ if (document.activeElement === first) {
+ e.preventDefault();
+ last.focus();
+ }
+ } else {
+ // Tab: if on last, wrap to first
+ if (document.activeElement === last) {
+ e.preventDefault();
+ first.focus();
+ }
+ }
+ }
+
+ // Store the element that had focus before the overlay opened
+ var _preOverlayFocusElement = null;
+
function showOverlay() {
+ // Save current focus for restoration on dismiss
+ _preOverlayFocusElement = document.activeElement;
+
crisisOverlay.classList.add('active');
overlayDismissBtn.disabled = true;
var countdown = 10;
overlayDismissBtn.textContent = 'Continue to chat (' + countdown + 's)';
+ // Disable background interaction via inert attribute
+ var mainApp = document.querySelector('.app');
+ if (mainApp) mainApp.setAttribute('inert', '');
+ // Also hide from assistive tech
+ var chatSection = document.getElementById('chat');
+ if (chatSection) chatSection.setAttribute('aria-hidden', 'true');
+ var footerEl = document.querySelector('footer');
+ if (footerEl) footerEl.setAttribute('aria-hidden', 'true');
+
if (overlayTimer) clearInterval(overlayTimer);
overlayTimer = setInterval(function() {
countdown--;
@@ -1005,6 +1053,9 @@ Sovereignty and service always.`;
overlayDismissBtn.focus();
}
+ // Register focus trap on document (always listening, gated by class check)
+ document.addEventListener('keydown', trapFocusInOverlay);
+
overlayDismissBtn.addEventListener('click', function() {
if (!overlayDismissBtn.disabled) {
crisisOverlay.classList.remove('active');
@@ -1012,7 +1063,22 @@ Sovereignty and service always.`;
clearInterval(overlayTimer);
overlayTimer = null;
}
- msgInput.focus();
+
+ // 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;
}
});
diff --git a/tests/test_crisis_overlay_focus_trap.py b/tests/test_crisis_overlay_focus_trap.py
new file mode 100644
index 0000000..f657afc
--- /dev/null
+++ b/tests/test_crisis_overlay_focus_trap.py
@@ -0,0 +1,57 @@
+import pathlib
+import re
+import unittest
+
+ROOT = pathlib.Path(__file__).resolve().parents[1]
+INDEX_HTML = ROOT / 'index.html'
+
+
+class TestCrisisOverlayFocusTrap(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.html = INDEX_HTML.read_text()
+
+ def test_overlay_registers_tab_key_focus_trap(self):
+ self.assertRegex(
+ self.html,
+ r"function\s+trapFocusInOverlay\s*\(e\)",
+ 'Expected crisis overlay focus trap handler to exist.',
+ )
+ self.assertRegex(
+ self.html,
+ r"if\s*\(e\.key\s*!==\s*'Tab'\)\s*return;",
+ 'Expected focus trap handler to guard on Tab key events.',
+ )
+ self.assertRegex(
+ self.html,
+ r"document\.addEventListener\('keydown',\s*trapFocusInOverlay\)",
+ 'Expected overlay focus trap to register on document keydown.',
+ )
+
+ def test_overlay_disables_background_interaction(self):
+ self.assertRegex(
+ self.html,
+ r"mainApp\.setAttribute\('inert',\s*''\)",
+ 'Expected overlay to set inert on the main app while active.',
+ )
+ self.assertRegex(
+ self.html,
+ r"mainApp\.removeAttribute\('inert'\)",
+ 'Expected overlay dismissal to remove inert from the main app.',
+ )
+
+ def test_overlay_restores_focus_after_dismiss(self):
+ self.assertRegex(
+ self.html,
+ r"_preOverlayFocusElement\s*=\s*document\.activeElement",
+ 'Expected overlay to remember the pre-overlay focus target.',
+ )
+ self.assertRegex(
+ self.html,
+ r"_preOverlayFocusElement\.focus\(\)",
+ 'Expected overlay dismissal to restore focus to the prior target.',
+ )
+
+
+if __name__ == '__main__':
+ unittest.main()