fix(number-input): backspace behavior with formatted numbers (#5719)

* fix(number-input): backspace behavior with formatted numbers

* refactor(number-input): handle locale separator

* fix(number-input): set number input value logic
This commit is contained in:
WK 2025-10-04 10:16:56 +08:00 committed by GitHub
parent f13c6875db
commit 736293b8a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 105 additions and 1 deletions

View File

@ -0,0 +1,5 @@
---
"@heroui/number-input": patch
---
fix backspace behavior with formatted numbers (#5712)

View File

@ -621,5 +621,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(
<NumberInput
defaultValue={1234}
formatOptions={{
style: "decimal",
useGrouping: true,
}}
label="test number input"
/>,
);
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(
<NumberInput
defaultValue={1234567}
formatOptions={{
style: "decimal",
useGrouping: true,
}}
label="test number input"
/>,
);
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");
});
});
});
});

View File

@ -242,7 +242,48 @@ export function useNumberInput(originalProps: UseNumberInputProps) {
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
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(