Skip to content

[number field] format.roundingMode is ignored when committing rounded values #4803

@elecdeer

Description

@elecdeer

Bug report

Current behavior

NumberField.Root does not consistently respect format.roundingMode.

The initial/controlled display formatting appears to use Intl.NumberFormat, but when the value is committed or validated, Base UI rounds the numeric value with Number.prototype.toFixed(). As a result, roundingMode is ignored for the committed value.

For example, with:

format={{ maximumFractionDigits: 2, roundingMode: 'floor' }}

typing 1.239 and blurring the input commits 1.24, even though Intl.NumberFormat with roundingMode: 'floor' formats the same value as 1.23.

Expected behavior

NumberField should respect format.roundingMode whenever it rounds a value according to the provided format options.

With maximumFractionDigits: 2 and roundingMode: 'floor', committing 1.239 should produce 1.23, not 1.24.

Reproducible example

The issue can be reproduced with the following test added to packages/react/src/number-field/input/NumberFieldInput.test.tsx:

it('should respect roundingMode when rounding to explicit maximumFractionDigits on blur', async () => {
  const onValueChange = vi.fn();

  function Controlled() {
    const [value, setValue] = React.useState<number | null>(null);

    return (
      <NumberField.Root
        value={value}
        onValueChange={(nextValue) => {
          onValueChange(nextValue);
          setValue(nextValue);
        }}
        format={{
          maximumFractionDigits: 2,
          roundingMode: 'floor',
        }}
        locale="en-US"
      >
        <NumberField.Input />
      </NumberField.Root>
    );
  }

  const { user } = await render(<Controlled />);
  const input = screen.getByRole('textbox');

  await act(async () => {
    input.focus();
  });

  await user.keyboard('1.239');
  fireEvent.blur(input);

  expect(onValueChange.mock.lastCall?.[0]).toBe(1.23);
  expect(input).toHaveValue('1.23');
});

Currently this fails because the committed value is 1.24.

Base UI version

v1.4.1

Which browser are you using?

Google Chrome 147.0.7727.139

options.roundingMode is supported by all major browsers.
https://caniuse.com/mdn-javascript_builtins_intl_numberformat_numberformat_options_parameter_options_roundingmode_parameter

Which OS are you using?

macOS

Which assistive tech are you using (if applicable)?

N/A

Additional context

This seems to come from internal rounding logic using toFixed() instead of Intl.NumberFormat semantics.

const maxFrac = formatOptions?.maximumFractionDigits;
const committed =
hasExplicitPrecision && typeof maxFrac === 'number'
? Number(parsedValue.toFixed(maxFrac))
: parsedValue;

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