Skip to content

feat(app): announce scan phase changes to screen readers politely - #176

Merged
Muawiya-contact merged 2 commits into
Coding-Moves:mainfrom
LouisDeconinck:feat/issue-156-sr-announcements
Sep 15, 2026
Merged

Muawiya-contact merged 2 commits into
Coding-Moves:mainfrom
LouisDeconinck:feat/issue-156-sr-announcements

Conversation

@LouisDeconinck

Copy link
Copy Markdown
Collaborator

What & why

Closes #156.

The scan panel re-renders its file and byte counters on every scan-progress event (~150ms). Any live region wrapping that output would chatter constantly, so screen reader users currently get either nothing or noise.

This adds a visually-hidden aria-live="polite" region to ScanningIndicator carrying only the phase text ({phase || "Walking files"}). Because its text node only changes on a phase transition — walking files → impact graph → classifying → duplicates → findings — each stage is announced once and the count ticks stay silent. aria-atomic="true" makes the whole phrase announce rather than a diff.

One deliberate addition beyond the issue's snippet: while cancelling, the region reads the same "Stopping the scan… nothing has been changed" copy as the visible status line, so pressing Cancel announces the state change instead of going quiet until the report/cancelled notice appears.

The new .sr-only helper is the classic 1px clip pattern — off screen, but still in the accessibility tree (display:none would silence it).

Checklist

  • cargo fmt --all and cargo clippy --workspace are clean — no Rust touched
  • cargo test --workspace passes — no Rust touched; cd app && npm test (75 tests) and npm run build pass
  • Commits are small and focused (one logical change each)
  • Doesn't weaken a safety principle (read-only scans, quarantine over deletion, deterministic verdicts) — UI text only
  • User-visible changes have a unique fragment in changelog.d/156-sr-announcements.added.md
  • PR dependencies and overlapping component edits are identified — none; touches ScanningIndicator.jsx which no open PR modifies
  • Applicable CI passes; use the merge queue once it is enabled

The scan panel re-renders its file and byte counters on every progress
event (~150ms), so any live region wrapping them would chatter
constantly. The phase label only changes a handful of times per scan —
walking files, impact graph, classifying, duplicates, findings — so a
visually-hidden aria-live=polite region carrying just that text
announces each transition and stays quiet between them.

Cancelling also swaps the announcement to the stopping copy so the
state change is heard, matching the visible status text.

Adds the shared .sr-only clip helper — keeps content in the
accessibility tree but off screen — and string-match tests asserting
the live region exists, carries the phase rather than the counters,
and announces the cancelling state.
@Muawiya-contact

Copy link
Copy Markdown
Member

@LouisDeconinck have to read the discussion? I leave a message for you.
#149

@Muawiya-contact Muawiya-contact left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for contributing again, Louis! Keeping counters out of the announcements is a good improvement, and the focused tests and current CI pass. Please fix the live-region lifecycle described inline so the first scan phase is announced reliably, with a check for the initial scan and preview transition.

once more when a cancel registers — so a live region
carrying just it announces each transition while the
150ms file/byte ticks above stay silent. */}
<p className="sr-only" aria-live="polite" aria-atomic="true">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Mount an empty live region before adding phase text. App.jsx mounts ScanningIndicator only after scanning starts, so this region appears already containing "Walking files". Polite regions generally announce subsequent content changes, not initial populated insertion (https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Guides/Live_regions); users can therefore miss the start and long walking phase. The first preview also replaces this component via the conditional report branches. Please keep one initially empty live region mounted outside those branches and update its text when scanning/phase changes, while continuing to exclude counters.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch — thank you! Fixed in 94914e0: the region moved out of ScanningIndicator into App.jsx, where one initially-empty aria-live="polite" region mounts inside <main> ahead of the !displayedReport / displayedReport branches. It now tracks liveProgress.phase plus the cancelling state, so the first phase announces as a content change and the region survives the preview swap — still no counters in it.

The region inside ScanningIndicator mounted already populated, and
polite regions announce content changes rather than initial content —
and the first preview swaps the whole panel between the report
branches, so the first phase (and the long walking phase) could pass
unannounced. The region now lives in App.jsx, mounted empty inside
<main> ahead of the conditional branches, and follows liveProgress.phase
plus the cancelling state; the 150ms counters stay out of it.
@LouisDeconinck

Copy link
Copy Markdown
Collaborator Author

Fixed in 94914e0.

The live region moved out of ScanningIndicator into App.jsx: one aria-live="polite" region now mounts inside <main>, empty, ahead of the !displayedReport / displayedReport branches. It tracks liveProgress.phase (defaulting to "Walking files") plus the cancelling state, so the initial mount → phase transitions → preview swap → scan end all announce correctly, while the ~150ms file/byte counters stay out of the announced text. ScanningIndicator no longer carries its own region.

Tests updated to match the new ownership: App.test.mjs now asserts the region mounts before/outside the report branches, is empty while idle, carries the phase (not the counters), and announces cancelling; the ScanningIndicator test asserts the panel owns no aria-live region. All 77 app tests pass (node ../scripts/test-app.mjs).

@Muawiya-contact Muawiya-contact left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, Louis—the persistent, initially empty live region now addresses the start-of-scan and preview lifecycle concern while keeping counter updates silent. I rechecked the code, all 12 app test files passed locally, and current CI is green. Approved!

@Muawiya-contact
Muawiya-contact merged commit fe68b7b into Coding-Moves:main Sep 15, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add polite screen reader announcements for scan phases

2 participants