diff --git a/.changeset/gentle-owls-arrive.md b/.changeset/gentle-owls-arrive.md
new file mode 100644
index 0000000000..25fa7f3adf
--- /dev/null
+++ b/.changeset/gentle-owls-arrive.md
@@ -0,0 +1,5 @@
+---
+"@heroui/number-input": patch
+---
+
+fix backspace behavior with formatted numbers (#5712)
diff --git a/packages/components/number-input/__tests__/number-input.test.tsx b/packages/components/number-input/__tests__/number-input.test.tsx
index fb111f6e99..bb7eb18d82 100644
--- a/packages/components/number-input/__tests__/number-input.test.tsx
+++ b/packages/components/number-input/__tests__/number-input.test.tsx
@@ -592,5 +592,63 @@ describe("NumberInput with React Hook Form", () => {
await user.keyboard("1234");
});
});
+
+ describe("Backspace behavior with formatted numbers", () => {
+ it("should handle backspace when cursor is between first digit and comma", async () => {
+ const {container} = render(
+ ,
+ );
+
+ const input = container.querySelector("input[type='text']") as HTMLInputElement;
+
+ expect(input.value).toBe("1,234");
+
+ act(() => {
+ input.focus();
+ input.setSelectionRange(1, 1);
+ });
+
+ act(() => {
+ fireEvent.keyDown(input, {key: "Backspace", code: "Backspace"});
+ });
+
+ expect(input.value).toBe("234");
+ });
+
+ it("should handle backspace for other formatted number scenarios", async () => {
+ const {container} = render(
+ ,
+ );
+
+ const input = container.querySelector("input[type='text']") as HTMLInputElement;
+
+ expect(input.value).toBe("1,234,567");
+
+ act(() => {
+ input.focus();
+ input.setSelectionRange(5, 5);
+ });
+
+ act(() => {
+ fireEvent.keyDown(input, {key: "Backspace", code: "Backspace"});
+ });
+
+ expect(input.value).toBe("123,567");
+ });
+ });
});
});
diff --git a/packages/components/number-input/src/use-number-input.ts b/packages/components/number-input/src/use-number-input.ts
index fe9be28e00..3c4e5d84b9 100644
--- a/packages/components/number-input/src/use-number-input.ts
+++ b/packages/components/number-input/src/use-number-input.ts
@@ -242,7 +242,48 @@ export function useNumberInput(originalProps: UseNumberInputProps) {
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
+ const inputElement = e.currentTarget;
+ const {selectionStart, selectionEnd, value} = inputElement;
+ // locale-aware grouping separator
+ const nf = new Intl.NumberFormat(locale, {useGrouping: true});
+ const groupChar = nf.formatToParts(1000).find((p) => p.type === "group")?.value ?? ",";
+
+ // handle backspace when cursor is between a digit and the first group separator
+ // e.g. 1|,234 (en-US) or 1|.234 (de-DE) -> backspace removes the preceding digit if (
if (
+ e.key === "Backspace" &&
+ !originalProps.isReadOnly &&
+ !originalProps.isDisabled &&
+ selectionStart !== null &&
+ selectionEnd !== null &&
+ selectionStart === selectionEnd &&
+ selectionStart > 0 &&
+ value[selectionStart] === groupChar &&
+ value[selectionStart - 1] !== groupChar
+ ) {
+ e.preventDefault();
+ // e.g. 1,234 -> ,234
+ const newValue = value.slice(0, selectionStart - 1) + value.slice(selectionStart);
+ // e.g. ,234 -> 234
+ const cleanValue = newValue.replace(/[^\d.-]/g, "");
+
+ if (cleanValue === "" || cleanValue === "-") {
+ state.setInputValue("");
+ } else {
+ const numberValue = parseFloat(cleanValue);
+
+ if (!isNaN(numberValue)) {
+ state.setNumberValue(numberValue);
+ }
+ }
+
+ setTimeout(() => {
+ // set the new cursor position
+ const pos = Math.max(0, selectionStart - 1);
+
+ inputElement.setSelectionRange(pos, pos);
+ }, 0);
+ } else if (
e.key === "Escape" &&
inputValue &&
(isClearable || onClear) &&
@@ -252,7 +293,7 @@ export function useNumberInput(originalProps: UseNumberInputProps) {
onClear?.();
}
},
- [inputValue, state.setInputValue, onClear, isClearable, originalProps.isReadOnly],
+ [inputValue, state, onClear, isClearable, originalProps.isReadOnly],
);
const getBaseProps: PropGetter = useCallback(