Global progress.tsx Wrapper aria-valuenow Fix (Follow-up to Sprint 222 — Canonicalizing a Latent a11y Gap)

SummaryA frontend-only sprint that canonicalizes what Sprint 222 had worked around: the quiz progressbar's missing aria-valuenow was patched via a QuizPlay-local explicit aria-valuenow={percent}, while the root-cause shared-wrapper fix was carried over. Root cause: the shared progress.tsx wrapper destructured value but did not forward it to ProgressPrimitive.Root (value was also dropped from {...props}), so Radix could not auto-assign aria-valuenow → every progressbar using the wrapper was potentially missing aria-valuenow. (Fix) Pass value={value} explicitly to <ProgressPrimitive.Root> (before {...props}) — the Indicator transform still uses value||0, so the visual is completely unchanged. Result: every wrapper-based progressbar exposes role=progressbar's aria-valuenow/valuemin/valuemax via Radix auto-assignment. (Canonicalization) Remove the duplicate QuizPlay-local aria-valuenow={percent} that Sprint 222 added to work around the wrapper defect — Radix auto-assigns the same value; aria-label and aria-valuetext ('1 / 3') are kept because Radix cannot substitute them. (Safety net) The wrapper had no unit test; add +4 (value exposure, undefined/null indeterminate non-exposure, caller aria override). QuizPlay.test.tsx is unchanged and still passes (aria-valuenow='33'). The sole consumer of the ProgressPrimitive-based Progress is QuizPlay; ScoreGauge/CategoryBar are custom SVG/div implementations and out of scope. 0 new tokens, 0 new components. merge≠live (separate ops carryover).

Date
ImpactCritical

Goal

  • Canonically resolve the shared progress.tsx wrapper a11y gap that Sprint 222 carried over as follow-up, optional.
  • Globally fix the wrapper to forward value to ProgressPrimitive.Root so wrapper-based progressbars expose Radix's auto aria-valuenow (no visual change).
  • Frontend-only — no data/schema/backend change. 0 new design tokens/components. /quiz is auth-gated, so live verification is a separate ops carryover (merge ≠ live).

Background

In Sprint 222 (D2) we found the QuizPlay progress bar's aria-valuenow was empty and fixed it with a local patch — passing aria-valuenow={percent} explicitly from QuizPlay. But the root-cause global fix — the shared progress.tsx wrapper swallowing value instead of forwarding it to Radix — was carried over for scope discipline (ADR sprint-222 §carryover, lesson ⑥).

Confirming the root cause in code:

TSX
function Progress({ className, value, ...props }) {   // value destructured → dropped from props
  return (
    <ProgressPrimitive.Root className={...} {...props}> {/* value not forwarded */}
      <ProgressPrimitive.Indicator style={{ transform: `translateX(-${100 - (value || 0)}%)` }} />
    </ProgressPrimitive.Root>
  );
}

Because value is destructured it is not included in {...props}, so it is never forwarded to Root and is used only in the Indicator's transform style. As a result Radix does not know value and cannot auto-assign aria-valuenow. Every progressbar using the wrapper was potentially missing aria-valuenow (only the quiz site had a local patch).

Decisions

D1. Forward value to Root in progress.tsx (the core one-liner)

Pass value={value} to <ProgressPrimitive.Root> before {...props}.

  • The Indicator transform still uses value || 0completely unchanged visual.
  • Result: every wrapper-based progressbar exposes role="progressbar"'s aria-valuenow/aria-valuemin(0)/aria-valuemax(default 100) via Radix auto-assignment + normalized data-state (loading/complete).
  • Caller-supplied aria-* (e.g. aria-label, custom aria-valuetext) can still override via the trailing {...props} (Radix spreads user props last — Sprint 222 lesson ④).

D2. Remove the duplicate QuizPlay-local patch (canonicalization)

Remove the aria-valuenow={percent} line that Sprint 222 added to QuizPlay <Progress> to work around the wrapper defect.

  • After removal, Radix auto-assigns the same value from value={percent} → identical exposure/behavior.
  • aria-label (accessible name) and aria-valuetext ("1 / 3") are kept — Radix does not auto-generate an accessible name, and the default aria-valuetext is "{percent}%" (e.g. "33%"), which cannot substitute the desired "1 / 3" progress text, so keeping the caller values is required.

D3. Restraint — clarify out-of-scope

  • The sole consumer of the ProgressPrimitive-based Progress is QuizPlay (1 site).
  • ScoreGauge.tsx/CategoryBar.tsx are progress visualizations implemented with custom SVG/div, do not use Radix, and are therefore out of scope for this global fix (separate sprint if needed).
  • 0 new design tokens · 0 new components.

Implementation

Deliverables (Wave order)

2 atomic commits total (start 33155d7):

WaveAgentCommitContent
W-AHerald1b3dcf4progress.tsx: pass value={value} to <ProgressPrimitive.Root> (before {...props}) + JSDoc / QuizPlay: remove duplicate aria-valuenow={percent} (keep aria-label·aria-valuetext)
W-BHerald001b065New progress.test.tsx +4 — value → role=progressbar+aria-valuenow/valuemin/valuemax, undefined/null (indeterminate) → no aria-valuenow+no crash, caller aria-label/aria-valuetext overrides Radix defaults

Change details

  • progress.tsx (W-A 1b3dcf4): add value={value} before {...props} on <ProgressPrimitive.Root>. JSDoc documents that forwarding to Root is the prerequisite for Radix auto aria-valuenow and that caller aria-* can override via the trailing spread. Indicator style unchanged.
  • QuizPlay.tsx (W-A 1b3dcf4): remove aria-valuenow={percent} from <Progress>. value={percent}·aria-label·aria-valuetext retained.
  • Tests (W-B 001b065): new wrapper unit test (src/components/ui/__tests__/progress.test.tsx), 4 cases. QuizPlay.test.tsx unchanged — its aria-valuenow='33' assertion still passes via Radix auto-assignment (value=33) (regression confirmed).

Verification

  • tsc: 0 errors.
  • ESLint (real binary): 0 new errors.
  • jest: all suites PASS — progress +4 and QuizPlay regression 5 pass. Global coverage gates (lines/branches 83/71) maintained.
  • next build: ✓. /[locale]/quiz bundle effectively unchanged (attribute/forwarding only, no new code).
  • ADR gates: index count (sprint 161, --strict) / adr-en coverage (sprint-223 EN, --strict) / adr-links 0 broken / doc-refs no broken.

Critic Cross-Review

  • Tool: Codex codex-cli 0.130.0, codex review --base 33155d7 -c model=gpt-5.5
  • Rounds: 1

R1 — CLEAN (0 Critical/High/Medium/Low): "The change correctly forwards the destructured Progress value to the Radix root, allowing Radix to emit progressbar ARIA attributes, and the removed QuizPlay aria-valuenow is covered by that forwarding. The added tests align with the intended behavior and no regressions are evident from the diff."

Verdict: ✅ mergeable — the wrapper's valueRoot forwarding emits Radix progressbar ARIA correctly, the aria-valuenow removed from QuizPlay is covered by that forwarding, and the added tests align with the intent. Single-round CLEAN, 0 fixes.