fix(fields): announce bad input across the type="number" widget class - #6801
Merged
Conversation
A `type="number"` widget could DISPLAY one value and store another with nothing said. Typing `1e` leaves Chromium visibly showing `1e` while `.value` reads the empty string, so the widget emitted `null`, `aria-invalid` stayed `"false"`, and no diagnostic was drawn. Announce when `e.target.validity.badInput` is true — the platform's own predicate, not a renderer-side dialect — across CurrencyField, PercentField, NumberField and GeolocationField as one class, reusing objectui#6716's refusal shape. Adds the blur arm the paste route needs: pasting `1e` into an empty box never moves `.value` off `''`, so React suppresses the change event entirely. The guard ANNOUNCES; it deliberately does not refuse. Refusing would leave `props.value` unchanged and React's `updateInput` would write it back over the raw text the diagnostic points at (measured in Chromium and read out of React 19.2.8's own source). Part of #6780 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
The binding second half of #6780's ruling. A control that warns about `1e` but silently stores `1.23` for a pasted `1.2.3` teaches users that no warning means the value is right, so the asymmetry is documented rather than left implicit. The canonical explanation is the fields guide; the currency, percent and number pages carry a short pointer with their own numbers, and the package README states it for consumers rendering these widgets directly. Also collapses the new suite's `any` casts into one structural widget type (AGENTS.md #6), and adds the changeset. Part of #6780 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
This was referenced Aug 29, 2026
Closed
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
This was referenced Aug 29, 2026
os-sales
pushed a commit
that referenced
this pull request
Aug 29, 2026
Both said `buildValidationRules` "still has no `location` branch". This branch gives it one, so shipping that prose in the same package would leave a contradiction for the next reader — the failure mode the docblock exists to prevent. Each sentence was a compound claim and only half of it is falsified: - "still has no `location` branch" -> false as of this branch - "this card does not give it one" -> still true (#6716 / the refusal-diagnostic card did not add it; objectui#6744 did) So the true half is kept verbatim and only the false half is corrected, with a pointer to the card that added the branch and a note that it serves the STORED case and never these refusal arms — which is the property the surrounding paragraph is actually about, and which this branch confirms rather than contradicts. Prose only: no assertion, no behaviour, no export, no changeset. Neither file is in PR #6801's file list (verified against its 14 files). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
This was referenced Aug 29, 2026
os-sales
marked this pull request as ready for review
August 29, 2026 22:15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6780
Implements the 2026-08-29 ruling (option A): announce when
e.target.validity.badInputis true, across the wholetype="number"widgetCLASS as one change — CurrencyField, PercentField, NumberField,
GeolocationField — reusing the refusal shape #6716 introduced for LocationField.
Terrain, re-derived on my own base
Base
d06059f24. Two corrections to the dispatch note, both on that same ref:badInputis NOT absent from the repo. PR docs(fields): record the measured browser reading behind CurrencyField/PercentField #6777 (c6732825d) is anancestor of
d06059f24, and it left 11 occurrences — a measurement suite pluscomments in CurrencyField and PercentField. There WAS an in-repo record to
build on, and this PR builds on it. (The dispatch note's other hit, an onBlur
comment in
data-table.tsx, does not exist; that file has nobadInput.)packages/fields/src/widgets/, andPercentField has no
onBlurtoday. Neither do NumberField orGeolocationField, so all three gain one here.
Is the guard real, or a second no-op?
#6715's anchored guard was measured to be a provable no-op in this exact area,
so this was measured before it was written — Chromium 141.0.7390.37 via
Playwright, typing key by key into a real number input served over
http://127.0.0.1:.valuebadInput1e,1e-,1e+,5e,-,.,+,-.,e""12,1.23,010,15,1,12.345It fires on nine keyboard-reachable states and on nothing a real browser
emits. That is the opposite of #6715's result here, and it is why this is
worth shipping.
The dispatch order said
badInputis "the ONE signal happy-dom and Chromiumagree on", and asked me to stop if that measured false. Measured, it is too
strong — but not in a way that undermines option A, so I proceeded and
recorded the narrower truth instead.
badInputis never true for a programmatic.valuewrite.It reports that the agent cannot convert the user's input; a script write has
no user input to fail on. So on the only route a happy-dom unit test has
(
fireEvent.change, which sets.valuethen dispatches), Chromium answersfalse for every string and happy-dom answers true for nine — on that route the
two engines agree on nothing.
programmatic verdict and Chromium's TYPED verdict. It holds for a real subset:
four strings agree on "bad", seven agree on "fine", and eight disagree.
⇒ Consequence, and the reason this matters rather than being trivia: a unit test
here may drive ONLY the agreeing subset. Driving
0x10or12abcwould go greenover a branch the product never executes — the exact failure #6765 exists to
prevent. The full matrix and the allowed lists are pinned in
packages/fields/src/__tests__/numberInputBrowserReadings.ts, the overstatedone-liner is corrected where it was written, and the disagreement is now an
assertion, not a comment.
It announces; it deliberately does not refuse
#6716's shape refuses (its
onChangenever fires). Here that would destroy thevery text the diagnostic points at. Measured in both halves:
1e, a script write of.valueclears the raw displayand flips
badInputback to false — even writing the empty string.updateInputrestores a number input withif ((0 === value && "" === element.value) || element.value != value).So for a box that already held
5, refusing leaves the React value at5whilethe element value is empty, and React writes
"5"back over the5ethe user islooking at. Emitting keeps the two equal, the write is skipped, and the raw text
survives — which is what #6716 requires of a refusal.
The blur arm
Pasting
1einto an empty box fires one DOMinputevent but never moves.valueoff empty, so React's input-value tracking suppresses the syntheticchange entirely;
badInputis still true at blur. The three widgets that gain anonBlurcompose any handler a host supplied rather than replacing it —onBluris a declared DOM pass-through key, and a bare handler after thetoDomPropsspread would silently drop it.Second ruling condition: truncation stays silent, in writing
Binding, not a nicety.
1.2.3stores1.23and0x10stores10, and nowidget-side guard can refuse them — the browser discards the characters before
any handler runs. Documented in
content/docs/guide/fields.md(canonical), onthe currency / percent / number field pages, and in the package README, each
stating plainly that no warning does not mean the value is right.
Ablation, both directions
From the committed tree, one lock acquisition, absolute-path restore trap,
mutation proven on disk by counting injected and removed text, restore proven by
git diff HEADempty and blob hash equal to the HEAD blob(
6de8befe06fa505d116189b0bb97f6f417a2d6cb) after each leg. Runs on source —the tests import relative paths, so no
distis in the resolution and no rebuildleg is required.
The second leg is the one that matters beyond the usual: it proves the suite also
refuses a guard that just warns about everything, so the guard is measured to
DISCRIMINATE, not merely to fire.
Verification
Gate union run after the final commit, at
adb3c3ace:pnpm --filter '@object-ui/fields' type-check— clean (it runstsconfig.test.jsontoo, so the new test files are typed, not merely present)pnpm exec vitest run packages/fields/ packages/plugin-detail/ packages/plugin-form/— 308 files, 3860 tests, all passed. The two consumer packages are included
because NumberField's root changed from a bare input to a wrapped one.
check-control-bytes,check-changeset-presence,check-changeset-no-major,check-doc-links,check:doc-snippets(267/267 blocks judged, 0 failed),check:readme-exports,check:doc-types,check:doc-fences— all green.check:entry-guard,check:action-forward-parity,check:designer-field-key-parity,check:element-data-source-declaration,check:icon-record-names,check:phantom-deps,check:self-import,check:side-effects-array,check:spec-symbols,check:i18n-drift,check:i18n-dead-keys,check:skills-paths,check:shell-escape-residue,check:node-esm-load,check:vi-mock-specifiers,check:lint-coverage.check:eager-closureandcheck:sdui-registration-pinsexit 2 —PREREQUISITE NOT MET, they need an
apps/consolebuild this container has noreason to produce. Recorded as NOT MEASURED, not as green.
Lint — declared narrowing. Ran
eslint .overpackages/fields(201 filesjudged, read from
--format json) rather than the whole repo. The narrowingprovably excludes nothing relevant: eslint's own config declares its population
as
**/*.{ts,tsx}, so the doc and changeset changes are outside it entirely, andthe config sets no
projectService/project, so type-aware linting is off andthis diff cannot move the verdict on any file it did not touch. All 14 changed
.ts/.tsxfiles are insidepackages/fields. Result: 0 errors, and thenew suite carries 0 warnings after its
anycasts were collapsed into onestructural type.
Out of scope, filed rather than folded in
onBlur. It writesonBlurafter thetoDomPropsspread, so a host handler is overridden — pre-existing, and leftexactly as it was. No host in this repo passes one today (the data-table inline
editor uses a document-level pointerdown listener), so it is latent; repairing
it would be an unmeasured behaviour change outside this ruling.
errorslot. 字段 widget 的错误提示键:spec 声明error,objectui 渲染errorMessage(declared ≠ enforced) #3222 gave the othernumeric widgets one and this widget none, so a form validation failure cannot
mark it invalid. This PR writes
aria-invalidon it only when refused,precisely so it does not stamp
"false"over the value FormControl's Slothands down.
deliver) stays with the PM seat's own card, per the dispatch order. This PR
touches that suite only to correct the
badInputclaim and to move the sharedreadings into one module; it does not widen it.
Generated by Claude Code