mui / mui/base-ui

[number-field] Input displays one number while submitting another for implausibly formatted text

Open
#5,424 0 comments 0 reactions 0 assignees View on GitHub
component: number field type: bug
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.