Skip to content

Commit 5533a98

Browse files
fix(InputNumber): fix decimal value on spin (#8382) (#8392)
Amended to factor in exponential numbers and added tests with formatting and linting - take 2
1 parent 24b94b5 commit 5533a98

2 files changed

Lines changed: 150 additions & 2 deletions

File tree

components/lib/inputnumber/InputNumber.js

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,55 @@ 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+
260+
return Math.round(sum * fallbackFactor) / fallbackFactor;
261+
}
262+
263+
// Correct integer math → avoids floating point errors
264+
return Math.round(base * factor + increment * factor) / factor;
218265
};
219266

220267
const repeat = (event, interval, dir) => {
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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+
36+
expect(Number(input.value)).toBeCloseTo(tiny);
37+
38+
rerender(<InputNumber value={tiny * 2} mode="decimal" maxFractionDigits={15} />);
39+
expect(Number(input.value)).toBeCloseTo(2e-7);
40+
});
41+
42+
test('handles large decimals (many fraction digits) within precision cap', () => {
43+
const num = 0.123456789012345;
44+
45+
const { container } = render(<InputNumber value={num} mode="decimal" maxFractionDigits={15} />);
46+
47+
const input = container.querySelector('input');
48+
49+
expect(Number(input.value)).toBeCloseTo(num, 12);
50+
});
51+
52+
test('caps precision at safe 15 decimals', () => {
53+
const tooPrecise = 0.1234567890123456;
54+
55+
const { container } = render(<InputNumber value={tooPrecise} mode="decimal" maxFractionDigits={15} />);
56+
57+
const input = container.querySelector('input');
58+
59+
expect(Number(input.value)).toBeCloseTo(0.123456789012346);
60+
});
61+
62+
test('small step accumulation remains precise', async () => {
63+
const { container } = render(<InputNumber step={0.01} showButtons mode="decimal" />);
64+
65+
const input = container.querySelector('input');
66+
const { inc } = getButtons(container);
67+
68+
for (let i = 0; i < 11; i++) await userEvent.click(inc);
69+
70+
expect(Number(input.value)).toBeCloseTo(0.11);
71+
});
72+
73+
test('invalid values (NaN, Infinity) do not break controlled value', () => {
74+
const { container, rerender } = render(<InputNumber value={0} mode="decimal" />);
75+
76+
const input = container.querySelector('input');
77+
78+
rerender(<InputNumber value={Infinity} mode="decimal" />);
79+
expect(Number.isFinite(input.value)).toBe(false);
80+
81+
rerender(<InputNumber value={NaN} mode="decimal" />);
82+
expect(Number.isNaN(Number(input.value))).toBe(true);
83+
});
84+
85+
test('step works after setting initial value programmatically', async () => {
86+
const { container, rerender } = render(<InputNumber value={0.5} step={0.25} showButtons mode="decimal" />);
87+
88+
const input = container.querySelector('input');
89+
const { inc, dec } = getButtons(container);
90+
91+
expect(Number(input.value)).toBeCloseTo(0.5);
92+
93+
await userEvent.click(inc);
94+
expect(Number(input.value)).toBeCloseTo(0.75);
95+
96+
rerender(<InputNumber value={1.0} step={0.25} showButtons mode="decimal" />);
97+
98+
await userEvent.click(dec);
99+
expect(Number(input.value)).toBeCloseTo(0.75);
100+
});
101+
});

0 commit comments

Comments
 (0)