Skip to content

a11y(4.1.2): add label prop to Button primitive and enforce accessible names#3556

Merged
ardiewen merged 3 commits into
mainfrom
wcag/4.1.2-button-anchor-empty
Jul 13, 2026
Merged

a11y(4.1.2): add label prop to Button primitive and enforce accessible names#3556
ardiewen merged 3 commits into
mainfrom
wcag/4.1.2-button-anchor-empty

Conversation

@rosanusi

Copy link
Copy Markdown
Contributor

Summary

  • Adds a label?: string prop to BaseProps in the Button primitive (src/lib/holocene/button.svelte) rendered as aria-label on both the <a> anchor branch and <button> branch
  • Adds a dev-time onMount check that throws (dev) or console.errors (prod) when a button is rendered without any accessible name (no label, aria-label, aria-labelledby, title, slot text, leading/trailing icon, or count badge)
  • Updates 7 consumer call-sites that were triggering the defect: Workflows List (per-row Start button + 3 toolbar buttons), Activities List (per-row Start button), Schedules List (toolbar configure-columns button), and Schedule Detail page (refresh button)
  • Adds configure-columns i18n key to src/lib/i18n/locales/en/common.ts

Affected files

  • src/lib/holocene/button.svelte
  • src/lib/i18n/locales/en/common.ts
  • src/lib/components/workflow/start-workflow-button.svelte
  • src/lib/components/standalone-activities/start-activity-button.svelte
  • src/lib/components/workflow/workflows-summary-configurable-table.svelte
  • src/lib/pages/schedule-view.svelte
  • src/lib/pages/schedules.svelte

Test plan

  • Visual regression: all 7 consumer buttons render and function identically; no visible change
  • Screen reader (NVDA/VoiceOver): each button announces its label before activation
  • Dev-mode enforcement: render a <Button> with no name source — confirm it throws; add label="…" — confirm no throw
  • Anchor branch: render <Button href="…"> with no name source — confirm same enforcement fires
  • Count badge: render <Button count={3}> — confirm enforcement does not fire (count provides a numeric name)
  • pnpm check passes (no new type errors beyond pre-existing $$_$$ slot interop errors on main)

A11y-Audit-Ref: 4.1.2-button-anchor-empty

🤖 Generated with Claude Code

…cessible names

Adds a `label` prop to the Button/anchor primitive that renders as
`aria-label`, plus a dev-time `onMount` check that warns (prod) or throws
(dev) when a button is rendered without any accessible name. Updates
seven consumer call-sites and adds the `configure-columns` i18n key.

A11y-Audit-Ref: 4.1.2-button-anchor-empty

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 10, 2026 8:23pm

Request Review

@rosanusi
rosanusi marked this pull request as ready for review June 12, 2026 14:26
@rosanusi
rosanusi requested a review from a team as a code owner June 12, 2026 14:26
@ardiewen ardiewen changed the title [a11y] WCAG 4.1.2 — add label prop to Button primitive and enforce accessible names a11y(4.1.2): add label prop to Button primitive and enforce accessible names Jul 6, 2026
@github-actions github-actions Bot added a11y Accessibility audit PR a11y:bucket-3 Bucket 3: engineer required a11y:sc-4.1.2 labels Jul 6, 2026
…orcement

Reworks the approach based on review:

- button.svelte: remove the `label` prop and the onMount accessible-name
  throw. `aria-label` is already a typed attribute (Button's props intersect
  HTMLButtonAttributes/HTMLAnchorAttributes), so call sites set it directly —
  no redundant, ambiguously-named prop. Dropping the throw avoids crashing dev
  on any unlabeled Button (the check also wrongly treated a decorative
  leadingIcon as a name, so it both missed real violations and threw on
  equivalent ones).
- Convert the 7 call sites from label={…} to aria-label={…}.
- Add aria-label to two icon-only buttons the original PR missed (same defect):
  activities-summary configure-columns and workflow-current-details refresh.

