Skip to content

a11y(4.1.3): Add live-region wrappers for workflow status, event count, filter counts, and Banner#3561

Merged
ardiewen merged 5 commits into
mainfrom
wcag/4.1.3-ui-main-live-region-gaps
Jul 6, 2026
Merged

a11y(4.1.3): Add live-region wrappers for workflow status, event count, filter counts, and Banner#3561
ardiewen merged 5 commits into
mainfrom
wcag/4.1.3-ui-main-live-region-gaps

Conversation

@rosanusi

@rosanusi rosanusi commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes WCAG 2.2 SC 4.1.3 (Status Messages) gaps across ui-main — dynamic surfaces that changed without announcing to screen readers. Verified manually with VoiceOver + Safari/Chrome (see AT notes below).

# Surface Fix
1 WorkflowStatus chip Opt-in announce prop adds role="status" aria-atomic="true"; enabled only on the detail-header chip. Per-row table cells and the status-filter dropdown stay inert — avoids mounting dozens of live regions per list page (no proliferation).
2 Event history polling sr-only role="status" in event-summary-table; debounced (250 ms) so a burst rendered across multiple reactive passes announces a single accurate total ("N new events loaded") instead of per-pass partial counts.
3 Filter result counts Count number + noun wrapped together in one role="status" aria-atomic="true" region on the workflows / activities / schedules pages, so it announces "N Workflows" rather than a context-free "N".
4 Holocene Banner getRole() maps dangerrole="alert" (assertive), defaultrole="status" (polite) + aria-atomic. Banner's type space is only default/danger, so this mapping is complete (not an Alert mismatch).
5 HeartBeat indicator aria-hidden on the decorative EKG wrapper so the animated graphic isn't surfaced as an unlabeled img inside the status chip's aria-atomic live region.

Files changed

  • src/lib/components/workflow-status.svelte, src/lib/layouts/workflow-header.svelte — Defect 1
  • src/lib/components/event/event-summary-table.svelte, src/lib/i18n/locales/en/workflows.ts — Defect 2
  • src/lib/pages/workflows-with-new-search.svelte, src/lib/pages/standalone-activities.svelte, src/lib/pages/schedules.svelte — Defect 3
  • src/lib/holocene/banner/banner.svelte — Defect 4
  • src/lib/components/heart-beat-indicator.svelte — Defect 5

Manual AT verification (VoiceOver + Safari/Chrome)

  • Defect 1 status chip announces on transition; correctly silent on initial page load (initial render, not a change).
  • Defect 2 event count announces an accurate burst total — debounce fix confirmed via DOM instrumentation (one signal = 4 raw events rendered as two +2 passes → single "4 new events loaded").
  • Defect 3 count region verified correct: persistent, inside <main>, never aria-hidden/inert, text mutates in place.
  • Defect 4 Banner role mapping is correct, but dynamic-insertion announcement is not AT-tested: holocene Banner has no consumer in OSS ui (and isn't exported from @temporalio/ui, so it doesn't cascade to cloud-ui). cloud-ui's own banner already uses the same warning → alert, else status pattern. Note: the role sits on a <section> mounted via {#if show} (created with its content), so dynamic appearance may not announce on all SR/browser combos — acceptable for banners shown on load; flagged for awareness.
  • ⚠️ VoiceOver caveat: VO only reliably announces a polite region when the VO cursor is within that region's landmark, and suppresses background polite regions while a menu/dropdown is open. So Defect 3 may be silent on VoiceOver while filtering via an open dropdown — NVDA/JAWS announce regardless. The markup is correct; this is a VO platform limitation, not a defect here.

