diff --git a/packages/core/src/number-field/number-field-root.tsx b/packages/core/src/number-field/number-field-root.tsx index 4391afa7..e9fb7bb4 100644 --- a/packages/core/src/number-field/number-field-root.tsx +++ b/packages/core/src/number-field/number-field-root.tsx @@ -214,6 +214,15 @@ export function NumberFieldRoot( ? numberParser().parse(value ?? "") : Number(value ?? ""); + const isValidPartialValue = (value: string | number | undefined) => + local.format && typeof value !== "number" + ? numberParser().isValidPartialNumber( + value ?? "", + mergedProps.minValue, + mergedProps.maxValue, + ) + : !Number.isNaN(Number(value)); + const [value, setValue] = createControllableSignal({ value: () => local.value, defaultValue: () => local.defaultValue ?? local.rawValue, @@ -259,9 +268,13 @@ export function NumberFieldRoot( } const target = e.target as HTMLInputElement; + // cache the cursor position in case we need to update the input's value. + const cursorPosition = target.selectionStart; - if (e.inputType !== "insertText" || isAllowedInput(e.data || "")) { - setValue(target.value); + if (isValidPartialValue(target.value)) { + if (e.inputType !== "insertText" || isAllowedInput(e.data || "")) { + setValue(target.value); + } } // Unlike in React, inputs `value` can be out of sync with our value state. @@ -269,7 +282,12 @@ export function NumberFieldRoot( // typing on the input will change its internal `value`. // // To prevent this, we need to force the input `value` to be in sync with the text field value state. - target.value = String(value() ?? ""); + const v = value(); + if (v !== target.value) { + target.value = String(v ?? ""); + target.selectionStart = cursorPosition; + target.selectionEnd = cursorPosition; + } }; const context: NumberFieldContextValue = {