Automated enforcement will move to the existing axe accessibility suite once it
is repaired and gated in CI (tracked as a follow-up).

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

ardiewen commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Pushed a revision reworking the approach based on review:

  • Dropped the label prop. aria-label is already a typed attribute on Button (its props intersect HTMLButtonAttributes/HTMLAnchorAttributes), so label was redundant — and it reads like it should render visible text. Call sites now pass aria-label directly.
  • Dropped the onMount enforcement. It throws in dev for any unlabeled Button (including Storybook/tests), and its hasName check treated a decorative leadingIcon as a name (icon/svg.svelte renders <svg aria-hidden={!title}>, and no title is passed) — so it simultaneously missed real violations (~22 leadingIcon-only buttons) and would have thrown on the equivalent icon-in-slot pattern.
  • Fixed 2 icon-only buttons this PR missed (same defect, would have tripped the removed check): activities-summary-configurable-table configure-columns and workflow-current-details refresh.

On enforcement: the repo already has an axe suite (tests/accessibility/) that computes the real accessible name and would catch this correctly — but it isn't gated in CI and has rotted (the page fails to load before axe runs). Repairing + CI-gating it is the right systematic guard, tracked as a follow-up in DT-4255 rather than a bespoke runtime check.

…hor-empty-review

# Conflicts:
#	src/lib/i18n/locales/en/common.ts
#	src/lib/pages/schedule-view.svelte
@ardiewen
ardiewen merged commit 273cc4b into main Jul 13, 2026
18 checks passed
@ardiewen
ardiewen deleted the wcag/4.1.2-button-anchor-empty branch July 13, 2026 14:34
ardiewen added a commit that referenced this pull request Jul 13, 2026
Follows the reinforcement guidance from #3556: bespoke onMount
accessible-name checks are removed in favor of the axe suite (DT-4255),
which computes the real accessible name. The dev throw also broke
Storybook/tests and could crash consumer dev environments.

Revert `label` to optional (`label?: string`, default '') so this is
not a breaking change for downstream consumers — cloud-ui has ~74
CodeBlock usages, none passing `label`, which a required prop would
break on tarball repack. The ui-side labels remain; axe enforces names.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
ardiewen added a commit that referenced this pull request Jul 13, 2026
Follows the reinforcement guidance from #3556: bespoke onMount
accessible-name checks are removed in favor of the axe suite (DT-4255),
which computes the real accessible name. The dev throw also broke
Storybook/tests and could crash consumer dev environments.

