fix(input): esc key to clear input value (#4892)

* 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 <jrgarciadev@gmail.com>
Co-authored-by: աӄա <wingkwong.code@gmail.com>
This commit is contained in:
LinYongLu 2025-02-28 12:44:32 +08:00 committed by GitHub
parent ff8c9b3fec
commit 6453149543
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 157 additions and 0 deletions

View File

@ -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)

View File

@ -300,6 +300,66 @@ describe("Input", () => {
expect(onClear).toHaveBeenCalledTimes(0); 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", () => { describe("Input with React Hook Form", () => {

View File

@ -352,6 +352,21 @@ export function useInput<T extends HTMLInputElement | HTMLTextAreaElement = HTML
[slots, isLabelHovered, labelProps, classNames?.label], [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( const getInputProps: PropGetter = useCallback(
(props = {}) => { (props = {}) => {
return { return {
@ -375,6 +390,7 @@ export function useInput<T extends HTMLInputElement | HTMLTextAreaElement = HTML
), ),
"aria-readonly": dataAttr(originalProps.isReadOnly), "aria-readonly": dataAttr(originalProps.isReadOnly),
onChange: chain(inputProps.onChange, onChange), onChange: chain(inputProps.onChange, onChange),
onKeyDown: chain(inputProps.onKeyDown, props.onKeyDown, handleKeyDown),
ref: domRef, ref: domRef,
}; };
}, },
@ -392,6 +408,7 @@ export function useInput<T extends HTMLInputElement | HTMLTextAreaElement = HTML
originalProps.isReadOnly, originalProps.isReadOnly,
originalProps.isRequired, originalProps.isRequired,
onChange, onChange,
handleKeyDown,
], ],
); );

View File

@ -241,6 +241,63 @@ describe("NumberInput", () => {
expect(stepperButton).toBeNull(); 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 () => { it("should emit onChange", async () => {
const onChange = jest.fn(); const onChange = jest.fn();

View File

@ -239,6 +239,21 @@ export function useNumberInput(originalProps: UseNumberInputProps) {
[objectToDeps(variantProps), isInvalid, isClearable, hasStartContent, disableAnimation], [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( const getBaseProps: PropGetter = useCallback(
(props = {}) => { (props = {}) => {
return { return {
@ -324,6 +339,7 @@ export function useNumberInput(originalProps: UseNumberInputProps) {
), ),
"aria-readonly": dataAttr(originalProps.isReadOnly), "aria-readonly": dataAttr(originalProps.isReadOnly),
onChange: chain(inputProps.onChange, onChange), onChange: chain(inputProps.onChange, onChange),
onKeyDown: chain(inputProps.onKeyDown, props.onKeyDown, handleKeyDown),
ref: domRef, ref: domRef,
}; };
}, },
@ -339,6 +355,7 @@ export function useNumberInput(originalProps: UseNumberInputProps) {
originalProps.isReadOnly, originalProps.isReadOnly,
originalProps.isRequired, originalProps.isRequired,
onChange, onChange,
handleKeyDown,
], ],
); );