diff --git a/.changeset/grumpy-pandas-rescue.md b/.changeset/grumpy-pandas-rescue.md new file mode 100644 index 0000000000..d2aae6cfb4 --- /dev/null +++ b/.changeset/grumpy-pandas-rescue.md @@ -0,0 +1,6 @@ +--- +"@heroui/input": patch +"@heroui/number-input": patch +--- + +add missing logic to handle esc key to clear input / number-input value (#4850) diff --git a/packages/components/input/__tests__/input.test.tsx b/packages/components/input/__tests__/input.test.tsx index 4079688268..8493de8191 100644 --- a/packages/components/input/__tests__/input.test.tsx +++ b/packages/components/input/__tests__/input.test.tsx @@ -300,6 +300,66 @@ describe("Input", () => { expect(onClear).toHaveBeenCalledTimes(0); }); + + it("should clear value when isClearable and pressing ESC key", async () => { + const onClear = jest.fn(); + const defaultValue = "test value"; + + const {getByRole} = render(); + + const input = getByRole("textbox") as HTMLInputElement; + + expect(input.value).toBe(defaultValue); + + fireEvent.keyDown(input, {key: "Escape"}); + + expect(input.value).toBe(""); + + expect(onClear).toHaveBeenCalledTimes(1); + }); + + it("should not clear value when pressing ESC key if input is empty", () => { + const onClear = jest.fn(); + + const {getByRole} = render(); + + const input = getByRole("textbox"); + + fireEvent.keyDown(input, {key: "Escape"}); + + expect(onClear).not.toHaveBeenCalled(); + }); + + it("should not clear value when pressing ESC key if input is isClearable", () => { + const defaultValue = "test value"; + + const {getByRole} = render(); + + const input = getByRole("textbox") as HTMLInputElement; + + fireEvent.keyDown(input, {key: "Escape"}); + + expect(input.value).toBe("test value"); + }); + + it("should not clear value when pressing ESC key if input is readonly", () => { + const onClear = jest.fn(); + const defaultValue = "test value"; + + const {getByRole} = render( + , + ); + + const input = getByRole("textbox") as HTMLInputElement; + + expect(input.value).toBe(defaultValue); + + fireEvent.keyDown(input, {key: "Escape"}); + + expect(input.value).toBe(defaultValue); + + expect(onClear).not.toHaveBeenCalled(); + }); }); describe("Input with React Hook Form", () => { diff --git a/packages/components/input/src/use-input.ts b/packages/components/input/src/use-input.ts index 96ec39906b..7918bba877 100644 --- a/packages/components/input/src/use-input.ts +++ b/packages/components/input/src/use-input.ts @@ -352,6 +352,21 @@ export function useInput) => { + if ( + e.key === "Escape" && + inputValue && + (isClearable || onClear) && + !originalProps.isReadOnly + ) { + setInputValue(""); + onClear?.(); + } + }, + [inputValue, setInputValue, onClear, isClearable, originalProps.isReadOnly], + ); + const getInputProps: PropGetter = useCallback( (props = {}) => { return { @@ -375,6 +390,7 @@ export function useInput { expect(stepperButton).toBeNull(); }); + it("should clear value when isClearable and pressing ESC key", async () => { + const onClear = jest.fn(); + const defaultValue = 12; + + const {container} = render( + , + ); + + const input = container.querySelector("input") as HTMLInputElement; + + expect(input.value).toBe(defaultValue.toString()); + + fireEvent.keyDown(input, {key: "Escape"}); + expect(input.value).toBe(""); + expect(onClear).toHaveBeenCalledTimes(1); + }); + + it("should not clear value when pressing ESC key if input is empty", () => { + const onClear = jest.fn(); + + const {container} = render(); + + const input = container.querySelector("input") as HTMLInputElement; + + fireEvent.keyDown(input, {key: "Escape"}); + expect(onClear).not.toHaveBeenCalled(); + }); + + it("should not clear value when pressing ESC key without isClearable", () => { + const defaultValue = 12; + + const {container} = render(); + + const input = container.querySelector("input") as HTMLInputElement; + + expect(input.value).toBe(defaultValue.toString()); + + fireEvent.keyDown(input, {key: "Escape"}); + expect(input.value).toBe(defaultValue.toString()); + }); + + it("should not clear value when pressing ESC key if input is readonly", () => { + const onClear = jest.fn(); + const defaultValue = 42; + + const {container} = render(); + + const input = container.querySelector("input") as HTMLInputElement; + + expect(input.value).toBe(defaultValue.toString()); + + fireEvent.keyDown(input, {key: "Escape"}); + + expect(input.value).toBe(defaultValue.toString()); + expect(onClear).not.toHaveBeenCalled(); + }); + it("should emit onChange", async () => { const onChange = jest.fn(); diff --git a/packages/components/number-input/src/use-number-input.ts b/packages/components/number-input/src/use-number-input.ts index 8543a07370..e8fa6fa3c8 100644 --- a/packages/components/number-input/src/use-number-input.ts +++ b/packages/components/number-input/src/use-number-input.ts @@ -239,6 +239,21 @@ export function useNumberInput(originalProps: UseNumberInputProps) { [objectToDeps(variantProps), isInvalid, isClearable, hasStartContent, disableAnimation], ); + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if ( + e.key === "Escape" && + inputValue && + (isClearable || onClear) && + !originalProps.isReadOnly + ) { + state.setInputValue(""); + onClear?.(); + } + }, + [inputValue, state.setInputValue, onClear, isClearable, originalProps.isReadOnly], + ); + const getBaseProps: PropGetter = useCallback( (props = {}) => { return { @@ -324,6 +339,7 @@ export function useNumberInput(originalProps: UseNumberInputProps) { ), "aria-readonly": dataAttr(originalProps.isReadOnly), onChange: chain(inputProps.onChange, onChange), + onKeyDown: chain(inputProps.onKeyDown, props.onKeyDown, handleKeyDown), ref: domRef, }; }, @@ -339,6 +355,7 @@ export function useNumberInput(originalProps: UseNumberInputProps) { originalProps.isReadOnly, originalProps.isRequired, onChange, + handleKeyDown, ], );