Revert `label` to optional (`label?: string`, default '') so this is
not a breaking change for downstream consumers — cloud-ui has ~74
CodeBlock usages, none passing `label`, which a required prop would
break on tarball repack. The ui-side labels remain; axe enforces names.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
ardiewen added a commit that referenced this pull request Jul 13, 2026
…for read-only mode (#3557)

* [a11y] WCAG 4.1.2 — require label on CodeBlock; add aria-readonly for read-only mode

Makes the `label` prop required on the CodeBlock primitive so every
CodeMirror instance gets an accessible name via `aria-label` on the
`.cm-content` element. Adds `aria-readonly="true"` when `editable=false`
so AT correctly conveys non-editability (JAWS forms-mode fix). Adds a
dev-time `onMount` guard that throws if label is empty after mount.

Migrates all ~28 affected files: primitive, 4 domain wrappers
(PayloadCodeBlock, PayloadInput, InputAndResultsPayload,
PayloadInputWithEncoding), and ~22 direct consumers across Workflow
History, Queries, Schedules, Activities, Events, Workers, and Archival.

Also fixes the PayloadInput orphaned <Label for> association (§NEW1):
replaces <Label for={id}> with <span> and forwards label to the inner
CodeBlock so the accessible name is set on the cm-content element rather
than a wrapper div that for= can't reach.

New i18n keys: common.json, events.import-format-{1,2},
standalone-activities.{activity-input,activity-outcome,cluster-config,
namespace-config}, workers.{heartbeat-config,setup-guide-command},
workflows.{query-result,callback-metadata,update-input,update-result}.

A11y-Audit-Ref: 4.1.2-codemirror-no-name

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

* [a11y] Add missing label to CodeBlock stories and compute-fields

code-block.stories.svelte: add label to "With Header" and "Test page
scrolling" stories which were missing the now-required prop.

compute-fields.svelte: add label to the Terraform IAM template CodeBlock
added in the serverless worker form refactor (landed in main after the
original PR was cut).

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

* a11y: drop CodeBlock runtime label check; keep label optional

Follows the reinforcement guidance from #3556: bespoke onMount
accessible-name checks are removed in favor of the axe suite (DT-4255),
which computes the real accessible name. The dev throw also broke
Storybook/tests and could crash consumer dev environments.

Revert `label` to optional (`label?: string`, default '') so this is
not a breaking change for downstream consumers — cloud-ui has ~74
CodeBlock usages, none passing `label`, which a required prop would
break on tarball repack. The ui-side labels remain; axe enforces names.

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

* a11y(4.1.2): make read-only CodeBlock focusable so its name is announced

Read-only CodeMirror sets .cm-content to contenteditable="false" with no
tabindex, so the element is not focusable — the aria-label/aria-readonly
land in the AX tree but no keyboard/SR user can reach the region, and it
is never announced (only the copy button was focusable).

Add tabindex="0" to the read-only contentAttributes so the named,
read-only textbox is reachable and announced ("Input, read only").
Verified in-browser: focusable + named. Note: an accessible-name axe
check passes without this, since it does not test reachability.

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

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Ardie Wen <[email protected]>
rossedfort added a commit that referenced this pull request Jul 13, 2026
Auto-generated version bump from 2.52.0 to 2.52.1

Bump type: patch

Changes included:
- [`3be0df00`](3be0df0) fix(schedules): default absent second/minute/hour to 0 when editing (#3653)
- [`508b65b7`](508b65b) fix(schedules): default emptied second/minute/hour to 0 on submit (#3655)
- [`1f0e149c`](1f0e149) fix(workers): reject $LATEST qualifier in Lambda ARN validation (#3657)
- [`b42b0776`](b42b077) a11y(2.4.3): inert-based focus containment + restore; drop trap from native dialogs (#3598)
- [`cc491a29`](cc491a2) a11y(2.4.3): keep toast live region announcing during a focus trap [DT-4252] (#3654)
- [`ddc7f8e2`](ddc7f8e) a11y(2.4.11): add scroll-padding so sticky overlays don't obscure focused elements (#3532)
- [`b7e3a472`](b7e3a47) FilterBar icon update (#3660)
- [`09318ec6`](09318ec) Add danger PR comment for api version bump (#3662)
- [`273cc4be`](273cc4b) a11y(4.1.2): add label prop to Button primitive and enforce accessible names (#3556)
- [`4507115f`](4507115) Use an editable combobox for the Cloud Run region field (DT-4232) (#3636)
- [`694a7471`](694a747) a11y(1.4.13): migrate saved-query nav tooltips to Tooltip primitive (#3607)
- [`8b384a40`](8b384a4) a11y(4.1.2): fix nested interactive elements in Copyable component (#3558)
- [`1bfd2750`](1bfd275) fix describe SANO API requests to include runId parameter (#3661)
- [`ba18b43e`](ba18b43) a11y(4.1.2): require label on CodeBlock primitive; add aria-readonly for read-only mode (#3557)
- [`6e2290f1`](6e2290f) feat(deployments): make compute provider picker configurable (#3659)
- [`4470b76a`](4470b76) fix(workers): persist WDV compute-config edits via update mask; revert $LATEST validation (#3658)
- [`17fd591b`](17fd591) fix(nexus-operations): make start standalone nexus operation timeouts optional (#3656)

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.2 a11y Accessibility audit PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants