[P2] Replay and planner playback push every playback tick through polite live regions, so screen readers keep announcing while replay runs #115

Open
opened 2026-10-31 19:47:41 +00:00 by pedro · 0 comments
Owner

Priority: P2
Type: bug
Source: full codebase review requested 2026-10-31 (independently reproduced against 2cac7cf)

Location: src/replay/DiveReplayComparison.tsx:335, src/replay/DiveReplayComparison.tsx:57-68, src/replay/DiveReplayComparison.tsx:605-650, src/planner/ProfilePlanner.tsx:196-199, src/planner/ProfilePlanner.tsx:846-916, src/planner/ProfilePlanner.tsx:970-973

Problem and evidence

In DiveReplayComparison, the whole .replay-results container has aria-live="polite". Inside it are content that changes with time: the "Replay timeline · m:ss" label, the .summary-overview depth and gas-required lines, and ReplayOverlays, which renders from replayPointAt(..., time). They sit next to the static table. While playing, a setInterval advances time every 50 ms (lines 63-66), so the live region changes at about 20 Hz.

ProfilePlanner has the same problem. .planner-stats has aria-live="polite" and holds depth, ppO2, ceiling, NDL and per-cylinder pressure, remaining gas and GTR for the current position. useSimulationClock advances position on every tick. A separate <p role="status">Replay m:ss / total · speed×</p> also changes on every displayed second.

The result is that screen readers get a continuous stream of polite announcements for the whole replay. That buries other speech and makes the playback controls hard to use with assistive technology.

I served the HEAD production build (/tmp/divesim-review-dist-SqbiNx) and ran Playwright. I dismissed the welcome dialog with Escape, clicked Planner Mode, then clicked Replay profile with the default plan. I attached a MutationObserver (childList, characterData, subtree) to every [aria-live] and [role=status] element. Over 5 s it recorded 187 mutations in .planner-stats and 35 in the role=status replay readout, with 70 distinct texts. Examples: "At 0:03 · Depth 3.8 ft · Gas Air · ppO₂ 3.44 psi · GF-high c…", "Replay 0:03 / 21:00 · 8×", "At 0:05 · Depth 5.5 ft …". This matches the claim's 194/33 figures.

For the replay comparison I traced the code instead of re-running the browser. Line 335 wraps the timeline label (line 621 Replay timeline · {formatTime(time)}), the summary overview (at(results[name]) → depth and volume) and <ReplayOverlays time={time}> in aria-live="polite". The setInterval(..., 50) at lines 63-66 updates time while playing, so every one of these nodes re-renders at 20 Hz. The earlier reviewer measured 521 mutations in 5 s there, which is consistent with this trace.

Suggested correction

  • Remove aria-live from .replay-results and .planner-stats.
  • Announce only discrete events through a small dedicated status element. Examples: "Comparison calculated", the existing "Settings changed" notice (move it out of the container but keep its own role=status), and "Replay paused/finished".
  • Make the planner "Replay m:ss / total" readout non-live: use plain text, or role="timer", which implies aria-live=off.
  • Expose the current position through the range input's aria-valuetext (for example "3:05 of 21:00") so users can get it on demand.

Acceptance criteria

  • During playback in both the replay comparison and Planner Mode, no element with aria-live polite/assertive or role=status/alert changes more than once per discrete event. A Playwright MutationObserver over all live regions should record 0 text mutations over 5 s of 8× playback, apart from a single start/pause/finish announcement.
  • The "Settings changed" notice and the calculation results are still announced once.
  • The timeline sliders expose the current position through aria-valuetext.
  • Add a regression test (component test or Playwright) that plays each replay and asserts that live regions do not change per tick. pnpm verify passes.

Scope: maintenance of the educational simulator. Preserve historical R1 acceptance and operational-validation limits.

Priority: P2 Type: bug Source: full codebase review requested 2026-10-31 (independently reproduced against `2cac7cf`) Location: `src/replay/DiveReplayComparison.tsx:335`, `src/replay/DiveReplayComparison.tsx:57-68`, `src/replay/DiveReplayComparison.tsx:605-650`, `src/planner/ProfilePlanner.tsx:196-199`, `src/planner/ProfilePlanner.tsx:846-916`, `src/planner/ProfilePlanner.tsx:970-973` Problem and evidence In DiveReplayComparison, the whole `.replay-results` container has `aria-live="polite"`. Inside it are content that changes with time: the "Replay timeline · m:ss" label, the `.summary-overview` depth and gas-required lines, and ReplayOverlays, which renders from `replayPointAt(..., time)`. They sit next to the static table. While playing, a `setInterval` advances `time` every 50 ms (lines 63-66), so the live region changes at about 20 Hz. ProfilePlanner has the same problem. `.planner-stats` has `aria-live="polite"` and holds depth, ppO2, ceiling, NDL and per-cylinder pressure, remaining gas and GTR for the current `position`. `useSimulationClock` advances `position` on every tick. A separate `<p role="status">Replay m:ss / total · speed×</p>` also changes on every displayed second. The result is that screen readers get a continuous stream of polite announcements for the whole replay. That buries other speech and makes the playback controls hard to use with assistive technology. I served the HEAD production build (/tmp/divesim-review-dist-SqbiNx) and ran Playwright. I dismissed the welcome dialog with Escape, clicked Planner Mode, then clicked Replay profile with the default plan. I attached a MutationObserver (childList, characterData, subtree) to every `[aria-live]` and `[role=status]` element. Over 5 s it recorded 187 mutations in `.planner-stats` and 35 in the role=status replay readout, with 70 distinct texts. Examples: "At 0:03 · Depth 3.8 ft · Gas Air · ppO₂ 3.44 psi · GF-high c…", "Replay 0:03 / 21:00 · 8×", "At 0:05 · Depth 5.5 ft …". This matches the claim's 194/33 figures. For the replay comparison I traced the code instead of re-running the browser. Line 335 wraps the timeline label (line 621 `Replay timeline · {formatTime(time)}`), the summary overview (`at(results[name])` → depth and volume) and `<ReplayOverlays time={time}>` in `aria-live="polite"`. The `setInterval(..., 50)` at lines 63-66 updates `time` while playing, so every one of these nodes re-renders at 20 Hz. The earlier reviewer measured 521 mutations in 5 s there, which is consistent with this trace. Suggested correction - Remove `aria-live` from `.replay-results` and `.planner-stats`. - Announce only discrete events through a small dedicated status element. Examples: "Comparison calculated", the existing "Settings changed" notice (move it out of the container but keep its own role=status), and "Replay paused/finished". - Make the planner "Replay m:ss / total" readout non-live: use plain text, or `role="timer"`, which implies aria-live=off. - Expose the current position through the range input's `aria-valuetext` (for example "3:05 of 21:00") so users can get it on demand. Acceptance criteria - During playback in both the replay comparison and Planner Mode, no element with aria-live polite/assertive or role=status/alert changes more than once per discrete event. A Playwright MutationObserver over all live regions should record 0 text mutations over 5 s of 8× playback, apart from a single start/pause/finish announcement. - The "Settings changed" notice and the calculation results are still announced once. - The timeline sliders expose the current position through aria-valuetext. - Add a regression test (component test or Playwright) that plays each replay and asserts that live regions do not change per tick. pnpm verify passes. Scope: maintenance of the educational simulator. Preserve historical R1 acceptance and operational-validation limits.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: pedro/DiveSim#115
No description provided.