[number-field] Input displays one number while submitting another for implausibly formatted text
- Dominant language
- TypeScript
- Stars
- 10.9k
- Forks
- 543
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 101
Description
## Current behavior
`parseNumber` keeps only the last `.` and strips the rest ([`utils/parse.ts:190-193`](https://github.com/mui/base-ui/blob/master/packages/react/src/number-field/utils/parse.ts#L190-L193), "Mixed-locale safety"). That is deliberate — it lets a European-formatted paste like `1.234.567.89` resolve to `1234567.89` in a US-locale field, pinned by `utils/parse.test.ts:193-204`. But the same rule also accepts strings that are not a plausible number in any locale:
```js
parseNumber('1.2.3'); // 12.3
```
When such text arrives as raw input, the field shows what was entered while `value` and the hidden `` hold the normalized number. The two stay divergent until blur, so a form submitted in between sends a number the user never saw.
```tsx
```
Drop, IME-compose, or autofill `1.2.3` into the input:
- visible input: `1.2.3`
- `onValueChange`: `12.3`
- hidden submitted input: `12.3`
Typing is unaffected — `onKeyDown` blocks a second decimal separator — so this only reaches non-keystroke text entry. It is also reachable through `actionsRef.current.setInputValue('1.2.3')` (API added in #5421), which routes through the same validation.
`NumberFieldRoot.test.tsx:2606` covers the mixed-locale case but asserts only the resulting value, never the visible text, which is why the divergence has gone unnoticed.
Related leniencies from the same call, which may or may not be in scope:
- `parseNumber('1-2')` → `1` (`parseFloat` stops at the first invalid character)
- `parseNumber('5-')` → `-5` (deliberate trailing-sign / accounting support — should stay)
## Expected behavior
Text that resolves to a number the user did not enter should either be rejected, or normalized in the visible input, rather than leaving the display and the submitted value silently divergent.
## Reproducible example
No CodeSandbox — reproduced directly against `master` in the repo's own test environment:
```js
fireEvent.change(input, { target: { value: '1.2.3' } });
// visible "1.2.3", onValueChange(12.3), hidden input value "12.3"
```
## Base UI version
`master` as of 1a2ca3c. Not a regression — the normalization predates #5421.
## Which browser are you using?
All. Confirmed in jsdom and Chromium.
## Which OS are you using?
All.
## Additional context
Two places a fix could go:
1. Tighten `parseNumber` to reject implausible grouping, for example requiring interior dot-separated runs to be 3 digits. This must keep `utils/parse.test.ts:193-204` green, which pins `1.234.567.89`, and `1.234.567,89` in both `fr-FR` and `en-US`.
2. Add a structural check next to `isValidInputString` ([`utils/parse.ts:75`](https://github.com/mui/base-ui/blob/master/packages/react/src/number-field/utils/parse.ts#L75)) and call it from both raw-text entry points: `NumberFieldInput`'s `onChange`, and the `setInputValue` action in `NumberFieldRoot.tsx`. This leaves `parseNumber` lenient for blur and paste normalization.
Option 2 looks lower risk. Either way the check belongs on **both** entry points — they share `isValidInputString` specifically so typed and imperative text cannot diverge.
Done when:
- Implausible text is either rejected or normalized in the visible input, never silently divergent from `value` and the hidden input.
- Both the change-event path and the `actionsRef` action are covered by tests.
- The existing mixed-locale cases in `utils/parse.test.ts` still pass.
- `pnpm test:jsdom NumberField --no-watch` and `pnpm test:chromium NumberField --no-watch` are green.
Contributor guide
Research direction
Start with packages/react/src/number-field/utils/parse.ts, especially isValidInputString and parseNumber, then trace NumberFieldInput's onChange and the setInputValue action in NumberFieldRoot.tsx. Review NumberFieldRoot.test.tsx:2606 and utils/parse.test.ts:193-204, and run the jsdom and Chromium NumberField commands; done means both raw-text paths avoid visible/value divergence while mixed-locale cases remain green.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend, testing-qa
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100