Skip to content

Give ProgressBar the progressbar ARIA role and value range#5586

Merged
lukemelia merged 3 commits into
mainfrom
cs-progressbar-a11y-role
Jul 23, 2026
Merged

Give ProgressBar the progressbar ARIA role and value range#5586
lukemelia merged 3 commits into
mainfrom
cs-progressbar-a11y-role

Conversation

@lukemelia

Copy link
Copy Markdown
Contributor

What

The shared ProgressBar (@cardstack/boxel-ui/components) rendered a plain <div>, so assistive technology couldn't identify it as a progress indicator. Add the standard progressbar semantics:

  • role="progressbar"
  • aria-valuenow — the value clamped into [0, max] (via a new valueNow getter, so it's always a valid value between min and max)
  • aria-valuemin="0" and aria-valuemax={{max}}
  • aria-valuetext — the rounded percentage (e.g. "25%") for a friendly screen-reader readout
  • keeps the existing aria-label

Why

Every consumer of ProgressBar benefits, so the fix belongs in the shared component rather than in each caller. (Surfaced while adopting ProgressBar in a card, whose hand-rolled bar previously carried these attributes itself.)

Tests

Adds progress-bar-test.gts (the component had none): asserts the role + aria value range, that aria-valuenow clamps into [0, max], and that @label becomes the accessible name. Run locally — 3 passed. Type-check, eslint, template-lint, prettier all clean.

🤖 Generated with Claude Code

The shared ProgressBar rendered a plain div, so assistive tech couldn't read
it as a progress indicator. Add role="progressbar" with aria-valuenow (the
value clamped into [0, max]), aria-valuemin, aria-valuemax, and aria-valuetext
(the percentage), keeping the existing aria-label. Benefits every consumer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lukemelia
lukemelia requested review from a team and burieberry July 22, 2026 21:44
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files  ±  0      1 suites  ±0   3h 3m 42s ⏱️ + 19m 33s
3 595 tests +267  3 580 ✅ +267  15 💤 ±0  0 ❌ ±0 
3 614 runs  +268  3 599 ✅ +268  15 💤 ±0  0 ❌ ±0 

Results for commit 0dd27b2. ± Comparison against earlier commit 533354e.

Realm Server Test Results

    1 files  ±0      1 suites  ±0   13m 19s ⏱️ -2s
1 933 tests +9  1 933 ✅ +9  0 💤 ±0  0 ❌ ±0 
2 012 runs  +9  2 012 ✅ +9  0 💤 ±0  0 ❌ ±0 

Results for commit 0dd27b2. ± Comparison against earlier commit 533354e.

A node with role="progressbar" must have an accessible name, and the visible
label text does not supply one (progressbar is not a name-from-content role).
Binding aria-label directly to an absent @Label produced aria-label="", which
fails axe's aria-progressbar-name rule wherever a ProgressBar renders without a
label (e.g. the docs accessibility audit). Fall back to a generic "Progress"
name when no @Label is passed so the progressbar always has one; callers with
context still pass a specific label.

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

@habdelra habdelra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Reviewed for ARIA correctness and whether the new progressbar semantics hold across the component's real consumers (I checked out the head and traced each ProgressBar call site).

Bottom line: a clean, correctly-scoped a11y fix with real test coverage — no blocking issues. Putting the semantics in the shared component rather than each caller is the right call, and the clamping design is exactly what assistive tech expects. One reachable edge case (aria-valuetext="NaN%" when @max is 0) is worth folding in since the fix is a one-liner in a getter you're already changing.

What lands right (and why it's safe):

  • valueNow clamped into [0, max]. aria-valuenow never escapes the declared aria-valuemin/aria-valuemax range — screen readers compute their percentage from valuenow within that range, so an out-of-range valuenow would be announced wrong. The clamp is the correct guard. I verified the display width is unchanged for every in-range and over-range input (only the degenerate max=0 case differs — see the inline thread).
  • accessibleLabel fallback to 'Progress'. Correct: a role="progressbar" node needs an accessible name, and unlike name-from-content roles the visible label text doesn't supply one, so the fallback prevents an unnamed progressbar. The hardcoded English is idiomatic here — boxel-ui has no i18n layer (no ember-intl/intl usage in the addon), so sibling components hardcode strings the same way.
  • ...attributes after the aria attributes. Callers can still override aria-label/role/etc. via attribute splatting, so this doesn't lock consumers out.
  • Percentage as aria-valuetext. A friendly readout ("25%") consistent with valuenow/valuemax; a reasonable choice.

Recommendations:

  1. Guard the max === 0 division in progressPercentage so aria-valuetext (and the CSS width) can't become NaN% on an empty-board bar — see the inline thread on index.gts. Non-blocking, one-liner.
  2. Add a lower-clamp test (negative @valuearia-valuenow '0'); optionally a @max={{0}} case — see the inline thread on the test file. Non-blocking.

Adjacent, out of scope: progressBarPosition has a dead return position ?? 'end' after an earlier if (!position) return '' — the ?? branch is unreachable. Pre-existing, not this PR; flagging for whoever next touches that getter.


Generated by Claude Code

Comment thread packages/boxel-ui/addon/src/components/progress-bar/index.gts
lukemelia added a commit that referenced this pull request Jul 23, 2026
…onent

The circular "falls back to the card title" note on listingName now
describes the actual relationship (cardTitle reads listingName). The
rendering test title no longer claims accessible attributes it does not
assert; the progressbar ARIA semantics live on the shared ProgressBar
(added in #5586) rather than at this call site.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When @max is 0, value/max is NaN, so aria-valuetext (announced by screen
readers) and the CSS width became "NaN%" — reachable on an empty-board bar.
Treat max=0 as 0%. Add tests for the lower value clamp and the max=0 case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lukemelia
lukemelia merged commit f338274 into main Jul 23, 2026
72 of 74 checks 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.

3 participants