diff --git a/packages/components/autocomplete/__tests__/autocomplete.test.tsx b/packages/components/autocomplete/__tests__/autocomplete.test.tsx
index 1c4c47103f..f9b166f245 100644
--- a/packages/components/autocomplete/__tests__/autocomplete.test.tsx
+++ b/packages/components/autocomplete/__tests__/autocomplete.test.tsx
@@ -1,6 +1,7 @@
import * as React from "react";
-import {act, render} from "@testing-library/react";
+import {render, renderHook, act} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
+import {useForm} from "react-hook-form";
import {Autocomplete, AutocompleteItem, AutocompleteSection} from "../src";
import {Modal, ModalContent, ModalBody, ModalHeader, ModalFooter} from "../../modal/src";
@@ -220,3 +221,105 @@ describe("Autocomplete", () => {
expect(autocomplete).toHaveAttribute("aria-expanded", "false");
});
});
+
+describe("Autocomplete with React Hook Form", () => {
+ let autocomplete1: HTMLInputElement;
+ let autocomplete2: HTMLInputElement;
+ let autocomplete3: HTMLInputElement;
+ let submitButton: HTMLButtonElement;
+ let wrapper: any;
+ let onSubmit: () => void;
+
+ beforeEach(() => {
+ const {result} = renderHook(() =>
+ useForm({
+ defaultValues: {
+ withDefaultValue: "cat",
+ withoutDefaultValue: "",
+ requiredField: "",
+ },
+ }),
+ );
+
+ const {
+ handleSubmit,
+ register,
+ formState: {errors},
+ } = result.current;
+
+ onSubmit = jest.fn();
+
+ wrapper = render(
+
,
+ );
+
+ autocomplete1 = wrapper.getByTestId("autocomplete-1");
+ autocomplete2 = wrapper.getByTestId("autocomplete-2");
+ autocomplete3 = wrapper.getByTestId("autocomplete-3");
+ submitButton = wrapper.getByTestId("submit-button");
+ });
+
+ it("should work with defaultValues", () => {
+ expect(autocomplete1).toHaveValue("Cat");
+ expect(autocomplete2).toHaveValue("");
+ expect(autocomplete3).toHaveValue("");
+ });
+
+ it("should not submit form when required field is empty", async () => {
+ const user = userEvent.setup();
+
+ await user.click(submitButton);
+
+ expect(onSubmit).toHaveBeenCalledTimes(0);
+ });
+
+ it("should submit form when required field is not empty", async () => {
+ const user = userEvent.setup();
+
+ await user.click(autocomplete3);
+
+ expect(autocomplete3).toHaveAttribute("aria-expanded", "true");
+
+ let listboxItems = wrapper.getAllByRole("option");
+
+ await user.click(listboxItems[1]);
+
+ expect(autocomplete3).toHaveValue("Dog");
+
+ await user.click(submitButton);
+
+ expect(onSubmit).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/components/autocomplete/package.json b/packages/components/autocomplete/package.json
index ed7631e180..9532726636 100644
--- a/packages/components/autocomplete/package.json
+++ b/packages/components/autocomplete/package.json
@@ -74,7 +74,8 @@
"framer-motion": "^11.0.28",
"clean-package": "2.2.0",
"react": "^18.0.0",
- "react-dom": "^18.0.0"
+ "react-dom": "^18.0.0",
+ "react-hook-form": "^7.51.3"
},
"clean-package": "../../../clean-package.config.json"
}
diff --git a/packages/components/autocomplete/stories/autocomplete.stories.tsx b/packages/components/autocomplete/stories/autocomplete.stories.tsx
index a3b422bbcc..4ea9dae2db 100644
--- a/packages/components/autocomplete/stories/autocomplete.stories.tsx
+++ b/packages/components/autocomplete/stories/autocomplete.stories.tsx
@@ -2,6 +2,7 @@ import type {ValidationResult} from "@react-types/shared";
import React, {Key} from "react";
import {Meta} from "@storybook/react";
+import {useForm} from "react-hook-form";
import {autocomplete, input, button} from "@nextui-org/theme";
import {
Pokemon,
@@ -686,6 +687,45 @@ const CustomStylesWithCustomItemsTemplate = ({color, ...args}: AutocompleteProps
);
};
+const WithReactHookFormTemplate = (args: AutocompleteProps) => {
+ const {
+ register,
+ formState: {errors},
+ handleSubmit,
+ } = useForm({
+ defaultValues: {
+ withDefaultValue: "cat",
+ withoutDefaultValue: "",
+ requiredField: "",
+ },
+ });
+
+ const onSubmit = (data: any) => {
+ // eslint-disable-next-line no-console
+ console.log(data);
+ alert("Submitted value: " + JSON.stringify(data));
+ };
+
+ return (
+
+ );
+};
+
export const Default = {
render: Template,
args: {
@@ -733,15 +773,6 @@ export const DisabledOptions = {
},
};
-export const WithDescription = {
- render: MirrorTemplate,
-
- args: {
- ...defaultProps,
- description: "Select your favorite animal",
- },
-};
-
export const LabelPlacement = {
render: LabelPlacementTemplate,
@@ -782,6 +813,27 @@ export const EndContent = {
},
};
+export const IsInvalid = {
+ render: Template,
+
+ args: {
+ ...defaultProps,
+ isInvalid: true,
+ variant: "bordered",
+ defaultSelectedKey: "dog",
+ errorMessage: "Please select a valid animal",
+ },
+};
+
+export const WithDescription = {
+ render: MirrorTemplate,
+
+ args: {
+ ...defaultProps,
+ description: "Select your favorite animal",
+ },
+};
+
export const WithoutScrollShadow = {
render: Template,
@@ -847,67 +899,63 @@ export const WithValidation = {
},
};
-export const IsInvalid = {
- render: Template,
+export const WithSections = {
+ render: WithSectionsTemplate,
args: {
...defaultProps,
- isInvalid: true,
- variant: "bordered",
- defaultSelectedKey: "dog",
- errorMessage: "Please select a valid animal",
},
};
-export const Controlled = {
- render: ControlledTemplate,
+export const WithCustomSectionsStyles = {
+ render: WithCustomSectionsStylesTemplate,
args: {
...defaultProps,
},
};
-export const CustomSelectorIcon = {
- render: Template,
+export const WithAriaLabel = {
+ render: WithAriaLabelTemplate,
args: {
...defaultProps,
- disableSelectorIconRotation: true,
- selectorIcon: ,
+ label: "Select an animal 🐹",
+ "aria-label": "Select an animal",
},
};
-export const CustomItems = {
- render: CustomItemsTemplate,
+export const WithReactHookForm = {
+ render: WithReactHookFormTemplate,
args: {
...defaultProps,
},
};
-export const WithSections = {
- render: WithSectionsTemplate,
+export const Controlled = {
+ render: ControlledTemplate,
args: {
...defaultProps,
},
};
-export const WithCustomSectionsStyles = {
- render: WithCustomSectionsStylesTemplate,
+export const CustomSelectorIcon = {
+ render: Template,
args: {
...defaultProps,
+ disableSelectorIconRotation: true,
+ selectorIcon: ,
},
};
-export const WithAriaLabel = {
- render: WithAriaLabelTemplate,
+export const CustomItems = {
+ render: CustomItemsTemplate,
args: {
...defaultProps,
- label: "Select an animal 🐹",
- "aria-label": "Select an animal",
},
};
diff --git a/packages/components/checkbox/__tests__/checkbox.test.tsx b/packages/components/checkbox/__tests__/checkbox.test.tsx
index c3e7308b74..8e854a4a38 100644
--- a/packages/components/checkbox/__tests__/checkbox.test.tsx
+++ b/packages/components/checkbox/__tests__/checkbox.test.tsx
@@ -1,6 +1,7 @@
import * as React from "react";
-import {render, act} from "@testing-library/react";
+import {render, renderHook, act} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
+import {useForm} from "react-hook-form";
import {Checkbox, CheckboxProps} from "../src";
@@ -128,3 +129,74 @@ describe("Checkbox", () => {
expect(onChange).toBeCalled();
});
});
+
+describe("Checkbox with React Hook Form", () => {
+ let checkbox1: HTMLInputElement;
+ let checkbox2: HTMLInputElement;
+ let checkbox3: HTMLInputElement;
+ let submitButton: HTMLButtonElement;
+ let onSubmit: () => void;
+
+ beforeEach(() => {
+ const {result} = renderHook(() =>
+ useForm({
+ defaultValues: {
+ withDefaultValue: true,
+ withoutDefaultValue: false,
+ requiredField: false,
+ },
+ }),
+ );
+
+ const {
+ handleSubmit,
+ register,
+ formState: {errors},
+ } = result.current;
+
+ onSubmit = jest.fn();
+
+ render(
+ ,
+ );
+
+ checkbox1 = document.querySelector("input[name=withDefaultValue]")!;
+ checkbox2 = document.querySelector("input[name=withoutDefaultValue]")!;
+ checkbox3 = document.querySelector("input[name=requiredField]")!;
+ submitButton = document.querySelector("button")!;
+ });
+
+ it("should work with defaultValues", () => {
+ expect(checkbox1.checked).toBe(true);
+ expect(checkbox2.checked).toBe(false);
+ expect(checkbox3.checked).toBe(false);
+ });
+
+ it("should not submit form when required field is empty", async () => {
+ const user = userEvent.setup();
+
+ await user.click(submitButton);
+
+ expect(onSubmit).toHaveBeenCalledTimes(0);
+ });
+
+ it("should submit form when required field is not empty", async () => {
+ act(() => {
+ checkbox3.click();
+ });
+
+ expect(checkbox3.checked).toBe(true);
+
+ const user = userEvent.setup();
+
+ await user.click(submitButton);
+
+ expect(onSubmit).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/components/input/__tests__/input.test.tsx b/packages/components/input/__tests__/input.test.tsx
index d9ab83d694..a6da5128f4 100644
--- a/packages/components/input/__tests__/input.test.tsx
+++ b/packages/components/input/__tests__/input.test.tsx
@@ -1,6 +1,7 @@
import * as React from "react";
-import {render} from "@testing-library/react";
+import {render, renderHook, fireEvent} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
+import {useForm} from "react-hook-form";
import {Input} from "../src";
@@ -146,3 +147,78 @@ describe("Input", () => {
expect(onClear).toHaveBeenCalledTimes(1);
});
});
+
+describe("Input with React Hook Form", () => {
+ let input1: HTMLInputElement;
+ let input2: HTMLInputElement;
+ let input3: HTMLInputElement;
+ let submitButton: HTMLButtonElement;
+ let onSubmit: () => void;
+
+ beforeEach(() => {
+ const {result} = renderHook(() =>
+ useForm({
+ defaultValues: {
+ withDefaultValue: "wkw",
+ withoutDefaultValue: "",
+ requiredField: "",
+ },
+ }),
+ );
+
+ const {
+ handleSubmit,
+ register,
+ formState: {errors},
+ } = result.current;
+
+ onSubmit = jest.fn();
+
+ render(
+ ,
+ );
+
+ input1 = document.querySelector("input[name=withDefaultValue]")!;
+ input2 = document.querySelector("input[name=withoutDefaultValue]")!;
+ input3 = document.querySelector("input[name=requiredField]")!;
+ submitButton = document.querySelector("button")!;
+ });
+
+ it("should work with defaultValues", () => {
+ expect(input1).toHaveValue("wkw");
+ expect(input2).toHaveValue("");
+ expect(input3).toHaveValue("");
+ });
+
+ it("should not submit form when required field is empty", async () => {
+ const user = userEvent.setup();
+
+ await user.click(submitButton);
+
+ expect(onSubmit).toHaveBeenCalledTimes(0);
+ });
+
+ it("should submit form when required field is not empty", async () => {
+ fireEvent.change(input3, {target: {value: "updated"}});
+
+ const user = userEvent.setup();
+
+ await user.click(submitButton);
+
+ expect(onSubmit).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/components/select/__tests__/select.test.tsx b/packages/components/select/__tests__/select.test.tsx
index 654a58e484..3c46a4f59f 100644
--- a/packages/components/select/__tests__/select.test.tsx
+++ b/packages/components/select/__tests__/select.test.tsx
@@ -1,6 +1,7 @@
import * as React from "react";
-import {act, render} from "@testing-library/react";
+import {render, renderHook, act} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
+import {useForm} from "react-hook-form";
import {Select, SelectItem, SelectSection, type SelectProps} from "../src";
import {Modal, ModalContent, ModalHeader, ModalBody, ModalFooter} from "../../modal/src";
@@ -397,3 +398,94 @@ describe("Select", () => {
});
});
});
+
+describe("Select with React Hook Form", () => {
+ let select1: HTMLElement;
+ let select2: HTMLElement;
+ let select3: HTMLElement;
+ let submitButton: HTMLButtonElement;
+ let wrapper: any;
+ let onSubmit: () => void;
+
+ beforeEach(() => {
+ const {result} = renderHook(() =>
+ useForm({
+ defaultValues: {
+ withDefaultValue: "cat",
+ withoutDefaultValue: "",
+ requiredField: "",
+ },
+ }),
+ );
+
+ const {
+ register,
+ formState: {errors},
+ handleSubmit,
+ } = result.current;
+
+ onSubmit = jest.fn();
+
+ wrapper = render(
+ ,
+ );
+
+ select1 = wrapper.getByTestId("select-1");
+ select2 = wrapper.getByTestId("select-2");
+ select3 = wrapper.getByTestId("select-3");
+ submitButton = wrapper.getByTestId("submit-button");
+ });
+
+ it("should work with defaultValues", () => {
+ expect(select1).toHaveTextContent("Cat");
+ expect(select2).toHaveTextContent("");
+ expect(select3).toHaveTextContent("");
+ });
+
+ it("should not submit form when required field is empty", async () => {
+ const user = userEvent.setup();
+
+ await user.click(submitButton);
+
+ expect(onSubmit).toHaveBeenCalledTimes(0);
+ });
+
+ it("should submit form when required field is not empty", async () => {
+ const user = userEvent.setup();
+
+ await user.click(select3);
+
+ expect(select3).toHaveAttribute("aria-expanded", "true");
+
+ let listboxItems = wrapper.getAllByRole("option");
+
+ await user.click(listboxItems[1]);
+
+ expect(select3).toHaveTextContent("Dog");
+
+ await user.click(submitButton);
+
+ expect(onSubmit).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/components/select/package.json b/packages/components/select/package.json
index f7ee446f55..50e15956a4 100644
--- a/packages/components/select/package.json
+++ b/packages/components/select/package.json
@@ -73,7 +73,8 @@
"@react-stately/data": "^3.11.2",
"clean-package": "2.2.0",
"react": "^18.0.0",
- "react-dom": "^18.0.0"
+ "react-dom": "^18.0.0",
+ "react-hook-form": "^7.51.3"
},
"clean-package": "../../../clean-package.config.json"
}
diff --git a/packages/components/select/stories/select.stories.tsx b/packages/components/select/stories/select.stories.tsx
index 26dd2710b8..a30224cf3e 100644
--- a/packages/components/select/stories/select.stories.tsx
+++ b/packages/components/select/stories/select.stories.tsx
@@ -2,6 +2,7 @@
import type {ValidationResult} from "@react-types/shared";
import React, {ChangeEvent} from "react";
+import {useForm} from "react-hook-form";
import {Meta} from "@storybook/react";
import {select, button} from "@nextui-org/theme";
import {PetBoldIcon, SelectorIcon} from "@nextui-org/shared-icons";
@@ -585,6 +586,47 @@ const AsyncLoadingTemplate = ({color, variant, ...args}: SelectProps) =
);
};
+const WithReactHookFormTemplate = (args: SelectProps) => {
+ const {
+ register,
+ formState: {errors},
+ handleSubmit,
+ } = useForm({
+ defaultValues: {
+ withDefaultValue: "cat",
+ withoutDefaultValue: "",
+ requiredField: "",
+ },
+ });
+
+ const onSubmit = (data: any) => {
+ // eslint-disable-next-line no-console
+ console.log(data);
+ alert("Submitted value: " + JSON.stringify(data));
+ };
+
+ return (
+
+ );
+};
+
export const Default = {
render: MirrorTemplate,
@@ -631,23 +673,15 @@ export const DisabledOptions = {
},
};
-export const WithDescription = {
- render: MirrorTemplate,
-
- args: {
- ...defaultProps,
- description: "Select your favorite animal",
- },
-};
-
-export const WithoutLabel = {
+export const IsInvalid = {
render: Template,
args: {
...defaultProps,
- label: null,
- "aria-label": "Select an animal",
- placeholder: "Select an animal",
+ isInvalid: true,
+ variant: "bordered",
+ defaultSelectedKeys: ["dog"],
+ errorMessage: "Please select a valid animal",
},
};
@@ -675,6 +709,26 @@ export const StartContent = {
},
};
+export const WithDescription = {
+ render: MirrorTemplate,
+
+ args: {
+ ...defaultProps,
+ description: "Select your favorite animal",
+ },
+};
+
+export const WithoutLabel = {
+ render: Template,
+
+ args: {
+ ...defaultProps,
+ label: null,
+ "aria-label": "Select an animal",
+ placeholder: "Select an animal",
+ },
+};
+
export const WithoutScrollShadow = {
render: Template,
@@ -726,15 +780,62 @@ export const WithErrorMessageFunction = {
},
};
-export const IsInvalid = {
- render: Template,
+export const WithChips = {
+ render: CustomItemsTemplate,
args: {
...defaultProps,
- isInvalid: true,
variant: "bordered",
- defaultSelectedKeys: ["dog"],
- errorMessage: "Please select a valid animal",
+ selectionMode: "multiple",
+ isMultiline: true,
+ labelPlacement: "outside",
+ classNames: {
+ base: "max-w-xs",
+ trigger: "min-h-12 py-2",
+ },
+ renderValue: (items: SelectedItems) => {
+ return (
+
+ {items.map((item) => (
+ {item.data?.name}
+ ))}
+
+ );
+ },
+ },
+};
+
+export const WithSections = {
+ render: WithSectionsTemplate,
+
+ args: {
+ ...defaultProps,
+ },
+};
+
+export const WithCustomSectionsStyles = {
+ render: WithCustomSectionsStylesTemplate,
+
+ args: {
+ ...defaultProps,
+ },
+};
+
+export const WithAriaLabel = {
+ render: WithAriaLabelTemplate,
+
+ args: {
+ ...defaultProps,
+ label: "Select an animal 🐹",
+ "aria-label": "Select an animal",
+ },
+};
+
+export const WithReactHookForm = {
+ render: WithReactHookFormTemplate,
+
+ args: {
+ ...defaultProps,
},
};
@@ -808,57 +909,6 @@ export const CustomRenderValue = {
},
};
-export const WithChips = {
- render: CustomItemsTemplate,
-
- args: {
- ...defaultProps,
- variant: "bordered",
- selectionMode: "multiple",
- isMultiline: true,
- labelPlacement: "outside",
- classNames: {
- base: "max-w-xs",
- trigger: "min-h-12 py-2",
- },
- renderValue: (items: SelectedItems) => {
- return (
-
- {items.map((item) => (
- {item.data?.name}
- ))}
-
- );
- },
- },
-};
-
-export const WithSections = {
- render: WithSectionsTemplate,
-
- args: {
- ...defaultProps,
- },
-};
-
-export const WithCustomSectionsStyles = {
- render: WithCustomSectionsStylesTemplate,
-
- args: {
- ...defaultProps,
- },
-};
-
-export const WithAriaLabel = {
- render: WithAriaLabelTemplate,
-
- args: {
- ...defaultProps,
- label: "Select an animal 🐹",
- "aria-label": "Select an animal",
- },
-};
-
export const CustomStyles = {
render: CustomStylesTemplate,
diff --git a/packages/components/switch/__tests__/switch.test.tsx b/packages/components/switch/__tests__/switch.test.tsx
index 870ef8933c..d9b76e9092 100644
--- a/packages/components/switch/__tests__/switch.test.tsx
+++ b/packages/components/switch/__tests__/switch.test.tsx
@@ -1,5 +1,7 @@
import * as React from "react";
-import {act, render} from "@testing-library/react";
+import {render, renderHook, act} from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import {useForm} from "react-hook-form";
import {Switch} from "../src";
@@ -11,7 +13,7 @@ describe("Switch", () => {
});
it("ref should be forwarded", () => {
- const ref = React.createRef();
+ const ref = React.createRef();
render();
expect(ref.current).not.toBeNull();
@@ -198,3 +200,74 @@ describe("Switch", () => {
expect(wrapper.getByTestId("end-icon")).toBeInTheDocument();
});
});
+
+describe("Switch with React Hook Form", () => {
+ let switch1: HTMLInputElement;
+ let switch2: HTMLInputElement;
+ let switch3: HTMLInputElement;
+ let submitButton: HTMLButtonElement;
+ let onSubmit: () => void;
+
+ beforeEach(() => {
+ const {result} = renderHook(() =>
+ useForm({
+ defaultValues: {
+ defaultTrue: true,
+ defaultFalse: false,
+ requiredField: false,
+ },
+ }),
+ );
+
+ const {
+ register,
+ formState: {errors},
+ handleSubmit,
+ } = result.current;
+
+ onSubmit = jest.fn();
+
+ render(
+ ,
+ );
+
+ switch1 = document.querySelector("input[name=defaultTrue]")!;
+ switch2 = document.querySelector("input[name=defaultFalse]")!;
+ switch3 = document.querySelector("input[name=requiredField]")!;
+ submitButton = document.querySelector("button")!;
+ });
+
+ it("should work with defaultValues", () => {
+ expect(switch1.checked).toBe(true);
+ expect(switch2.checked).toBe(false);
+ expect(switch3.checked).toBe(false);
+ });
+
+ it("should not submit form when required field is empty", async () => {
+ const user = userEvent.setup();
+
+ await user.click(submitButton);
+
+ expect(onSubmit).toHaveBeenCalledTimes(0);
+ });
+
+ it("should submit form when required field is not empty", async () => {
+ act(() => {
+ switch3.click();
+ });
+
+ expect(switch3.checked).toBe(true);
+
+ const user = userEvent.setup();
+
+ await user.click(submitButton);
+
+ expect(onSubmit).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/components/switch/stories/switch.stories.tsx b/packages/components/switch/stories/switch.stories.tsx
index 8564dfd7e3..408762247f 100644
--- a/packages/components/switch/stories/switch.stories.tsx
+++ b/packages/components/switch/stories/switch.stories.tsx
@@ -221,32 +221,32 @@ export const WithIcons = {
},
};
-export const Controlled = {
- render: ControlledTemplate,
+export const WithReactHookForm = {
+ render: WithReactHookFormTemplate,
args: {
...defaultProps,
},
};
-export const CustomWithClassNames = {
- render: CustomWithClassNamesTemplate,
+export const Controlled = {
+ render: ControlledTemplate,
args: {
...defaultProps,
},
};
-export const CustomWithHooks = {
- render: CustomWithHooksTemplate,
+export const CustomWithClassNames = {
+ render: CustomWithClassNamesTemplate,
args: {
...defaultProps,
},
};
-export const WithReactHookForm = {
- render: WithReactHookFormTemplate,
+export const CustomWithHooks = {
+ render: CustomWithHooksTemplate,
args: {
...defaultProps,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index dcb67beea4..173c5faad7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -783,6 +783,9 @@ importers:
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
+ react-hook-form:
+ specifier: ^7.51.3
+ version: 7.51.3(react@18.2.0)
packages/components/avatar:
dependencies:
@@ -2311,6 +2314,9 @@ importers:
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
+ react-hook-form:
+ specifier: ^7.51.3
+ version: 7.51.3(react@18.2.0)
packages/components/skeleton:
dependencies: