Skip to content

fix(a11y): announce workflow / event status on timeline graph nodes (WCAG 1.4.1)#3443

Merged
bilal-karim merged 8 commits into
mainfrom
a11y/1.4.1-timeline-graph-aria-labels
Jun 4, 2026
Merged

fix(a11y): announce workflow / event status on timeline graph nodes (WCAG 1.4.1)#3443
bilal-karim merged 8 commits into
mainfrom
a11y/1.4.1-timeline-graph-aria-labels

Conversation

@bilal-karim
Copy link
Copy Markdown
Member

@bilal-karim bilal-karim commented May 22, 2026

Summary

Adds aria-label to each SVG <g role="button"> wrapper on the timeline-graph nodes so screen readers announce the workflow's id and status (or the event's type and classification) instead of bare "button".

 <g
   role="button"
   tabindex="0"
+  aria-label={translate('workflows.row-accessible-name', {
+    workflowId: workflow.id,
+    status: statusLabel,
+  })}
   class="relative cursor-pointer"
   {height}
 >

Previously, terminal statuses on the timeline graph (Completed, Failed, Canceled, TimedOut, Fired, Signaled) were distinguishable only by node color. The legend that decodes those colors lives inside a <Tooltip> — keyboard- and AT-reachable since PR #3429, but still requires the user to leave the graph, decode the color in the legend, and come back to identify each node. This change makes the graph self-describing at the node level.

Audit context

  • WCAG 2.2 SC 1.4.1 Use of Color (Level A) — Serious. Affects every workflow timeline view, which is the most-trafficked product surface for workflow investigation.
  • Issue file: audit-output/issues/1.4.1-timeline-graph-status.md (this is Option A — the recommended fix).
  • The sibling 1.4.1 fix (label required indicator) shipped in PR fix(a11y): non-color signal for Label required indicator (WCAG 1.4.1) #3439.

Cross-cutting wins

  • Closes the SC 4.1.2 Name, Role, Value missing-accessible-name defect on the same <g role="button"> widgets.
  • Closes the SC 1.3.1 Info and Relationships matrix-row item d (deferred to the Robust wave).

New i18n keys

  • workflows.row-accessible-name: "Workflow {{workflowId}}: {{status}}"
  • events.row-accessible-name: "Event {{eventType}}: {{classification}}"

Test plan

  • Open a workflow with failed/completed children. Tab into the timeline graph; with VoiceOver/NVDA, confirm each node announces "Workflow {id}: {status}, button" instead of just "button".
  • On the event history graph: tab to each dot; AT announces "Event {eventType}: {classification}, button".
  • Right-click → Inspect a node to confirm the <g> has the expected aria-label attribute.
  • axe-core svg-img-alt rule should no longer flag the timeline graph rows.
  • Zero visual change for sighted users.

🤖 Generated with Claude Code

A11y-Audit-Ref: 1.4.1-timeline-graph-status

…WCAG 1.4.1)

Adds aria-label to the SVG <g role="button"> wrapper on each
timeline-graph node so screen readers announce the workflow id
and status (or event type and classification) instead of bare
"button".

Previously, terminal statuses on the timeline graph (Completed,
Failed, Canceled, TimedOut, Fired, Signaled) were distinguishable
only by node color. The color legend lives inside a Tooltip --
keyboard- and AT-reachable since PR #3429, but still requires the
user to leave the graph, decode the color, and come back to
identify each node. This change makes the graph self-describing.

Cross-cutting wins:
- Closes the SC 4.1.2 missing-accessible-name defect on the same
  <g role="button"> widgets.
- Closes the SC 1.3.1 matrix-row item d deferred to the Robust
  wave.

New i18n keys:
- workflows.row-accessible-name = "Workflow {{workflowId}}: {{status}}"
- events.row-accessible-name = "Event {{eventType}}: {{classification}}"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bilal-karim bilal-karim requested a review from a team as a code owner May 22, 2026 16:40
@vercel
Copy link
Copy Markdown

vercel Bot commented May 22, 2026

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

Project Deployment Actions Updated (UTC)
holocene Ready Ready Preview, Comment Jun 3, 2026 8:03pm

Request Review

@temporal-cicd
Copy link
Copy Markdown
Contributor

temporal-cicd Bot commented May 22, 2026

Warnings
⚠️

📊 Strict Mode: 9 errors in 3 files (1.0% of 887 total)

src/lib/components/workflow-status.svelte (2)
  • L102:16: 'count' is possibly 'undefined'.
  • L103:9: 'count' is possibly 'undefined'.
src/lib/components/lines-and-dots/svg/timeline-graph-row.svelte (5)
  • L105:20: 'distance' is possibly 'null'.
  • L105:31: 'workflowDistance' is possibly 'null'.
  • L116:20: 'distance' is possibly 'null'.
  • L116:31: 'workflowDistance' is possibly 'null'.
  • L252:12: 'pendingActivity.attempt' is possibly 'null' or 'undefined'.
src/lib/components/lines-and-dots/svg/workflow-row.svelte (2)
  • L48:26: Type 'WorkflowStatus' is not assignable to type 'string | undefined'.
  • L58:24: Type 'WorkflowStatus' is not assignable to type 'string | undefined'.

Generated by 🚫 dangerJS against d5c7186

CI's check-types caught two TS errors in the timeline-graph
aria-label additions:

1. translate(\`workflows.\${workflowStatusKey(status)}\`) -- the
   helper's return type widened to plain string, which doesn't
   satisfy the strongly-typed translation-key union.
2. translate(\`events.event-classification.\${classification.toLowerCase()}\`)
   -- same shape; toLowerCase() returns plain string.
3. event.eventType -- doesn't exist on PendingActivity (a union
   member of WorkflowEventWithPending).

Replaced both dynamic template-literal lookups with `as const`
record literals keyed by the literal-union enum values, so
TypeScript can narrow to the exact translation-key union. Added
an `in` operator narrowing for the eventType access.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EventClassification union doesn't include 'Retrying' -- only the
i18n key exists. The lookup entry was unreachable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously the aria-label for a pending activity always said
"Pending" regardless of attempt count. The timeline already
communicates retry state visually (line styling), in the legend
("Retry" entry), and in the group-details row text -- but the
per-event aria-label missed it.

Check group.pendingActivity.attempt > 1 (matching the existing
group-details-row.svelte logic) and surface "Retrying" in the
aria-label when applicable. The events.event-classification.retrying
translation key already existed for this purpose.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added a11y Accessibility audit PR a11y:no-fix-doc No A11y-Audit-Ref line; audit team triage a11y:bucket-3 Bucket 3: engineer required a11y:sc-1.4.1 and removed a11y:no-fix-doc No A11y-Audit-Ref line; audit team triage labels May 28, 2026
ardiewen and others added 4 commits June 3, 2026 15:33
The status/classification -> translated-label mapping existed inline in
workflow-status.svelte and was duplicated by the two new lookup tables
added for the timeline-graph aria-labels (workflow-row,
history-graph-row-visual). Extract the canonical map into
$lib/utilities/get-status-label and reuse it in all three call sites so
there is a single source of truth.

No behavior change: the helper map is a verbatim copy of the canonical
Record<Status, string>; the history graph normalizes its lowercase
'pending'/'retrying' values to the PascalCase Status union for lookup
while leaving `classification` untouched for styling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
timeline-graph-row's <g role="button"> contained only SVG primitives and
<Text> (whose accessible-name contribution is unreliable across screen
readers), so it announced as bare "button" — the same defect this PR
fixes on workflow-row and history-graph-row-visual. Add an aria-label via
the shared getStatusLabel helper.

group-details-row's <g role="button"> is intentionally left for a
separate fix: it wraps foreignObject HTML (already name-contributing) and
nested interactive elements, so it needs a nested-interactive restructure
rather than an aria-label that would mask its content.

A11y-Audit-Ref: 1.4.1-timeline-graph-status

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Unit test for the shared getStatusLabel helper (workflow statuses, event
  classifications, pending/retrying, and the Unknown fallback).
- Integration test asserting the timeline graph's SVG <g role="button">
  nodes expose accessible names: the workflow node ("Workflow <id>: Running")
  and event nodes ("Event LongActivity: Scheduled", "Event customSignal:
  Signaled"). Verified red — the assertions fail if the aria-label regresses.

history-graph-row-visual shares the same getStatusLabel +
events.row-accessible-name mechanism (covered by the unit test and the
identical timeline-graph-row path); its /history graph view did not render
nodes deterministically in the integration harness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Avoid collisions between workflow statuses and event classifications that
share a name (Running, Completed, Failed, Canceled, Terminated, TimedOut).
The previous single combined map silently merged them and routed event
nodes through the workflows.* namespace, so a future divergence between
workflows.completed and events.event-classification.completed would have
mislabeled event nodes.

Now expose two domain-scoped resolvers, each backed by an exhaustive
Record over its own union (so a new status in either domain forces a label):
- getWorkflowStatusLabel  -> workflows.* (workflow-row)
- getEventClassificationLabel -> events.event-classification.* (history-graph-row-visual, timeline-graph-row)

getStatusLabel remains as the polymorphic resolver for WorkflowStatus.svelte,
which renders one badge for any status type and cannot know the domain from
the value; it prefers the workflow label on shared names, preserving that
component's existing behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ardiewen
Copy link
Copy Markdown
Contributor

ardiewen commented Jun 3, 2026

getStatusLabel split + workflow/event status overlap

Extracting the shared label helper surfaced that 6 status names already overlap between WorkflowStatus and EventClassification: Running, Completed, Failed, Canceled, Terminated, TimedOut. They're identical English today but live in separate i18n namespaces (workflows.* vs events.event-classification.*), so a future change to one could silently mislabel the other domain.

Handled in d5c7186d:

  • Domain resolvers (exhaustive over their own union): getWorkflowStatusLabelworkflows.* (workflow-row), getEventClassificationLabelevents.event-classification.* (graph event nodes).
  • getStatusLabel stays polymorphic for WorkflowStatus.svelte only — it renders one badge for any status (workflow/schedule/classification) and sees a bare string at runtime. On the 6 overlaps it prefers the workflow label, preserving existing behavior.

Covered by get-status-label.test.ts + timeline-graph-accessible-names.spec.ts.

@laurakwhit or @Alex-Tideman , do either one of you know why these lines represent some Workflow Statuses and Some EventClassification with overlap and aren't separated into their own translation domains?

@bilal-karim bilal-karim merged commit c848ec5 into main Jun 4, 2026
20 checks passed
@bilal-karim bilal-karim deleted the a11y/1.4.1-timeline-graph-aria-labels branch June 4, 2026 20:41
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-1.4.1 a11y Accessibility audit PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants