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,
],
);