Skip to content

Commit 746432c

Browse files
authored
fix(number-field): precision handling with floating point offsets and value snapping (#468)
1 parent cfb0a01 commit 746432c

File tree

2 files changed

+71
-5
lines changed

2 files changed

+71
-5
lines changed

packages/core/src/number-field/number-field-root.tsx

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import {
22
type ValidationState,
33
access,
44
createGenerateId,
5+
getPrecision,
56
mergeDefaultProps,
67
mergeRefs,
8+
snapValueToStep,
79
} from "@kobalte/utils";
810
import {
911
type JSX,
@@ -308,11 +310,33 @@ export function NumberFieldRoot<T extends ValidComponent = "div">(
308310
batch(() => {
309311
let newValue = rawValue;
310312

311-
if (rawValue % 1 === 0) {
312-
newValue += offset;
313-
} else {
314-
if (offset > 0) newValue = Math.ceil(newValue);
315-
else newValue = Math.floor(newValue);
313+
const operation = offset > 0 ? "+" : "-";
314+
const localStep = Math.abs(offset);
315+
// If there was no min or max provided, don't use our default values
316+
// use NaN instead to help with the calculation which will use 0
317+
// instead for a NaN value
318+
const min =
319+
props.minValue === undefined ? Number.NaN : context.minValue();
320+
const max =
321+
props.maxValue === undefined ? Number.NaN : context.maxValue();
322+
323+
// Try to snap the value to the nearest step
324+
newValue = snapValueToStep(rawValue, min, max, localStep);
325+
326+
// If the value didn't change in the direction we wanted to,
327+
// then add the step and snap that value
328+
if (
329+
!(
330+
(operation === "+" && newValue > rawValue) ||
331+
(operation === "-" && newValue < rawValue)
332+
)
333+
) {
334+
newValue = snapValueToStep(
335+
handleDecimalOperation(operation, rawValue, localStep),
336+
min,
337+
max,
338+
localStep,
339+
);
316340
}
317341

318342
context.setValue(newValue);
@@ -352,3 +376,34 @@ export function NumberFieldRoot<T extends ValidComponent = "div">(
352376
</FormControlContext.Provider>
353377
);
354378
}
379+
380+
function handleDecimalOperation(
381+
operator: "-" | "+",
382+
value1: number,
383+
value2: number,
384+
): number {
385+
let result = operator === "+" ? value1 + value2 : value1 - value2;
386+
if (
387+
Number.isFinite(value1) &&
388+
Number.isFinite(value2) &&
389+
(value2 % 1 !== 0 || value1 % 1 !== 0)
390+
) {
391+
const offsetPrecision = getPrecision(value2);
392+
const valuePrecision = getPrecision(value1);
393+
394+
const multiplier = 10 ** Math.max(offsetPrecision, valuePrecision);
395+
396+
const multipliedOffset = Math.round(value2 * multiplier);
397+
const multipliedValue = Math.round(value1 * multiplier);
398+
399+
const next =
400+
operator === "+"
401+
? multipliedValue + multipliedOffset
402+
: multipliedValue - multipliedOffset;
403+
404+
// Undo multiplier to get the new value
405+
result = next / multiplier;
406+
}
407+
408+
return result;
409+
}

packages/utils/src/number.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,14 @@ export function snapValueToStep(
5151

5252
return snappedValue;
5353
}
54+
55+
export const getPrecision = (n: number) => {
56+
let e = 1;
57+
let precision = 0;
58+
while (Math.round(n * e) / e !== n) {
59+
e *= 10;
60+
precision++;
61+
}
62+
63+
return precision;
64+
};

0 commit comments

Comments
 (0)