Test plan

  • Defect 1: trigger a status transition on a running workflow; confirm AT announces the change without navigation
  • Defect 2: open event history for an active workflow, let it poll; confirm AT announces "N new events loaded" per batch (not per row), with accurate counts
  • Defect 3: filter workflow/activity/schedule list; confirm AT announces "N Workflows/Activities/Schedules" on count change (best on NVDA/JAWS; see VO caveat)
  • Defect 4: (not AT-tested — no OSS consumer; role mapping verified by code review; matches cloud-ui's existing banner)
  • Visual regression: all surfaces unchanged visually
  • axe-core: no new violations on workflow detail, workflow list, schedules, activities

Related

  • Follow-up: DT-4199 — applying the first list filter drops focus to <body> (outside <main>), which also blocks the VoiceOver count announcement on that step. Focus-management fix tracked separately.

A11y-Audit-Ref: 4.1.3-ui-main-live-region-gaps

🤖 Generated with Claude Code

…ter counts, and Banner

Defect 1 — WorkflowStatus chip: add role="status" aria-atomic="true" so
status transitions (Running → Completed / Failed / etc.) announce to AT
without requiring navigation.

Defect 2 — Event history polling: track item-count delta in
event-summary-table and emit a sr-only role="status" span announcing
"{N} new events loaded" each time the poll appends rows, avoiding
verbose per-row narration.

Defect 3 — Filter result counts: add role="status" aria-live="polite"
aria-atomic="true" to the workflow, activity, and schedule count spans
so polled count updates announce as status messages.

Defect 4 — Holocene Banner: add getRole() mapping (danger → role="alert",
default → role="status") and aria-atomic="true" on the section element,
mirroring the Alert primitive pattern.

A11y-Audit-Ref: 4.1.3-ui-main-live-region-gaps

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
holocene Ready Ready Preview, Comment Jul 6, 2026 4:36pm

Request Review

@github-actions github-actions Bot added a11y Accessibility audit PR a11y:bucket-3 Bucket 3: engineer required a11y:sc-4.1.3 labels Jun 12, 2026
@temporal-cicd

temporal-cicd Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor
Warnings
⚠️

📊 Strict Mode: 3 errors in 2 files (0.3% of 888 total)

src/lib/components/workflow-status.svelte (2)
  • L106:16: 'count' is possibly 'undefined'.
  • L107:9: 'count' is possibly 'undefined'.
src/lib/holocene/banner/banner.svelte (1)
  • L70:13: Type 'null' is not assignable to type '"search" | "link" | "success" | "error" | "action" | "activity" | "add-square" | "add" | "apple" | "archives" | "arrow-down" | "arrow-left" | "arrow-up" | "arrow-right" | "ascending" | ... 146 more ... | "xmark-square"'.

Generated by 🚫 dangerJS against 5bfb815

- WorkflowStatus: gate role="status" behind an opt-in `announce` prop,
  enabled only on the detail-header chip, so per-row table cells and the
  status-filter dropdown no longer mount live regions (no proliferation).
- event-summary-table: debounce the "N new events loaded" announcement so
  a burst rendered across multiple reactive passes announces one accurate
  total instead of per-pass partial counts.
- filter counts (workflows/activities/schedules): wrap the number + noun in
  one role="status" region so it announces "N Workflows", not a bare "N".
- heart-beat-indicator: aria-hidden the decorative EKG wrapper so it isn't
  surfaced as an unlabeled image inside the status chip's live region.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@ardiewen
ardiewen marked this pull request as ready for review June 26, 2026 18:56
@ardiewen
ardiewen requested a review from a team as a code owner June 26, 2026 18:56
Pulls the debounced "N new events loaded" live-region logic out of
event-summary-table into:
- utilities/count-announcer.ts — framework-agnostic debounce/accumulate logic
  (tracks increases, coalesces bursts, clears-then-re-sets so identical
  consecutive counts re-announce); fully unit-tested with fake timers.
- components/live-count-announcer.svelte — thin Svelte adapter that wires a
  reactive count to the announcer and renders the sr-only polite region.

event-summary-table now renders <LiveCountAnnouncer count={items.length} ... />.
Behavior unchanged; the only tricky logic is now isolated and covered.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@ardiewen

ardiewen commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@rosanusi Changes made during review + AT testing (9351bfe9d2236ac79638a85d, 12 files):

  1. WorkflowStatusrole="status" now behind an opt-in announce prop, header-only (no per-row/dropdown proliferation).
  2. Event count — extracted to utilities/count-announcer.ts (debounced, coalesces bursts, re-announces identical counts) + live-count-announcer.svelte; 7 unit tests.
  3. Filter counts — wrap number + noun in one role="status" region → "N Workflows", not a bare "N".
  4. BannergetRole(): danger → alert, default → status.
  5. HeartBeataria-hidden so the decorative EKG isn't announced inside the chip's live region.

⚠️ VoiceOver won't announce polite regions while a dropdown is open (NVDA/JAWS do) — markup verified correct. Follow-up: DT-4199 (first filter-add drops focus to <body>).

Resolved i18n conflict in workflows.ts by keeping both main's
'total-workflows' key and this branch's 'new-events-announcement' keys.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@ardiewen
ardiewen merged commit d2111ec into main Jul 6, 2026
17 checks passed
@ardiewen
ardiewen deleted the wcag/4.1.3-ui-main-live-region-gaps branch July 6, 2026 17:03
temporal-cicd Bot pushed a commit that referenced this pull request Jul 8, 2026
Auto-generated version bump from 2.51.1 to 2.52.0

Bump type: minor

Changes included:
- [`9c6fcd54`](9c6fcd5) Add direct SSO redirect and backend logout (#3590)
- [`feab0d90`](feab0d9) a11y(4.1.3): Toaster role="log" → per-toast role="status"/"alert" for correct AT announcement semantics (#3560)
- [`d7bbfb01`](d7bbfb0) a11y(2.5.8): toast — replace bare close button with IconButton (36×36 px) (#3553)
- [`4efd6ab6`](4efd6ab) a11y(3.3.1): Holocene input primitives — add aria-invalid, aria-describedby, and uniform live-region error announcement (#3554)
- [`0600b700`](0600b70) Use green only (#3599)
- [`94107549`](9410754) Clarify activity pause timeout behavior (#3600)
- [`9f88d5a8`](9f88d5a) Bump node version to 22.23.1 or newer to fix issue from previous security release (#3604)
- [`ca4104ae`](ca4104a) Update reason copy and inputs (#3588)
- [`51db73fa`](51db73f) Update batch operation reason input hint (#3605)
- [`836d7c55`](836d7c5) a11y(2.4.3): route navigation — move focus to the main landmark on afterNavigate, not just scroll (#3538)
- [`4ee95696`](4ee9569) Improve re-renders for large encoded event histories (#3592)
- [`fa5e7aa1`](fa5e7aa) feature/schedules (#3603)
- [`0d9467fd`](0d9467f) Default includeHeartbeatDetails and includeLastFailure to true for getActivityExecution and pollActivityExecution (#3521)
- [`e5bc514e`](e5bc514) fix schedules: duplicate key (#3612)
- [`f1eb489f`](f1eb489) Remove preview badge (#3581)
- [`1e5173b4`](1e5173b) Add existing versions to worker create deployment version form (#3601)
- [`a8552bae`](a8552ba) fix schedules -e.trim not a function (#3613)
- [`0442f847`](0442f84) Fix width of ComputeBadge in VersionTableRow (#3615)
- [`ddbeb54f`](ddbeb54) fix(cloud-nav): animate and stabilize side-nav collapse (#3606)
- [`3cfc684b`](3cfc684) fix - schedules recent/upcoming runs - sort issue (#3616)
- [`92b8875b`](92b8875) Move timestamp out of translate (#3617)
- [`901258af`](901258a) Fix SAA and WF consistency (#3614)
- [`99593d97`](99593d9) DT-4001 - standalone nexus operations (#3496)
- [`f10d1bf1`](f10d1bf) Clamp codeblock to container width (#3621)
- [`0a9d93ce`](0a9d93c) Remove unnecessary bottom-0 (#3623)
- [`9dabb522`](9dabb52) chore(deps-dev): bump vite from 6.4.2 to 6.4.3 (#3610)
- [`5c1854d3`](5c1854d) News Feed (#3596)
- [`340da30f`](340da30) a11y(2.1.1): guard global r/R keydown so text inputs can receive the letter (#3589)
- [`dd97a9d1`](dd97a9d) fix(markdown-editor): disable chromatic snapshot for preview story (#3631)
- [`7db61ab9`](7db61ab) a11y(4.1.2): Table / ProgressBar / Loading primitives — expose aria-busy on data-refresh regions (#3555)
- [`d2111ecf`](d2111ec) a11y(4.1.3): Add live-region wrappers for workflow status, event count, filter counts, and Banner (#3561)
- [`24eabaec`](24eabae) a11y(2.4.6): guard DrawerContent against empty <h2> when title is omitted (#3595)
- [`3552e307`](3552e30) Fix CodeMirror Lezer dependency mismatch (#3632)
- [`7ac37199`](7ac3719) Update defaults to match workflow defaults (#3634)
- [`c1f02865`](c1f0286) Draft fix for link variant handling (#3620)
- [`bbbf2b8c`](bbbf2b8) Add connection status and optional Latest Version column to deployments (DT-4174) (#3618)
- [`76f266f4`](76f266f) DurationInput updates (#3633)
- [`07da53c5`](07da53c) COM-191 - prefill CloudFormation template params in Launch Stack link (#3619)
- [`90946e0b`](90946e0) Schedules - Timeout empty defaults (#3637)
- [`97f9ad39`](97f9ad3) fix(cloud-nav): smooth label fade on side-nav collapse/expand (#3628)
- [`a399ed35`](a399ed3) Improve Search Attribute deletion messaging (#3629)
- [`ad976d94`](ad976d9) Pass through source prop for News Feed Widget (#3638)
- [`6ef84fe3`](6ef84fe) Restrict select-all to exclude workflow status (#3639)
- [`4c3729f8`](4c3729f) Reduce news feed markdown whitespace (#3642)
- [`aa04a9b6`](aa04a9b) feat(nexus-operations): add loading state to standalone nexus operations list page header (#3643)
- [`dce2f804`](dce2f80) chore: remove unused generate-holocene-props script (#3644)
- [`760bc7bc`](760bc7b) Make megaphone icon bigger (#3645)
- [`c047384a`](c047384) refactor(nexus): replace hand-rolled types with @temporalio/proto (#3646)
rossedfort added a commit that referenced this pull request Jul 8, 2026
Auto-generated version bump from 2.51.1 to 2.52.0

Bump type: minor

Changes included:
- [`9c6fcd54`](9c6fcd5) Add direct SSO redirect and backend logout (#3590)
- [`feab0d90`](feab0d9) a11y(4.1.3): Toaster role="log" → per-toast role="status"/"alert" for correct AT announcement semantics (#3560)
- [`d7bbfb01`](d7bbfb0) a11y(2.5.8): toast — replace bare close button with IconButton (36×36 px) (#3553)
- [`4efd6ab6`](4efd6ab) a11y(3.3.1): Holocene input primitives — add aria-invalid, aria-describedby, and uniform live-region error announcement (#3554)
- [`0600b700`](0600b70) Use green only (#3599)
- [`94107549`](9410754) Clarify activity pause timeout behavior (#3600)
- [`9f88d5a8`](9f88d5a) Bump node version to 22.23.1 or newer to fix issue from previous security release (#3604)
- [`ca4104ae`](ca4104a) Update reason copy and inputs (#3588)
- [`51db73fa`](51db73f) Update batch operation reason input hint (#3605)
- [`836d7c55`](836d7c5) a11y(2.4.3): route navigation — move focus to the main landmark on afterNavigate, not just scroll (#3538)
- [`4ee95696`](4ee9569) Improve re-renders for large encoded event histories (#3592)
- [`fa5e7aa1`](fa5e7aa) feature/schedules (#3603)
- [`0d9467fd`](0d9467f) Default includeHeartbeatDetails and includeLastFailure to true for getActivityExecution and pollActivityExecution (#3521)
- [`e5bc514e`](e5bc514) fix schedules: duplicate key (#3612)
- [`f1eb489f`](f1eb489) Remove preview badge (#3581)
- [`1e5173b4`](1e5173b) Add existing versions to worker create deployment version form (#3601)
- [`a8552bae`](a8552ba) fix schedules -e.trim not a function (#3613)
- [`0442f847`](0442f84) Fix width of ComputeBadge in VersionTableRow (#3615)
- [`ddbeb54f`](ddbeb54) fix(cloud-nav): animate and stabilize side-nav collapse (#3606)
- [`3cfc684b`](3cfc684) fix - schedules recent/upcoming runs - sort issue (#3616)
- [`92b8875b`](92b8875) Move timestamp out of translate (#3617)
- [`901258af`](901258a) Fix SAA and WF consistency (#3614)
- [`99593d97`](99593d9) DT-4001 - standalone nexus operations (#3496)
- [`f10d1bf1`](f10d1bf) Clamp codeblock to container width (#3621)
- [`0a9d93ce`](0a9d93c) Remove unnecessary bottom-0 (#3623)
- [`9dabb522`](9dabb52) chore(deps-dev): bump vite from 6.4.2 to 6.4.3 (#3610)
- [`5c1854d3`](5c1854d) News Feed (#3596)
- [`340da30f`](340da30) a11y(2.1.1): guard global r/R keydown so text inputs can receive the letter (#3589)
- [`dd97a9d1`](dd97a9d) fix(markdown-editor): disable chromatic snapshot for preview story (#3631)
- [`7db61ab9`](7db61ab) a11y(4.1.2): Table / ProgressBar / Loading primitives — expose aria-busy on data-refresh regions (#3555)
- [`d2111ecf`](d2111ec) a11y(4.1.3): Add live-region wrappers for workflow status, event count, filter counts, and Banner (#3561)
- [`24eabaec`](24eabae) a11y(2.4.6): guard DrawerContent against empty <h2> when title is omitted (#3595)
- [`3552e307`](3552e30) Fix CodeMirror Lezer dependency mismatch (#3632)
- [`7ac37199`](7ac3719) Update defaults to match workflow defaults (#3634)
- [`c1f02865`](c1f0286) Draft fix for link variant handling (#3620)
- [`bbbf2b8c`](bbbf2b8) Add connection status and optional Latest Version column to deployments (DT-4174) (#3618)
- [`76f266f4`](76f266f) DurationInput updates (#3633)
- [`07da53c5`](07da53c) COM-191 - prefill CloudFormation template params in Launch Stack link (#3619)
- [`90946e0b`](90946e0) Schedules - Timeout empty defaults (#3637)
- [`97f9ad39`](97f9ad3) fix(cloud-nav): smooth label fade on side-nav collapse/expand (#3628)
- [`a399ed35`](a399ed3) Improve Search Attribute deletion messaging (#3629)
- [`ad976d94`](ad976d9) Pass through source prop for News Feed Widget (#3638)
- [`6ef84fe3`](6ef84fe) Restrict select-all to exclude workflow status (#3639)
- [`4c3729f8`](4c3729f) Reduce news feed markdown whitespace (#3642)
- [`aa04a9b6`](aa04a9b) feat(nexus-operations): add loading state to standalone nexus operations list page header (#3643)
- [`dce2f804`](dce2f80) chore: remove unused generate-holocene-props script (#3644)
- [`760bc7bc`](760bc7b) Make megaphone icon bigger (#3645)
- [`c047384a`](c047384) refactor(nexus): replace hand-rolled types with @temporalio/proto (#3646)

Co-authored-by: rossedfort <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a11y:bucket-3 Bucket 3: engineer required a11y:sc-4.1.3 a11y Accessibility audit PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants