Skip to content

Commit

Permalink
fix(input): esc key to clear input value (#4892)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: աӄա <[email protected]>
  • Loading branch information
3 people authored Feb 28, 2025
1 parent ff8c9b3 commit 6453149
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .changeset/grumpy-pandas-rescue.md
Original file line number Diff line number Diff line change
@@ -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)
60 changes: 60 additions & 0 deletions packages/components/input/__tests__/input.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Input isClearable defaultValue={defaultValue} onClear={onClear} />);

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(<Input isClearable defaultValue="" onClear={onClear} />);

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(<Input defaultValue={defaultValue} />);

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(
<Input isClearable isReadOnly defaultValue={defaultValue} onClear={onClear} />,
);

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", () => {
Expand Down
17 changes: 17 additions & 0 deletions packages/components/input/src/use-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,21 @@ export function useInput<T extends HTMLInputElement | HTMLTextAreaElement = HTML
[slots, isLabelHovered, labelProps, classNames?.label],
);

const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (
e.key === "Escape" &&
inputValue &&
(isClearable || onClear) &&
!originalProps.isReadOnly
) {
setInputValue("");
onClear?.();
}
},
[inputValue, setInputValue, onClear, isClearable, originalProps.isReadOnly],
);

const getInputProps: PropGetter = useCallback(
(props = {}) => {
return {
Expand All @@ -375,6 +390,7 @@ export function useInput<T extends HTMLInputElement | HTMLTextAreaElement = HTML
),
"aria-readonly": dataAttr(originalProps.isReadOnly),
onChange: chain(inputProps.onChange, onChange),
onKeyDown: chain(inputProps.onKeyDown, props.onKeyDown, handleKeyDown),
ref: domRef,
};
},
Expand All @@ -392,6 +408,7 @@ export function useInput<T extends HTMLInputElement | HTMLTextAreaElement = HTML
originalProps.isReadOnly,
originalProps.isRequired,
onChange,
handleKeyDown,
],
);

Expand Down
57 changes: 57 additions & 0 deletions packages/components/number-input/__tests__/number-input.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,63 @@ describe("NumberInput", () => {
expect(stepperButton).toBeNull();
});

it("should clear value when isClearable and pressing ESC key", async () => {
const onClear = jest.fn();
const defaultValue = 12;

const {container} = render(
<NumberInput isClearable defaultValue={defaultValue} onClear={onClear} />,
);

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(<NumberInput isClearable onClear={onClear} />);

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(<NumberInput defaultValue={defaultValue} />);

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(<NumberInput isReadOnly defaultValue={defaultValue} />);

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();

Expand Down
17 changes: 17 additions & 0 deletions packages/components/number-input/src/use-number-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,21 @@ export function useNumberInput(originalProps: UseNumberInputProps) {
[objectToDeps(variantProps), isInvalid, isClearable, hasStartContent, disableAnimation],
);

const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
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 {
Expand Down Expand Up @@ -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,
};
},
Expand All @@ -339,6 +355,7 @@ export function useNumberInput(originalProps: UseNumberInputProps) {
originalProps.isReadOnly,
originalProps.isRequired,
onChange,
handleKeyDown,
],
);

Expand Down

0 comments on commit 6453149

Please sign in to comment.