Skip to content

Commit b1fc4fc

Browse files
fix(NumberInput): fix floating-point precision issues (#18670)
Co-authored-by: Gururaj J <89023023+Gururajj77@users.noreply.github.com>
1 parent f11f809 commit b1fc4fc

File tree

2 files changed

+52
-17
lines changed

2 files changed

+52
-17
lines changed

packages/react/src/components/NumberInput/NumberInput.tsx

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ import { Add, Subtract } from '@carbon/icons-react';
99
import cx from 'classnames';
1010
import PropTypes from 'prop-types';
1111
import React, {
12+
FC,
13+
MouseEvent,
14+
ReactElement,
1215
ReactNode,
1316
useContext,
17+
useEffect,
1418
useRef,
1519
useState,
16-
useEffect,
17-
FC,
18-
ReactElement,
1920
} from 'react';
2021
import { useMergedRefs } from '../../internal/useMergedRefs';
2122
import { useNormalizedInputProps as normalize } from '../../internal/useNormalizedInputProps';
@@ -352,27 +353,37 @@ const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
352353

353354
const Icon = normalizedProps.icon as any;
354355

355-
function handleStepperClick(event, direction) {
356+
const getDecimalPlaces = (num: number) => {
357+
const parts = num.toString().split('.');
358+
359+
return parts[1] ? parts[1].length : 0;
360+
};
361+
362+
const handleStepperClick = (
363+
event: MouseEvent<HTMLButtonElement>,
364+
direction: 'up' | 'down'
365+
) => {
356366
if (inputRef.current) {
357367
const currentValue = Number(inputRef.current.value);
358-
let newValue =
368+
const rawValue =
359369
direction === 'up' ? currentValue + step : currentValue - step;
360-
if (min !== undefined) {
361-
newValue = Math.max(newValue, min);
362-
}
363-
if (max !== undefined) {
364-
newValue = Math.min(newValue, max);
365-
}
366-
const currentInputValue = inputRef.current
367-
? inputRef.current.value
368-
: '';
370+
const precision = Math.max(
371+
getDecimalPlaces(currentValue),
372+
getDecimalPlaces(step)
373+
);
374+
const floatValue = parseFloat(rawValue.toFixed(precision));
375+
const newValue =
376+
typeof min !== 'undefined' && typeof max !== 'undefined'
377+
? Math.min(Math.max(floatValue, min), max)
378+
: floatValue;
369379
const state = {
370380
value:
371-
allowEmpty && currentInputValue === '' && step === 0
381+
allowEmpty && inputRef.current.value === '' && step === 0
372382
? ''
373383
: newValue,
374-
direction: direction,
384+
direction,
375385
};
386+
376387
setValue(state.value);
377388

378389
if (onChange) {
@@ -383,7 +394,7 @@ const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
383394
onClick(event, state);
384395
}
385396
}
386-
}
397+
};
387398

388399
// AILabel always size `mini`
389400
let normalizedDecorator = React.isValidElement(slug ?? decorator)

packages/react/src/components/NumberInput/__tests__/NumberInput-test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,4 +449,28 @@ describe('NumberInput', () => {
449449
})
450450
);
451451
});
452+
453+
it('should increment and decrement decimal numbers without floating-point precision errors', async () => {
454+
render(
455+
<NumberInput
456+
label="NumberInput label"
457+
id="number-input"
458+
min={0}
459+
value={15.01}
460+
step={1}
461+
max={100}
462+
translateWithId={translateWithId}
463+
/>
464+
);
465+
466+
const input = screen.getByLabelText('NumberInput label');
467+
468+
expect(input).toHaveValue(15.01);
469+
470+
await userEvent.click(screen.getByLabelText('increment'));
471+
expect(input).toHaveValue(16.01);
472+
473+
await userEvent.click(screen.getByLabelText('decrement'));
474+
expect(input).toHaveValue(15.01);
475+
});
452476
});

0 commit comments

Comments
 (0)