From 645314954309f2bffa2b387f68c07d63d09fed43 Mon Sep 17 00:00:00 2001 From: LinYongLu <88522000+linyonglu@users.noreply.github.com> Date: Fri, 28 Feb 2025 12:44:32 +0800 Subject: [PATCH] fix(input): esc key to clear input value (#4892) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: theme generator credits * fix: blog date and spinner default variant * fix: #4850 Solve Pressing ESC doesn't clear input value * fix: #4850 code review change * fix: undo changes in apps/docs/content/blog/v2.7.0.mdx and add a test case for my changes * fix: run through the test cases successfully * fix: change md content * fix: using isClearable not clear the value * fix: add number-input clearable esc clear * fix: edit review problem * fix: delete unless file * chore(changeset): update changeset * fix: add inputProps.onKeyDown * fix: pressing ESC key in a read-only input not clear --------- Co-authored-by: Junior Garcia Co-authored-by: աӄա --- .changeset/grumpy-pandas-rescue.md | 6 ++ .../components/input/__tests__/input.test.tsx | 60 +++++++++++++++++++ packages/components/input/src/use-input.ts | 17 ++++++ .../__tests__/number-input.test.tsx | 57 ++++++++++++++++++ .../number-input/src/use-number-input.ts | 17 ++++++ 5 files changed, 157 insertions(+) create mode 100644 .changeset/grumpy-pandas-rescue.md 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, ], );