Skip to content

fix: accessibility, motion, and dark-mode QA pass across loading states (MON-217) - #304

Open
Walid-peach wants to merge 1 commit into
masterfrom
feature/mon-217-accessibility-motion-and-dark-mode-qa-pass-across-loading
Open

fix: accessibility, motion, and dark-mode QA pass across loading states (MON-217)#304
Walid-peach wants to merge 1 commit into
masterfrom
feature/mon-217-accessibility-motion-and-dark-mode-qa-pass-across-loading

Conversation

@Walid-peach

Copy link
Copy Markdown
Owner

What

The final sub-issue in the MON-207 epic: a cross-cutting QA pass over everything MON-214/215/216 shipped (reduced motion, dark-mode contrast, keyboard access, screen-reader cycle, cached/throttled network, mobile viewport), fixing one real gap it found rather than filing it onward.

Why

MON-214/215/216 each shipped with their own unit tests and manual verification, but nothing had checked the system end to end - across viewports, themes, motion preference, and input modality at once. This issue is that check.

Changes

  • frontend/src/components/ui/AsyncStatus.tsx: new opt-in autoFocusAction prop that focuses the cancel/retry button on mount.
  • frontend/src/app/chat/page.tsx: wired autoFocusAction on both the typing-state and error-state AsyncStatus instances.
  • frontend/__tests__/components/AsyncStatus.test.tsx: three new tests for the prop (focuses cancel, focuses retry, does nothing when unset).

QA pass findings

All checks below were run against the live production API (not mocks) via a scripted Playwright pass, plus the existing unit suites.

Bug found and fixed:

  • Keyboard access to cancel/retry was broken. In /chat, AsyncStatus's Annuler/Réessayer button renders inside the message list, which sits above the input bar in the DOM (messages accumulate above a fixed-position textarea). A keyboard user who just pressed Enter to submit stays focused in the textarea - and forward-Tab from there skips straight past the newly-appeared cancel button to the page's disclaimer link, never reaching it. Confirmed with a scripted Tab-order walk (landed on assemblee-nationale.fr, not Annuler) and confirmed the button was only reachable via 3x Shift+Tab - not a discoverable path. Fixed with autoFocusAction: focus now moves to Annuler/Réessayer the instant it mounts, verified live (focus lands there immediately, Enter activates it, no Tab needed).

Verified clean, no gaps:

  • Reduced motion: .dp-skeleton-block's shimmer computed animation-duration: 1e-06s (0.001ms) under prefers-reduced-motion: reduce - the existing MON-194 global CSS rule catches it automatically, no component-level change needed. AsyncStatus's activity dot already omits animate-pulse under reduced motion (tested since MON-214).
  • Dark mode: /votes, /deputes, and /chat all render correctly in dark mode - verified visually (screenshots), tokens are the existing contrast-vetted var(--dp-*) set already used elsewhere in the app.
  • Screen-reader question-answer cycle: confirmed on both chat search and claim verification - role="status" + aria-busy="true" while in flight, then the sr-only aria-live="polite" region announces the completed answer/verdict once it lands (e.g. "Nouvelle réponse : Positions de vote par groupe parlementaire : ...", "Vérification terminée : trompeur").
  • API failure → retry: confirmed live with a mocked 500 response - retry button appears, is keyboard-focusable (now auto-focused), and resubmits correctly.
  • Cached / throttled network: /votes' client-side filter refetch under 900ms CDP-throttled latency settles cleanly (confirmed with a patient poll after an initial false-negative in a too-eager first check - the request completes and the skeleton clears within ~1.5s, matching the timing policy). Cached ISR navigation to /votes shows no loader per MON-215's existing verification.
  • Mobile viewport (390×844): /votes skeleton and /chat loading state both render with no horizontal overflow.

Not independently re-verified in this pass (already covered by MON-214/215/216's own test suites and manual checks, not re-litigated here): exact 100ms/500ms/1.5s/8s timing-phase boundaries - frontend/__tests__/lib/loadingPolicy.test.ts already asserts these deterministically with fake timers, which is a more precise instrument than a live wall-clock screenshot race would be.

Testing

  • npm test - full suite, 232/232 passing.
  • npm run lint / npm run build - clean.
  • Manual verification as described above (Playwright against the live production API): reduced motion, keyboard focus order (before and after the fix), dark mode, screen-reader-relevant ARIA state, API failure/retry, throttled refetch, mobile viewport.

Risks / Notes

  • No DB/schema/deploy-config changes. AsyncStatus's new prop is optional and defaults to off, so no existing usage changes behavior.
  • This closes out the MON-207 epic (MON-214, MON-215, MON-216 already merged).

Breaking Changes

None.

…ON-217)

