Found while implementing #6780 (PR #6801). Filed rather than folded in — it is
#3222's class, not #6780's.
What
NumberField never reads the published validation slot. It destructures
{ value, onChange, field, readonly, ...props } — no error — so the error
prop lands in props, and toDomProps (a whitelist) drops it. The widget
renders no aria-invalid of its own.
Consequence: when the form renderer marks a number field invalid, the
control is not announced as invalid to assistive tech by the widget itself.
#3222 gave email, phone, url, textarea, currency, percent,
markdown and later select an aria-invalid={!!error} and pinned each of
them in packages/fields/src/__tests__/widget-aria-invalid-e2e.test.tsx.
NumberField is absent from that list and from that pin — so the gap is
invisible to the suite that exists precisely to catch it.
⚠️ Note for whoever picks this up
PR #6801 writes aria-invalid on NumberField only when its own bad-input
refusal is active, deliberately spread conditionally rather than as
aria-invalid={!!refusal}. That is because the widget does NOT read error:
an unconditional attribute would stamp "false" over the correct value that
FormControl's Radix Slot hands down — the exact overwrite the #3222 e2e file
warns about in its header.
⇒ When error is wired up here, that conditional spread should become the
ordinary aria-invalid={!!error || !!refusal} the other three widgets use, and
number should be added to the #3222 e2e list. Doing one without the other
re-opens the overwrite.
Scope
Small: destructure error, OR it into the existing refusal computation, add
number to the WIDGETS table in widget-aria-invalid-e2e.test.tsx.
Generated by Claude Code
Found while implementing #6780 (PR #6801). Filed rather than folded in — it is
#3222's class, not #6780's.
What
NumberFieldnever reads the published validation slot. It destructures{ value, onChange, field, readonly, ...props }— noerror— so theerrorprop lands in
props, andtoDomProps(a whitelist) drops it. The widgetrenders no
aria-invalidof its own.Consequence: when the form renderer marks a
numberfield invalid, thecontrol is not announced as invalid to assistive tech by the widget itself.
#3222 gave
email,phone,url,textarea,currency,percent,markdownand laterselectanaria-invalid={!!error}and pinned each ofthem in
packages/fields/src/__tests__/widget-aria-invalid-e2e.test.tsx.NumberFieldis absent from that list and from that pin — so the gap isinvisible to the suite that exists precisely to catch it.
PR #6801 writes
aria-invalidonNumberFieldonly when its own bad-inputrefusal is active, deliberately spread conditionally rather than as
aria-invalid={!!refusal}. That is because the widget does NOT readerror:an unconditional attribute would stamp
"false"over the correct value thatFormControl's Radix Slot hands down — the exact overwrite the #3222 e2e file
warns about in its header.
⇒ When
erroris wired up here, that conditional spread should become theordinary
aria-invalid={!!error || !!refusal}the other three widgets use, andnumbershould be added to the #3222 e2e list. Doing one without the otherre-opens the overwrite.
Scope
Small: destructure
error, OR it into the existing refusal computation, addnumberto theWIDGETStable inwidget-aria-invalid-e2e.test.tsx.Generated by Claude Code