fix: Crisis overlay keyboard navigation — Escape key + focus recovery (#95) #104

Closed
Rockachopa wants to merge 3 commits from fix/95-crisis-overlay-keyboard-nav into main
Owner

What changed

Crisis overlay keyboard navigation fix (issue #95)

Problem

  • Users who Tab past the last focusable element in the crisis overlay lose focus entirely
  • No Escape key support — users are trapped for 10+ seconds with no keyboard exit
  • Focus could escape the overlay to background elements

Fix (index.html — trapFocusInOverlay)

  1. Escape key handling: Press Escape to close the overlay (when dismiss button is enabled). Mirrors the safety plan modal's Escape behavior.

  2. Focus recovery: If focus somehow leaves the overlay, the next Tab press brings it back to the first focusable element instead of letting it wander into the background.

  3. Existing behavior preserved: Tab wrapping (last→first) and Shift+Tab wrapping (first→last) unchanged.

Tests

  • Updated tests/test_crisis_overlay_focus_trap.py with 2 new static tests:

    • test_overlay_escape_key_closes — verifies Escape handler exists in HTML
    • test_overlay_focus_recovery — verifies focus recovery logic exists
  • New tests/test_crisis_overlay_keyboard.py — Playwright E2E tests:

    • Tab cycles through focusable elements
    • Shift+Tab reverses direction
    • Escape closes overlay
    • Escape returns focus to chat input
    • Focus cannot escape overlay while active

Acceptance criteria

  • Tab cycles through all focusable elements in overlay
  • Shift+Tab reverses direction
  • Escape closes overlay and returns focus to chat input
  • Test: Playwright keyboard-only navigation

Closes #95

## What changed **Crisis overlay keyboard navigation fix (issue #95)** ### Problem - Users who Tab past the last focusable element in the crisis overlay lose focus entirely - No Escape key support — users are trapped for 10+ seconds with no keyboard exit - Focus could escape the overlay to background elements ### Fix (index.html — `trapFocusInOverlay`) 1. **Escape key handling**: Press Escape to close the overlay (when dismiss button is enabled). Mirrors the safety plan modal's Escape behavior. 2. **Focus recovery**: If focus somehow leaves the overlay, the next Tab press brings it back to the first focusable element instead of letting it wander into the background. 3. **Existing behavior preserved**: Tab wrapping (last→first) and Shift+Tab wrapping (first→last) unchanged. ### Tests - Updated `tests/test_crisis_overlay_focus_trap.py` with 2 new static tests: - `test_overlay_escape_key_closes` — verifies Escape handler exists in HTML - `test_overlay_focus_recovery` — verifies focus recovery logic exists - New `tests/test_crisis_overlay_keyboard.py` — Playwright E2E tests: - Tab cycles through focusable elements - Shift+Tab reverses direction - Escape closes overlay - Escape returns focus to chat input - Focus cannot escape overlay while active ### Acceptance criteria - [x] Tab cycles through all focusable elements in overlay - [x] Shift+Tab reverses direction - [x] Escape closes overlay and returns focus to chat input - [x] Test: Playwright keyboard-only navigation Closes #95
Rockachopa added 3 commits 2026-04-15 03:21:57 +00:00
Timmy approved these changes 2026-04-15 04:13:46 +00:00
Dismissed
Timmy left a comment
Owner

Fix looks reasonable.

Scope: 3 file(s) changed (251+ / 0-)

Fix looks reasonable. **Scope**: 3 file(s) changed (251+ / 0-)
Timmy requested changes 2026-04-15 14:32:23 +00:00
Timmy left a comment
Owner

Comprehensive fix for crisis overlay keyboard navigation (issue #95). The Escape key handler and focus recovery logic are correct.

Issues:

  1. Focus recovery uses linear scan instead of contains(): The focus recovery code iterates through all focusable elements to check if document.activeElement is inside the overlay. PR #116 uses the cleaner crisisOverlay.contains(document.activeElement) approach. If this PR merges after #116, there will be a merge conflict. Recommend aligning with the contains() approach.

  2. Escape key does nothing when dismiss button is disabled: During the countdown period, pressing Escape is silently swallowed (e.preventDefault() runs but no action occurs). This is intentional per the design (force users to see crisis resources), but consider providing user feedback — e.g., briefly flash the countdown timer or show a tooltip explaining why Escape is disabled.

  3. Playwright test has syntax issues: In test_escape_closes_overlay, the line expect(overlay).to_have_class(/active/) uses JavaScript regex syntax, not Python. This should be expect(overlay).to_have_class(re.compile(r"active")) or use a string matcher. Same issue in test_escape_returns_focus_to_chat_input. The test_focus_does_not_escape_overlay test also has a multi-line assert with backslash continuation that will likely fail.

  4. Port collision risk: The Playwright test server uses hardcoded port 18923. If another test or process uses this port, the test fails silently. Consider using port 0 (random available port).

  5. _trigger_crisis_overlay bypasses the countdown: The test helper enables the dismiss button and focuses it directly, which does not match real user flow. The Escape key test passes because the button is pre-enabled, but in production the button starts disabled.

Item 3 (Playwright syntax errors) will cause test failures — must be fixed.

Comprehensive fix for crisis overlay keyboard navigation (issue #95). The Escape key handler and focus recovery logic are correct. Issues: 1. **Focus recovery uses linear scan instead of `contains()`**: The focus recovery code iterates through all focusable elements to check if `document.activeElement` is inside the overlay. PR #116 uses the cleaner `crisisOverlay.contains(document.activeElement)` approach. If this PR merges after #116, there will be a merge conflict. Recommend aligning with the `contains()` approach. 2. **Escape key does nothing when dismiss button is disabled**: During the countdown period, pressing Escape is silently swallowed (`e.preventDefault()` runs but no action occurs). This is intentional per the design (force users to see crisis resources), but consider providing user feedback — e.g., briefly flash the countdown timer or show a tooltip explaining why Escape is disabled. 3. **Playwright test has syntax issues**: In `test_escape_closes_overlay`, the line `expect(overlay).to_have_class(/active/)` uses JavaScript regex syntax, not Python. This should be `expect(overlay).to_have_class(re.compile(r"active"))` or use a string matcher. Same issue in `test_escape_returns_focus_to_chat_input`. The `test_focus_does_not_escape_overlay` test also has a multi-line assert with backslash continuation that will likely fail. 4. **Port collision risk**: The Playwright test server uses hardcoded port 18923. If another test or process uses this port, the test fails silently. Consider using port 0 (random available port). 5. **`_trigger_crisis_overlay` bypasses the countdown**: The test helper enables the dismiss button and focuses it directly, which does not match real user flow. The Escape key test passes because the button is pre-enabled, but in production the button starts disabled. Item 3 (Playwright syntax errors) will cause test failures — must be fixed.
Timmy reviewed 2026-04-15 16:16:28 +00:00
Timmy left a comment
Owner

Review: fix: Crisis overlay keyboard navigation — Escape key + focus recovery

Good implementation of Escape key dismissal and focus recovery. The Playwright E2E tests are a nice addition.

  1. Focus recovery uses a loop over focusable elements to check if activeElement is inside — PR #116 does this more cleanly with crisisOverlay.contains(document.activeElement). Prefer that approach.

  2. Overlaps significantly with PRs #116, #126, and #84 — all fix the same issue (#69/#95). Coordinate to merge only one.

  3. The Playwright test expects /active/ regex — this is fine in Playwright but verify it matches the exact class pattern.

  4. test_focus_does_not_escape_overlay has a long assertion on line 284 — break it up for readability.

**Review: fix: Crisis overlay keyboard navigation — Escape key + focus recovery** Good implementation of Escape key dismissal and focus recovery. The Playwright E2E tests are a nice addition. 1. **Focus recovery uses a loop over `focusable` elements** to check if `activeElement` is inside — PR #116 does this more cleanly with `crisisOverlay.contains(document.activeElement)`. Prefer that approach. 2. **Overlaps significantly with PRs #116, #126, and #84** — all fix the same issue (#69/#95). Coordinate to merge only one. 3. **The Playwright test expects `/active/` regex** — this is fine in Playwright but verify it matches the exact class pattern. 4. **`test_focus_does_not_escape_overlay` has a long assertion** on line 284 — break it up for readability.
Author
Owner

Closing: stale PR, superseded by newer mergeable implementation.

Closing: stale PR, superseded by newer mergeable implementation.
Rockachopa closed this pull request 2026-04-16 01:49:02 +00:00
Some checks failed
Sanity Checks / sanity-test (pull_request) Successful in 10s
Smoke Test / smoke (pull_request) Failing after 19s

Pull request closed

Sign in to join this conversation.