QA pass across MON-207's loading-state system found one real
accessibility gap: in /chat, the Annuler/Reessayer button renders
above the textarea in the DOM (messages first, input fixed below), so
a keyboard user who just submitted and stays focused in the textarea
can never reach it via forward Tab - only Shift+Tab, which is not a
discoverable path. Confirmed via Playwright (Tab order landed on the
page's disclaimer link, never on Annuler; Shift+Tab x3 reached it).

Adds an opt-in `autoFocusAction` prop to AsyncStatus that focuses the
cancel/retry button on mount, wired on for both of /chat's typing and
error states. Verified live: focus now lands directly on Annuler the
instant loading starts, and on Reessayer the instant a request fails.

Rest of the epic-wide QA pass (reduced motion, dark-mode contrast,
throttled network settling, mobile viewport, screen-reader
question-answer cycle on both search and verify) verified clean with
no further gaps - see PR description for the full pass writeup.
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mon-elu Ready Ready Preview Aug 3, 2026 11:27pm

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

🧪 dbt data-health check — ✅ passed

23:26:09  52 of 62 PASS source_not_null_raw_votes_vote_id ................................ [PASS in 0.88s]
23:26:09  56 of 62 START test source_unique_raw_deputies_deputy_id ....................... [RUN]
23:26:09  53 of 62 PASS source_not_null_raw_votes_voted_at ............................... [PASS in 0.90s]
23:26:09  57 of 62 START test source_unique_raw_votes_vote_id ............................ [RUN]
23:26:10  54 of 62 PASS source_relationships_raw_vote_positions_deputy_id__deputy_id__source_raw_deputies_  [PASS in 1.01s]
23:26:10  58 of 62 START test unique_mart_deputy_scorecard_deputy_id ..................... [RUN]
23:26:10  55 of 62 PASS source_relationships_raw_vote_positions_vote_id__vote_id__source_raw_votes_  [PASS in 1.11s]
23:26:10  59 of 62 START test unique_mart_party_alignment_deputy_id ...................... [RUN]
23:26:10  56 of 62 PASS source_unique_raw_deputies_deputy_id ............................. [PASS in 0.92s]
23:26:10  60 of 62 START test unique_mart_vote_summary_vote_id ........................... [RUN]
23:26:10  57 of 62 PASS source_unique_raw_votes_vote_id .................................. [PASS in 0.87s]
23:26:10  61 of 62 START test unique_stg_deputies_deputy_id .............................. [RUN]
23:26:11  58 of 62 PASS unique_mart_deputy_scorecard_deputy_id ........................... [PASS in 0.86s]
23:26:11  62 of 62 START test unique_stg_votes_vote_id ................................... [RUN]
23:26:11  59 of 62 PASS unique_mart_party_alignment_deputy_id ............................ [PASS in 0.89s]
23:26:11  60 of 62 PASS unique_mart_vote_summary_vote_id ................................. [PASS in 0.93s]
23:26:11  61 of 62 PASS unique_stg_deputies_deputy_id .................................... [PASS in 0.89s]
23:26:12  62 of 62 PASS unique_stg_votes_vote_id ......................................... [PASS in 0.87s]
23:26:12  
23:26:12  Finished running 62 data tests in 0 hours 0 minutes and 20.17 seconds (20.17s).
23:26:12  
23:26:12  Completed successfully
23:26:12  
23:26:12  Done. PASS=62 WARN=0 ERROR=0 SKIP=0 TOTAL=62

Validates prod data health. A failure means current prod data is stale/broken, not necessarily that this PR is wrong.

@Walid-peach Walid-peach left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attention Score

100/100 - SAFE TO SKIM

  • Base 100
    Reason you can skim: no Must/Should Fix findings, tiny diff (29 lines / 3 files, well under the size threshold), no high-risk paths, the new prop is optional/off-by-default so no existing usage changes behavior, new tests directly cover the change, and CI is green. The PR description also documents an extensive live-verification pass beyond what's in this diff.

Summary

This PR closes out the MON-207 epic with a targeted accessibility fix found during the promised cross-cutting QA pass: keyboard users could never reach AsyncStatus's cancel/retry button in /chat via forward-Tab, because it renders above the textarea in DOM order (messages accumulate above a fixed input bar) - only Shift+Tab reached it, which isn't discoverable. The fix adds an opt-in autoFocusAction prop that focuses the button on mount, wired on for both of chat's loading and error states. The PR description's QA writeup (reduced motion, dark-mode screenshots, screen-reader cycle, throttled network, mobile viewport) is a genuinely thorough verification pass for a feature this timing/accessibility-sensitive.

Must Fix

None.

Should Fix

None.

Nice to Have

  • autoFocusAction's doc comment on AsyncStatusProps (frontend/src/components/ui/AsyncStatus.tsx) explains why it's needed for chat's specific DOM ordering but doesn't flag that any future AsyncStatus consumer should think through whether their own DOM ordering has the same problem before blindly copying autoFocusAction on - worth a one-line addition if this component gains more call sites, not urgent now with a single consumer.
  • No test covers that autoFocusAction refocuses correctly across successive submissions (i.e., that a second typing message in the same conversation also receives focus, not just the first). Given autoFocus is a mount-time-only DOM behavior and each new typing/error message is a genuinely new React element (verified in the MON-216 PR's own reasoning about append-only message arrays), this should already work, but an explicit test would make that guarantee visible rather than inferred.

Testing / Validation Gaps

None. Three new unit tests cover the prop directly (focuses cancel, focuses retry, no-op when unset), and the PR description documents live verification of the actual fix (focus lands on Annuler/Réessayer immediately, Enter activates without any Tab needed) plus the rest of the epic-wide QA pass.

Documentation / Reviewer Notes

The PR body's QA section is worth reading in full even though the diff is small - it's the actual deliverable for this issue (a verification pass, not new component work) and documents real findings (e.g. the reduced-motion unit-conversion non-issue, the throttled-network false-negative caught and re-verified) rather than just asserting "verified." No further docs needed.

Verdict

Ready to merge

@Walid-peach Walid-peach added the safe-to-skim PR review score above threshold - agent review likely sufficient label Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

safe-to-skim PR review score above threshold - agent review likely sufficient

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant