Skip to content

Commit 29e0d41

Browse files
committed
fix(InputNumber): fix decimal value on spin (#8382)
Amended to factor in exponential numbers and added tests with npm format
1 parent f237f28 commit 29e0d41

2 files changed

Lines changed: 147 additions & 2 deletions

File tree

components/lib/inputnumber/InputNumber.js

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,54 @@ export const InputNumber = React.memo(
213213
return null;
214214
};
215215

216-
const addWithPrecision = (base, increment, precision = 10) => {
217-
return Math.round((base + increment) * precision) / precision;
216+
function countDecimals(value) {
217+
if (!isFinite(value)) return 0;
218+
219+
const s = String(value);
220+
221+
// handle exponential notation: e.g. "1e-7"
222+
if (s.toLowerCase().includes('e')) {
223+
const [mantissa, expStr] = s.split('e');
224+
const exp = parseInt(expStr, 10);
225+
const decimalPart = (mantissa.split('.')[1] || '').length;
226+
227+
// If exponent is negative, decimals increase
228+
if (exp < 0) return decimalPart + Math.abs(exp);
229+
230+
// If exponent is positive, decimals shrink
231+
return Math.max(0, decimalPart - exp);
232+
}
233+
234+
// normal decimal
235+
return (s.split('.')[1] || '').length;
236+
}
237+
238+
const addWithPrecision = (base, increment) => {
239+
base = Number(base);
240+
increment = Number(increment);
241+
242+
// invalid inputs → return NaN cleanly
243+
if (!isFinite(base) || !isFinite(increment)) return NaN;
244+
245+
const baseDec = countDecimals(base);
246+
const incDec = countDecimals(increment);
247+
248+
// Choose required decimal precision but cap so factor remains safe
249+
const decimals = Math.min(Math.max(baseDec, incDec), 15);
250+
const factor = Math.pow(10, decimals);
251+
252+
const maxSafe = Number.MAX_SAFE_INTEGER;
253+
254+
// avoid unsafe multiplication
255+
if (Math.abs(base) * factor > maxSafe || Math.abs(increment) * factor > maxSafe) {
256+
const sum = base + increment;
257+
// fallback to a safe rounding
258+
const fallbackFactor = Math.pow(10, 15);
259+
return Math.round(sum * fallbackFactor) / fallbackFactor;
260+
}
261+
262+
// Correct integer math → avoids floating point errors
263+
return Math.round(base * factor + increment * factor) / factor;
218264
};
219265

220266
const repeat = (event, interval, dir) => {
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import '@testing-library/jest-dom';
2+
import { render, fireEvent } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import React from 'react';
5+
import { InputNumber } from './InputNumber';
6+
7+
function getButtons(container) {
8+
const inc = container.querySelector('[data-pc-section="incrementbutton"]') || container.querySelector('.p-inputnumber-button-up');
9+
10+
const dec = container.querySelector('[data-pc-section="decrementbutton"]') || container.querySelector('.p-inputnumber-button-down');
11+
12+
return { inc, dec };
13+
}
14+
15+
describe('InputNumber decimal precision and stepping', () => {
16+
test('increments correctly with step=0.25', async () => {
17+
const { container } = render(<InputNumber step={0.25} showButtons mode="decimal" />);
18+
19+
const input = container.querySelector('input');
20+
const { inc } = getButtons(container);
21+
22+
await userEvent.click(inc);
23+
expect(Number(input.value)).toBeCloseTo(0.25);
24+
25+
await userEvent.click(inc);
26+
expect(Number(input.value)).toBeCloseTo(0.5);
27+
});
28+
29+
test('handles scientific notation by setting value prop', () => {
30+
const tiny = 1e-7;
31+
32+
const { container, rerender } = render(<InputNumber value={tiny} mode="decimal" />);
33+
34+
const input = container.querySelector('input');
35+
expect(Number(input.value)).toBeCloseTo(tiny);
36+
37+
rerender(<InputNumber value={tiny * 2} mode="decimal" maxFractionDigits={15} />);
38+
expect(Number(input.value)).toBeCloseTo(2e-7);
39+
});
40+
41+
test('handles large decimals (many fraction digits) within precision cap', () => {
42+
const num = 0.123456789012345;
43+
44+
const { container } = render(<InputNumber value={num} mode="decimal" maxFractionDigits={15} />);
45+
46+
const input = container.querySelector('input');
47+
expect(Number(input.value)).toBeCloseTo(num, 12);
48+
});
49+
50+
test('caps precision at safe 15 decimals', () => {
51+
const tooPrecise = 0.1234567890123456789;
52+
53+
const { container } = render(<InputNumber value={tooPrecise} mode="decimal" maxFractionDigits={15} />);
54+
55+
const input = container.querySelector('input');
56+
57+
expect(Number(input.value)).toBeCloseTo(0.123456789012346);
58+
});
59+
60+
test('small step accumulation remains precise', async () => {
61+
const { container } = render(<InputNumber step={0.01} showButtons mode="decimal" />);
62+
63+
const input = container.querySelector('input');
64+
const { inc } = getButtons(container);
65+
66+
for (let i = 0; i < 11; i++) await userEvent.click(inc);
67+
68+
expect(Number(input.value)).toBeCloseTo(0.11);
69+
});
70+
71+
test('invalid values (NaN, Infinity) do not break controlled value', () => {
72+
const { container, rerender } = render(<InputNumber value={0} mode="decimal" />);
73+
74+
const input = container.querySelector('input');
75+
76+
rerender(<InputNumber value={Infinity} mode="decimal" />);
77+
expect(Number.isFinite(input.value)).toBe(false);
78+
79+
rerender(<InputNumber value={NaN} mode="decimal" />);
80+
expect(Number.isNaN(Number(input.value))).toBe(true);
81+
});
82+
83+
test('step works after setting initial value programmatically', async () => {
84+
const { container, rerender } = render(<InputNumber value={0.5} step={0.25} showButtons mode="decimal" />);
85+
86+
const input = container.querySelector('input');
87+
const { inc, dec } = getButtons(container);
88+
89+
expect(Number(input.value)).toBeCloseTo(0.5);
90+
91+
await userEvent.click(inc);
92+
expect(Number(input.value)).toBeCloseTo(0.75);
93+
94+
rerender(<InputNumber value={1.0} step={0.25} showButtons mode="decimal" />);
95+
96+
await userEvent.click(dec);
97+
expect(Number(input.value)).toBeCloseTo(0.75);
98+
});
99+
});

0 commit comments

Comments
 (0)