Skip to content

fix(announcer): keep aria live-region label until next announcement (fixes #31)#33

Merged
chiefcll merged 1 commit into
mainfrom
fix/announcer-aria-live-region-teardown
Jun 18, 2026
Merged

fix(announcer): keep aria live-region label until next announcement (fixes #31)#33
chiefcll merged 1 commit into
mainfrom
fix/announcer-aria-live-region-teardown

Conversation

@chiefcll

Copy link
Copy Markdown
Contributor

Fixes #31.

Problem

In aria mode (Announcer.aria = true), focusElementForAria() injected the focus label into the #aria-parent aria-live="assertive" region, then a hard-coded setTimeout(…, 100) deleted those nodes and moved focus back to the canvas. On webOS/Samsung TV screen readers (Audio Guidance), that teardown fires before the reader has finished — often before it has started — so nothing is announced, even though the DOM mutates exactly as expected. The cancel() path did the same immediate clear + focusCanvas().

Fix — proposed option #1 (replace-on-next-write)

The live region now holds the current label until the next announcement replaces it, instead of being torn down on a timer:

  • focusElementForAria() — removed the 100ms teardown timer. The region is cleared (cleanAriaLabelParent()) immediately before the new spans are injected, so it always contains exactly the current label and the reader is never interrupted. Added an early return when there are no phrases to write, so a canceled/empty series can't blank out a label the reader is still speaking.
  • cancel() (aria path) — no longer clears the DOM region or moves focus. It drops any partially-accumulated phrases and falls through to actually stop the series (active = false, cancel nested) instead of returning early.
  • Removed the now-unused focusCanvas() helper — no focus is moved at all anymore, which also avoids focus changes interrupting the reader.

Tests

Added tests/announcer-aria.spec.ts covering:

  • label is injected into the assertive live region;
  • label survives well past the old 100ms window;
  • the next announcement replaces the prior label (region holds only one);
  • a canceled follow-up series does not wipe the region.

All 4 pass; tsc, ESLint, and Prettier are clean. (The pre-existing flex.performance.spec.ts timing benchmark can flake under load and is unrelated to this change.)

Reviewer note

This slightly changes cancel() semantics in aria mode: the series now actually halts (active = false / cancels nested) instead of returning early. That is more correct, but flagging in case anything relied on the old early-return behavior.

🤖 Generated with Claude Code

In aria mode, focusElementForAria() injected the label into the
assertive live region, then a hard-coded 100ms setTimeout deleted the
nodes and moved focus back to the canvas. On webOS/Samsung TV screen
readers that teardown fired before the reader finished (often before it
started) speaking, so nothing was announced. The cancel() path did the
same immediate clear + focusCanvas().

Implements proposed fix #1 (replace-on-next-write) from issue #31:

- Remove the 100ms teardown timer. The label is written and left in
  place; the region is cleared only when the next announcement replaces
  it (cleanAriaLabelParent moved before injecting new spans).
- Skip the write when there are no phrases, so a canceled/empty series
  can't blank out the label a reader is still speaking.
- cancel() (aria) no longer clears the region or moves focus; it drops
  partial phrases and properly stops the series (active=false, cancel
  nested) instead of returning early.
- Remove the now-unused focusCanvas() helper; no focus is moved at all.

Adds tests covering injection, persistence past the old 100ms window,
replace-on-next-write, and cancel not wiping the region.

Fixes #31

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chiefcll chiefcll merged commit dd0df90 into main Jun 18, 2026
1 check 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.

Announcer (aria mode): live-region text is deleted after 100 ms, so on-device screen readers never read it

1 participant