[P3] role="alert" warnings include the live ceiling value, so screen readers re-announce them every time the 0.1-unit value changes #114

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

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

Location: src/App.tsx:1499-1504, src/App.tsx:1581-1611

Problem and evidence

Two role="alert" regions contain depthText(highCeiling, 1), the live ceiling rounded to 0.1 ft/m. role="alert" is implicitly aria-live="assertive" and aria-atomic="true":

  • .manual-ascent-warning: "...shallower than the gf-high ceiling of X ft..."
  • .critical-alerts: the ceiling line "...of X ft" and the surfaced line "...ceiling remains X ft". It also contains the "Sound becomes available..." paragraph.

highCeiling is recomputed from the tissue state on every simulation tick. Each time the rounded value changes, the alert's text node changes. Assistive tech then interrupts and reads the whole alert again, for as long as the condition lasts. The two cited regions are the only alerts that contain the live number. The VPM-B metric and the stops panel are not alerts.

I ran Playwright against the HEAD production build in /tmp/divesim-review-dist-SqbiNx, served on a private port.

  1. Dismissed the welcome dialog, set playback to 128x and depth to 131 ft, and started the dive.
  2. Clicked "Override · resume" when it appeared. "Modeled decompression required" showed after about 5 s.
  3. Installed a MutationObserver that records each distinct textContent of every [role=alert].
  4. Switched to 8x and clicked the "Surface" preset.

Results for .manual-ascent-warning (role=alert):

  • 8 distinct texts in 5 s: "...gf-high ceiling of 17.0 ft", "17.1 ft", "17.2 ft", "17.3 ft", ...
  • 43 distinct texts in a 25 s window, so roughly 1.7 to 2 assertive re-announcements per second.

In my runs the .critical-alerts panel did not appear during the observation window. Its ceiling and surfaced paragraphs use the same depthText(highCeiling, 1) interpolation inside the same kind of atomic alert, so the mechanism is identical by code trace. The original reviewer also observed it churning (17 mutations in 5 s).

Suggested correction

Keep the text inside role="alert" static while the condition holds, for example "Ascent target is above the current ceiling" or "Above the decompression ceiling". Render the live ceiling number in a sibling element outside the alert: a plain paragraph, or at most a polite/off live region updated only on coarse changes. Alternatively, mark the number span aria-hidden and put a fixed-precision snapshot taken when the alert starts into the announced text. Also move the "Sound becomes available..." paragraph out of the critical alert so it is not re-read whenever the alert updates.

Acceptance criteria

A Playwright regression test (131 ft at 128x with override until deco is required, then 8x and Surface) uses a MutationObserver on [role=alert] for 10 s or more. It should record at most one text per alert condition: the alert text must not change while only the ceiling value changes. The visible ceiling number must still update in a non-alert element. The same check should cover .critical-alerts in both the ceiling-violation and surfaced-with-ceiling states. pnpm verify passes.

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

Priority: P3 Type: bug Source: full codebase review requested 2026-10-31 (independently reproduced against `2cac7cf`) Location: `src/App.tsx:1499-1504`, `src/App.tsx:1581-1611` Problem and evidence Two `role="alert"` regions contain `depthText(highCeiling, 1)`, the live ceiling rounded to 0.1 ft/m. `role="alert"` is implicitly `aria-live="assertive"` and `aria-atomic="true"`: - `.manual-ascent-warning`: "...shallower than the gf-high ceiling of X ft..." - `.critical-alerts`: the ceiling line "...of X ft" and the surfaced line "...ceiling remains X ft". It also contains the "Sound becomes available..." paragraph. `highCeiling` is recomputed from the tissue state on every simulation tick. Each time the rounded value changes, the alert's text node changes. Assistive tech then interrupts and reads the whole alert again, for as long as the condition lasts. The two cited regions are the only alerts that contain the live number. The VPM-B metric and the stops panel are not alerts. I ran Playwright against the HEAD production build in /tmp/divesim-review-dist-SqbiNx, served on a private port. 1. Dismissed the welcome dialog, set playback to 128x and depth to 131 ft, and started the dive. 2. Clicked "Override · resume" when it appeared. "Modeled decompression required" showed after about 5 s. 3. Installed a MutationObserver that records each distinct textContent of every [role=alert]. 4. Switched to 8x and clicked the "Surface" preset. Results for `.manual-ascent-warning` (role=alert): - 8 distinct texts in 5 s: "...gf-high ceiling of 17.0 ft", "17.1 ft", "17.2 ft", "17.3 ft", ... - 43 distinct texts in a 25 s window, so roughly 1.7 to 2 assertive re-announcements per second. In my runs the `.critical-alerts` panel did not appear during the observation window. Its ceiling and surfaced paragraphs use the same `depthText(highCeiling, 1)` interpolation inside the same kind of atomic alert, so the mechanism is identical by code trace. The original reviewer also observed it churning (17 mutations in 5 s). Suggested correction Keep the text inside role="alert" static while the condition holds, for example "Ascent target is above the current ceiling" or "Above the decompression ceiling". Render the live ceiling number in a sibling element outside the alert: a plain paragraph, or at most a polite/off live region updated only on coarse changes. Alternatively, mark the number span `aria-hidden` and put a fixed-precision snapshot taken when the alert starts into the announced text. Also move the "Sound becomes available..." paragraph out of the critical alert so it is not re-read whenever the alert updates. Acceptance criteria A Playwright regression test (131 ft at 128x with override until deco is required, then 8x and Surface) uses a MutationObserver on [role=alert] for 10 s or more. It should record at most one text per alert condition: the alert text must not change while only the ceiling value changes. The visible ceiling number must still update in a non-alert element. The same check should cover `.critical-alerts` in both the ceiling-violation and surfaced-with-ceiling states. 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#114
No description provided.