Give ProgressBar the progressbar ARIA role and value range#5586
Conversation
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>
Preview deploymentsHost Test Results 1 files ± 0 1 suites ±0 3h 3m 42s ⏱️ + 19m 33s Results for commit 0dd27b2. ± Comparison against earlier commit 533354e. Realm Server Test Results 1 files ±0 1 suites ±0 13m 19s ⏱️ -2s 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
left a comment
There was a problem hiding this comment.
[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):
valueNowclamped into[0, max].aria-valuenownever escapes the declaredaria-valuemin/aria-valuemaxrange — screen readers compute their percentage fromvaluenowwithin that range, so an out-of-rangevaluenowwould 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 degeneratemax=0case differs — see the inline thread).accessibleLabelfallback to'Progress'. Correct: arole="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 (noember-intl/intlusage in the addon), so sibling components hardcode strings the same way....attributesafter the aria attributes. Callers can still overridearia-label/role/etc. via attribute splatting, so this doesn't lock consumers out.- Percentage as
aria-valuetext. A friendly readout ("25%") consistent withvaluenow/valuemax; a reasonable choice.
Recommendations:
- Guard the
max === 0division inprogressPercentagesoaria-valuetext(and the CSS width) can't becomeNaN%on an empty-board bar — see the inline thread onindex.gts. Non-blocking, one-liner. - Add a lower-clamp test (negative
@value→aria-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
…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>
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 newvalueNowgetter, so it's always a valid value between min and max)aria-valuemin="0"andaria-valuemax={{max}}aria-valuetext— the rounded percentage (e.g."25%") for a friendly screen-reader readoutaria-labelWhy
Every consumer of
ProgressBarbenefits, so the fix belongs in the shared component rather than in each caller. (Surfaced while adoptingProgressBarin 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, thataria-valuenowclamps into[0, max], and that@labelbecomes the accessible name. Run locally — 3 passed. Type-check, eslint, template-lint, prettier all clean.🤖 Generated with Claude Code