Skip to content

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

Description

@michaldudak

Current behavior

parseNumber keeps only the last . and strips the rest (utils/parse.ts:190-193, "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:

parseNumber('1.2.3'); // 12.3

When such text arrives as raw input, the field shows what was entered while value and the hidden <input type="number"> hold the normalized number. The two stay divergent until blur, so a form submitted in between sends a number the user never saw.

<NumberField.Root name="n" defaultValue={5} onValueChange={console.log} />

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:

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) 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    component: number fieldChanges related to the number field component.type: bugIt doesn't behave as expected.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions