diff --git a/.changeset/fuzzy-lies-brush.md b/.changeset/fuzzy-lies-brush.md
new file mode 100644
index 0000000000..9b1eb50573
--- /dev/null
+++ b/.changeset/fuzzy-lies-brush.md
@@ -0,0 +1,5 @@
+---
+"@nextui-org/table": minor
+---
+
+The `layoutNode` prop has been removed due to the update to react-aria.
diff --git a/.changeset/polite-mails-kneel.md b/.changeset/polite-mails-kneel.md
new file mode 100644
index 0000000000..d573540365
--- /dev/null
+++ b/.changeset/polite-mails-kneel.md
@@ -0,0 +1,47 @@
+---
+"@nextui-org/accordion": patch
+"@nextui-org/avatar": patch
+"@nextui-org/breadcrumbs": patch
+"@nextui-org/button": patch
+"@nextui-org/calendar": patch
+"@nextui-org/card": patch
+"@nextui-org/chip": patch
+"@nextui-org/date-input": patch
+"@nextui-org/divider": patch
+"@nextui-org/dropdown": patch
+"@nextui-org/kbd": patch
+"@nextui-org/link": patch
+"@nextui-org/listbox": patch
+"@nextui-org/menu": patch
+"@nextui-org/modal": patch
+"@nextui-org/navbar": patch
+"@nextui-org/pagination": patch
+"@nextui-org/popover": patch
+"@nextui-org/progress": patch
+"@nextui-org/select": patch
+"@nextui-org/slider": patch
+"@nextui-org/snippet": patch
+"@nextui-org/switch": patch
+"@nextui-org/tabs": patch
+"@nextui-org/tooltip": patch
+"@nextui-org/user": patch
+"@nextui-org/react": patch
+"@nextui-org/system": patch
+"@nextui-org/system-rsc": patch
+"@nextui-org/use-aria-accordion": patch
+"@nextui-org/use-aria-accordion-item": patch
+"@nextui-org/use-aria-button": patch
+"@nextui-org/use-aria-link": patch
+"@nextui-org/use-aria-menu": patch
+"@nextui-org/use-aria-modal-overlay": patch
+"@nextui-org/use-aria-multiselect": patch
+"@nextui-org/use-aria-overlay": patch
+"@nextui-org/use-aria-toggle-button": patch
+"@nextui-org/use-disclosure": patch
+"@nextui-org/use-intersection-observer": patch
+"@nextui-org/use-is-mobile": patch
+"@nextui-org/use-pagination": patch
+"@nextui-org/aria-utils": patch
+---
+
+update react-aria version
diff --git a/.changeset/purple-berries-play.md b/.changeset/purple-berries-play.md
new file mode 100644
index 0000000000..24f04e5ad5
--- /dev/null
+++ b/.changeset/purple-berries-play.md
@@ -0,0 +1,5 @@
+---
+"@nextui-org/form": major
+---
+
+add form component
diff --git a/.changeset/sharp-pianos-pump.md b/.changeset/sharp-pianos-pump.md
new file mode 100644
index 0000000000..5a9fb5e906
--- /dev/null
+++ b/.changeset/sharp-pianos-pump.md
@@ -0,0 +1,9 @@
+---
+"@nextui-org/autocomplete": minor
+"@nextui-org/checkbox": minor
+"@nextui-org/date-picker": minor
+"@nextui-org/input": minor
+"@nextui-org/radio": minor
+---
+
+support server validation with form
diff --git a/.vscode/settings.json b/.vscode/settings.json
index c531e733d0..6b36bf07d3 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -10,5 +10,5 @@
},
"tailwindCSS.experimental.classRegex": [
["([\"'`][^\"'`]*.*?[\"'`])", "[\"'`]([^\"'`]*).*?[\"'`]"]
- ]
+ ],
}
diff --git a/apps/docs/package.json b/apps/docs/package.json
index 01e4bc4aba..501e799277 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -18,7 +18,7 @@
"@codesandbox/sandpack-react": "^2.6.4",
"@iconify/icons-solar": "^1.2.3",
"@iconify/react": "^4.1.1",
- "@internationalized/date": "3.5.5",
+ "@internationalized/date": "3.5.6",
"@mapbox/rehype-prism": "^0.6.0",
"@nextui-org/aria-utils": "workspace:*",
"@nextui-org/badge": "workspace:*",
@@ -37,14 +37,14 @@
"@nextui-org/use-infinite-scroll": "workspace:*",
"@nextui-org/use-is-mobile": "workspace:*",
"@radix-ui/react-scroll-area": "^1.0.5",
- "@react-aria/focus": "3.17.1",
- "@react-aria/i18n": "3.11.1",
- "@react-aria/interactions": "3.21.3",
- "@react-aria/selection": "3.18.1",
- "@react-aria/ssr": "3.9.4",
- "@react-aria/utils": "3.24.1",
+ "@react-aria/focus": "3.18.4",
+ "@react-aria/i18n": "3.12.3",
+ "@react-aria/interactions": "3.22.4",
+ "@react-aria/selection": "3.20.1",
+ "@react-aria/ssr": "3.9.6",
+ "@react-aria/utils": "3.25.3",
"@react-aria/virtualizer": "3.10.1",
- "@react-aria/visually-hidden": "3.8.12",
+ "@react-aria/visually-hidden": "3.8.17",
"@react-stately/data": "3.11.4",
"@react-stately/layout": "3.13.9",
"@react-stately/tree": "3.8.1",
@@ -98,7 +98,7 @@
"@docusaurus/utils": "2.0.0-beta.3",
"@next/bundle-analyzer": "^13.4.6",
"@next/env": "^13.4.12",
- "@react-types/shared": "3.24.1",
+ "@react-types/shared": "3.25.0",
"@tailwindcss/typography": "^0.5.9",
"@types/canvas-confetti": "^1.4.2",
"@types/marked": "^5.0.0",
diff --git a/package.json b/package.json
index 5a3e849bd7..08fbeb1e38 100644
--- a/package.json
+++ b/package.json
@@ -69,8 +69,8 @@
"@commitlint/cli": "^17.2.0",
"@commitlint/config-conventional": "^17.2.0",
"@react-bootstrap/babel-preset": "^2.1.0",
- "@react-types/link": "^3.4.4",
- "@react-types/shared": "3.24.1",
+ "@react-types/link": "3.5.7",
+ "@react-types/shared": "3.25.0",
"@storybook/react": "^7.4.6",
"@swc/core": "^1.3.35",
"@swc/jest": "^0.2.24",
diff --git a/packages/components/accordion/package.json b/packages/components/accordion/package.json
index 681011d80e..dcfcf8763d 100644
--- a/packages/components/accordion/package.json
+++ b/packages/components/accordion/package.json
@@ -55,13 +55,13 @@
"@nextui-org/divider": "workspace:*",
"@nextui-org/use-aria-accordion": "workspace:*",
"@nextui-org/dom-animation": "workspace:*",
- "@react-aria/interactions": "3.22.2",
- "@react-aria/focus": "3.18.2",
- "@react-aria/utils": "3.25.2",
- "@react-stately/tree": "3.8.4",
- "@react-aria/button": "3.9.8",
- "@react-types/accordion": "3.0.0-alpha.23",
- "@react-types/shared": "3.24.1"
+ "@react-aria/interactions": "3.22.4",
+ "@react-aria/focus": "3.18.4",
+ "@react-aria/utils": "3.25.3",
+ "@react-stately/tree": "3.8.5",
+ "@react-aria/button": "3.10.1",
+ "@react-types/accordion": "3.0.0-alpha.24",
+ "@react-types/shared": "3.25.0"
},
"devDependencies": {
"@nextui-org/theme": "workspace:*",
diff --git a/packages/components/alert/package.json b/packages/components/alert/package.json
index c6ab2e033a..455df85aa6 100644
--- a/packages/components/alert/package.json
+++ b/packages/components/alert/package.json
@@ -47,8 +47,8 @@
"@nextui-org/react-utils": "workspace:*",
"@nextui-org/shared-icons": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
- "@react-stately/utils": "3.10.1",
- "@react-aria/utils": "3.24.1",
+ "@react-stately/utils": "3.10.4",
+ "@react-aria/utils": "3.25.3",
"@nextui-org/button": "workspace:*"
},
"devDependencies": {
diff --git a/packages/components/autocomplete/__tests__/autocomplete.test.tsx b/packages/components/autocomplete/__tests__/autocomplete.test.tsx
index 5fa0b3a783..55a050d64c 100644
--- a/packages/components/autocomplete/__tests__/autocomplete.test.tsx
+++ b/packages/components/autocomplete/__tests__/autocomplete.test.tsx
@@ -3,6 +3,7 @@ import * as React from "react";
import {within, render, renderHook, act} from "@testing-library/react";
import userEvent, {UserEvent} from "@testing-library/user-event";
import {useForm} from "react-hook-form";
+import {Form} from "@nextui-org/form";
import {Autocomplete, AutocompleteItem, AutocompleteProps, AutocompleteSection} from "../src";
import {Modal, ModalContent, ModalBody, ModalHeader, ModalFooter} from "../../modal/src";
@@ -588,6 +589,60 @@ describe("Autocomplete", () => {
expect(autocomplete2).toHaveFocus();
});
+ it("should work when key equals textValue", async () => {
+ const wrapper = render(
+
+ {(item) => {item.value}}
+ ,
+ );
+
+ const autocomplete = wrapper.getByTestId("when-key-equals-textValue");
+
+ const user = userEvent.setup();
+
+ await user.click(autocomplete);
+
+ expect(autocomplete).toHaveValue("cat");
+ expect(autocomplete).toHaveAttribute("aria-expanded", "true");
+
+ let listboxItems = wrapper.getAllByRole("option");
+
+ await user.click(listboxItems[1]);
+
+ expect(autocomplete).toHaveValue("dog");
+ expect(autocomplete).toHaveAttribute("aria-expanded", "false");
+ });
+
+ it("should work when key equals textValue (controlled)", async () => {
+ const wrapper = render(
+
+ {(item) => {item.value}}
+ ,
+ );
+
+ const autocomplete = wrapper.getByTestId("when-key-equals-textValue");
+
+ const user = userEvent.setup();
+
+ await user.click(autocomplete);
+
+ expect(autocomplete).toHaveValue("cat");
+ expect(autocomplete).toHaveAttribute("aria-expanded", "true");
+
+ let listboxItems = wrapper.getAllByRole("option");
+
+ await user.click(listboxItems[1]);
+
+ expect(autocomplete).toHaveValue("dog");
+ expect(autocomplete).toHaveAttribute("aria-expanded", "false");
+ });
+
describe("validation", () => {
let user;
@@ -598,9 +653,9 @@ describe("Autocomplete", () => {
describe("validationBehavior=native", () => {
it("supports isRequired", async () => {
const {getByTestId, getByRole, findByRole} = render(
-
,
+ ,
);
const input = getByRole("combobox") as HTMLInputElement;
@@ -628,6 +683,62 @@ describe("Autocomplete", () => {
await user.click(items[0]);
expect(input).toHaveAttribute("aria-describedby");
});
+
+ it("supports server validation", async () => {
+ function Test() {
+ const [serverErrors, setServerErrors] = React.useState({});
+ const onSubmit = (e) => {
+ e.preventDefault();
+ setServerErrors({
+ value: "Invalid value.",
+ });
+ };
+
+ return (
+
+ );
+ }
+
+ const {getByTestId, getByRole} = render();
+
+ const input = getByTestId("input") as HTMLInputElement;
+
+ expect(input).not.toHaveAttribute("aria-describedby");
+
+ await user.click(getByTestId("submit"));
+
+ expect(input).toHaveAttribute("aria-describedby");
+ expect(document.getElementById(input.getAttribute("aria-describedby")!)).toHaveTextContent(
+ "Invalid value.",
+ );
+ expect(input.validity.valid).toBe(false);
+
+ await user.tab({shift: true});
+ await user.keyboard("[ArrowRight]Ze");
+
+ act(() => {
+ jest.runAllTimers();
+ });
+
+ const listbox = getByRole("listbox");
+ const items = within(listbox).getAllByRole("option");
+
+ await user.click(items[0]);
+ act(() => {
+ jest.runAllTimers();
+ });
+
+ expect(input).toHaveAttribute("aria-describedby");
+ await user.tab();
+
+ expect(input).not.toHaveAttribute("aria-describedby");
+ expect(input.validity.valid).toBe(true);
+ });
});
describe("validationBehavior=aria", () => {
@@ -635,8 +746,8 @@ describe("Autocomplete", () => {
let {getByRole, findByRole} = render(
,
@@ -664,61 +775,42 @@ describe("Autocomplete", () => {
expect(input).not.toHaveAttribute("aria-describedby");
expect(input).not.toHaveAttribute("aria-invalid");
});
- });
- });
- it("should work when key equals textValue", async () => {
- const wrapper = render(
-
- {(item) => {item.value}}
- ,
- );
-
- const autocomplete = wrapper.getByTestId("when-key-equals-textValue");
-
- const user = userEvent.setup();
-
- await user.click(autocomplete);
-
- expect(autocomplete).toHaveValue("cat");
- expect(autocomplete).toHaveAttribute("aria-expanded", "true");
-
- let listboxItems = wrapper.getAllByRole("option");
-
- await user.click(listboxItems[1]);
-
- expect(autocomplete).toHaveValue("dog");
- expect(autocomplete).toHaveAttribute("aria-expanded", "false");
- });
-
- it("should work when key equals textValue (controlled)", async () => {
- const wrapper = render(
-
- {(item) => {item.value}}
- ,
- );
+ it("supports server validation", async () => {
+ const {getByTestId, getByRole} = render(
+ ,
+ );
- const autocomplete = wrapper.getByTestId("when-key-equals-textValue");
+ const input = getByTestId("input");
- const user = userEvent.setup();
+ expect(input).toHaveAttribute("aria-describedby");
+ expect(input).toHaveAttribute("aria-invalid", "true");
+ expect(document.getElementById(input.getAttribute("aria-describedby")!)).toHaveTextContent(
+ "Invalid value",
+ );
- await user.click(autocomplete);
+ await user.tab();
+ await user.keyboard("[ArrowRight]Ze");
- expect(autocomplete).toHaveValue("cat");
- expect(autocomplete).toHaveAttribute("aria-expanded", "true");
+ act(() => {
+ jest.runAllTimers();
+ });
- let listboxItems = wrapper.getAllByRole("option");
+ const listbox = getByRole("listbox");
+ const items = within(listbox).getAllByRole("option");
- await user.click(listboxItems[1]);
+ await user.click(items[0]);
+ act(() => {
+ jest.runAllTimers();
+ });
- expect(autocomplete).toHaveValue("dog");
- expect(autocomplete).toHaveAttribute("aria-expanded", "false");
+ await user.tab();
+ expect(input).not.toHaveAttribute("aria-describedby");
+ expect(input).not.toHaveAttribute("aria-invalid");
+ });
+ });
});
});
diff --git a/packages/components/autocomplete/package.json b/packages/components/autocomplete/package.json
index adf887b029..38e2c4a5d4 100644
--- a/packages/components/autocomplete/package.json
+++ b/packages/components/autocomplete/package.json
@@ -41,6 +41,7 @@
"react-dom": ">=18"
},
"dependencies": {
+ "@nextui-org/form": "workspace:*",
"@nextui-org/aria-utils": "workspace:*",
"@nextui-org/button": "workspace:*",
"@nextui-org/input": "workspace:*",
@@ -53,15 +54,15 @@
"@nextui-org/spinner": "workspace:*",
"@nextui-org/use-aria-button": "workspace:*",
"@nextui-org/use-safe-layout-effect": "workspace:*",
- "@react-aria/combobox": "3.10.3",
- "@react-aria/focus": "3.18.2",
- "@react-aria/i18n": "3.12.2",
- "@react-aria/interactions": "3.22.2",
- "@react-aria/utils": "3.25.2",
- "@react-aria/visually-hidden": "3.8.15",
- "@react-stately/combobox": "3.9.2",
- "@react-types/combobox": "3.12.1",
- "@react-types/shared": "3.24.1"
+ "@react-aria/combobox": "3.10.5",
+ "@react-aria/focus": "3.18.4",
+ "@react-aria/i18n": "3.12.3",
+ "@react-aria/interactions": "3.22.4",
+ "@react-aria/utils": "3.25.3",
+ "@react-aria/visually-hidden": "3.8.17",
+ "@react-stately/combobox": "3.10.0",
+ "@react-types/combobox": "3.13.0",
+ "@react-types/shared": "3.25.0"
},
"devDependencies": {
"@nextui-org/avatar": "workspace:*",
@@ -70,7 +71,7 @@
"@nextui-org/system": "workspace:*",
"@nextui-org/theme": "workspace:*",
"@nextui-org/use-infinite-scroll": "workspace:*",
- "@react-stately/data": "3.11.6",
+ "@react-stately/data": "3.11.7",
"clean-package": "2.2.0",
"framer-motion": "11.9.0",
"react": "^18.0.0",
@@ -78,4 +79,4 @@
"react-hook-form": "^7.51.3"
},
"clean-package": "../../../clean-package.config.json"
-}
\ No newline at end of file
+}
diff --git a/packages/components/autocomplete/src/use-autocomplete.ts b/packages/components/autocomplete/src/use-autocomplete.ts
index 6f84d1c979..ba957d60c4 100644
--- a/packages/components/autocomplete/src/use-autocomplete.ts
+++ b/packages/components/autocomplete/src/use-autocomplete.ts
@@ -18,6 +18,7 @@ import {chain, mergeProps} from "@react-aria/utils";
import {ButtonProps} from "@nextui-org/button";
import {AsyncLoadable, PressEvent} from "@react-types/shared";
import {useComboBox} from "@react-aria/combobox";
+import {FormContext, useSlottedContext} from "@nextui-org/form";
import {ariaShouldCloseOnInteractOutside} from "@nextui-org/aria-utils";
interface Props extends Omit, keyof ComboBoxProps> {
@@ -120,6 +121,7 @@ export type UseAutocompleteProps = Props &
export function useAutocomplete(originalProps: UseAutocompleteProps) {
const globalContext = useProviderContext();
+ const {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
const [props, variantProps] = mapPropsVariants(originalProps, autocomplete.variantKeys);
const disableAnimation =
@@ -158,7 +160,7 @@ export function useAutocomplete(originalProps: UseAutocomplete
clearButtonProps = {},
showScrollIndicators = true,
allowsCustomValue = false,
- validationBehavior = globalContext?.validationBehavior ?? "aria",
+ validationBehavior = formValidationBehavior ?? globalContext?.validationBehavior ?? "aria",
className,
classNames,
errorMessage,
diff --git a/packages/components/autocomplete/stories/autocomplete.stories.tsx b/packages/components/autocomplete/stories/autocomplete.stories.tsx
index ca4db45df2..771d3f5525 100644
--- a/packages/components/autocomplete/stories/autocomplete.stories.tsx
+++ b/packages/components/autocomplete/stories/autocomplete.stories.tsx
@@ -18,6 +18,7 @@ import {useInfiniteScroll} from "@nextui-org/use-infinite-scroll";
import {PetBoldIcon, SearchLinearIcon, SelectorIcon} from "@nextui-org/shared-icons";
import {Avatar} from "@nextui-org/avatar";
import {Button} from "@nextui-org/button";
+import {Form} from "@nextui-org/form";
import {Autocomplete, AutocompleteItem, AutocompleteProps, AutocompleteSection} from "../src";
@@ -803,6 +804,33 @@ const WithReactHookFormTemplate = (args: AutocompleteProps) => {
);
};
+const ServerValidationTemplate = (args: AutocompleteProps) => {
+ const [serverErrors, setServerErrors] = React.useState({});
+ const onSubmit = (e) => {
+ e.preventDefault();
+ setServerErrors({
+ animals: "Please select a valid animal.",
+ });
+ };
+
+ return (
+
+ );
+};
+
export const Default = {
render: Template,
args: {
@@ -978,6 +1006,13 @@ export const WithValidation = {
},
};
+export const WithServerValidation = {
+ render: ServerValidationTemplate,
+ args: {
+ ...defaultProps,
+ },
+};
+
export const WithSections = {
render: WithSectionsTemplate,
diff --git a/packages/components/avatar/package.json b/packages/components/avatar/package.json
index 5d6ab35353..d1c82dd011 100644
--- a/packages/components/avatar/package.json
+++ b/packages/components/avatar/package.json
@@ -43,9 +43,9 @@
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/react-utils": "workspace:*",
"@nextui-org/use-image": "workspace:*",
- "@react-aria/interactions": "3.22.2",
- "@react-aria/focus": "3.18.2",
- "@react-aria/utils": "3.25.2"
+ "@react-aria/interactions": "3.22.4",
+ "@react-aria/focus": "3.18.4",
+ "@react-aria/utils": "3.25.3"
},
"devDependencies": {
"@nextui-org/theme": "workspace:*",
diff --git a/packages/components/breadcrumbs/package.json b/packages/components/breadcrumbs/package.json
index 4317644457..ef98db8b2d 100644
--- a/packages/components/breadcrumbs/package.json
+++ b/packages/components/breadcrumbs/package.json
@@ -43,11 +43,11 @@
"@nextui-org/react-utils": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/shared-icons": "workspace:*",
- "@react-aria/focus": "3.18.2",
- "@react-aria/breadcrumbs": "3.5.16",
- "@react-aria/utils": "3.25.2",
- "@react-types/breadcrumbs": "3.7.7",
- "@react-types/shared": "3.24.1"
+ "@react-aria/focus": "3.18.4",
+ "@react-aria/breadcrumbs": "3.5.18",
+ "@react-aria/utils": "3.25.3",
+ "@react-types/breadcrumbs": "3.7.8",
+ "@react-types/shared": "3.25.0"
},
"devDependencies": {
"@nextui-org/theme": "workspace:*",
diff --git a/packages/components/button/package.json b/packages/components/button/package.json
index 8f93565575..2b5a31e670 100644
--- a/packages/components/button/package.json
+++ b/packages/components/button/package.json
@@ -46,12 +46,12 @@
"@nextui-org/use-aria-button": "workspace:*",
"@nextui-org/ripple": "workspace:*",
"@nextui-org/spinner": "workspace:*",
- "@react-aria/button": "3.9.8",
- "@react-aria/interactions": "3.22.2",
- "@react-aria/utils": "3.25.2",
- "@react-aria/focus": "3.18.2",
- "@react-types/shared": "3.24.1",
- "@react-types/button": "3.9.6"
+ "@react-aria/button": "3.10.1",
+ "@react-aria/interactions": "3.22.4",
+ "@react-aria/utils": "3.25.3",
+ "@react-aria/focus": "3.18.4",
+ "@react-types/shared": "3.25.0",
+ "@react-types/button": "3.10.0"
},
"devDependencies": {
"@nextui-org/theme": "workspace:*",
diff --git a/packages/components/calendar/package.json b/packages/components/calendar/package.json
index 0272d267f0..39472eca04 100644
--- a/packages/components/calendar/package.json
+++ b/packages/components/calendar/package.json
@@ -47,18 +47,18 @@
"@nextui-org/use-aria-button": "workspace:*",
"@nextui-org/button": "workspace:*",
"@nextui-org/dom-animation": "workspace:*",
- "@internationalized/date": "3.5.5",
- "@react-aria/calendar": "3.5.11",
- "@react-aria/focus": "3.18.2",
- "@react-aria/i18n": "3.12.2",
- "@react-stately/calendar": "3.5.4",
- "@react-types/button": "3.9.6",
- "@react-aria/visually-hidden": "3.8.15",
- "@react-aria/utils": "3.25.2",
- "@react-stately/utils": "3.10.3",
- "@react-types/calendar": "3.4.9",
- "@react-aria/interactions": "3.22.2",
- "@react-types/shared": "3.24.1",
+ "@internationalized/date": "3.5.6",
+ "@react-aria/calendar": "3.5.13",
+ "@react-aria/focus": "3.18.4",
+ "@react-aria/i18n": "3.12.3",
+ "@react-stately/calendar": "3.5.5",
+ "@react-types/button": "3.10.0",
+ "@react-aria/visually-hidden": "3.8.17",
+ "@react-aria/utils": "3.25.3",
+ "@react-stately/utils": "3.10.4",
+ "@react-types/calendar": "3.4.10",
+ "@react-aria/interactions": "3.22.4",
+ "@react-types/shared": "3.25.0",
"scroll-into-view-if-needed": "3.0.10",
"@types/lodash.debounce": "^4.0.7"
},
@@ -73,4 +73,4 @@
"react-dom": "^18.0.0"
},
"clean-package": "../../../clean-package.config.json"
-}
\ No newline at end of file
+}
diff --git a/packages/components/card/package.json b/packages/components/card/package.json
index 2059b040da..83a43c12f9 100644
--- a/packages/components/card/package.json
+++ b/packages/components/card/package.json
@@ -45,11 +45,11 @@
"@nextui-org/react-utils": "workspace:*",
"@nextui-org/use-aria-button": "workspace:*",
"@nextui-org/ripple": "workspace:*",
- "@react-aria/focus": "3.18.2",
- "@react-aria/utils": "3.25.2",
- "@react-aria/interactions": "3.22.2",
- "@react-aria/button": "3.9.8",
- "@react-types/shared": "3.24.1"
+ "@react-aria/focus": "3.18.4",
+ "@react-aria/utils": "3.25.3",
+ "@react-aria/interactions": "3.22.4",
+ "@react-aria/button": "3.10.1",
+ "@react-types/shared": "3.25.0"
},
"devDependencies": {
"@nextui-org/theme": "workspace:*",
diff --git a/packages/components/checkbox/__tests__/checkbox-group.test.tsx b/packages/components/checkbox/__tests__/checkbox-group.test.tsx
index 7245ca6b2b..9e604b410b 100644
--- a/packages/components/checkbox/__tests__/checkbox-group.test.tsx
+++ b/packages/components/checkbox/__tests__/checkbox-group.test.tsx
@@ -1,5 +1,6 @@
import * as React from "react";
import {act, render} from "@testing-library/react";
+import {Form} from "@nextui-org/form";
import userEvent, {UserEvent} from "@testing-library/user-event";
import {CheckboxGroup, Checkbox} from "../src";
@@ -281,6 +282,60 @@ describe("Checkbox.Group", () => {
expect(input.validity.valid).toBe(true);
}
});
+
+ it("supports server validation", async () => {
+ function Test() {
+ const [serverErrors, setServerErrors] = React.useState({});
+ const onSubmit = (e) => {
+ e.preventDefault();
+ setServerErrors({
+ terms: "You must accept the terms.",
+ });
+ };
+
+ return (
+
+ );
+ }
+
+ const {getAllByRole, getByRole} = render();
+
+ const group = getByRole("group");
+
+ expect(group).not.toHaveAttribute("aria-describedby");
+ const button = getByRole("button");
+
+ expect(button).toBeVisible();
+ await user.click(button);
+
+ expect(group).toHaveAttribute("aria-describedby");
+ expect(document.getElementById(group.getAttribute("aria-describedby")!)).toHaveTextContent(
+ "You must accept the terms.",
+ );
+
+ const checkboxes = getAllByRole("checkbox") as HTMLInputElement[];
+
+ for (let input of checkboxes) {
+ expect(input.validity.valid).toBe(false);
+ }
+
+ await user.click(checkboxes[0]);
+ expect(group).not.toHaveAttribute("aria-describedby");
+ for (let input of checkboxes) {
+ expect(input.validity.valid).toBe(true);
+ }
+ });
});
describe("validationBehavior=aria", () => {
@@ -320,6 +375,35 @@ describe("Checkbox.Group", () => {
await user.click(checkboxes[2]);
expect(group).toHaveAttribute("aria-describedby");
});
+
+ it("supports server validation", async () => {
+ const {getAllByRole, getByRole} = render(
+ ,
+ );
+
+ const group = getByRole("group");
+
+ expect(group).toHaveAttribute("aria-describedby");
+ expect(document.getElementById(group.getAttribute("aria-describedby")!)).toHaveTextContent(
+ "You must accept the terms",
+ );
+
+ const checkboxes = getAllByRole("checkbox");
+
+ for (let input of checkboxes) {
+ expect(input).toHaveAttribute("aria-invalid", "true");
+ }
+
+ // TODO: Fix the following functions to work
+ // await user.click(checkboxes[0]);
+ // expect(group).not.toHaveAttribute("aria-describedby");
+ });
});
});
});
diff --git a/packages/components/checkbox/package.json b/packages/components/checkbox/package.json
index e25432c6a1..43b33194ef 100644
--- a/packages/components/checkbox/package.json
+++ b/packages/components/checkbox/package.json
@@ -40,19 +40,20 @@
"react-dom": ">=18"
},
"dependencies": {
+ "@nextui-org/form": "workspace:*",
"@nextui-org/react-utils": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/use-callback-ref": "workspace:*",
"@nextui-org/use-safe-layout-effect": "workspace:*",
- "@react-aria/checkbox": "3.14.6",
- "@react-aria/focus": "3.18.2",
- "@react-aria/interactions": "3.22.2",
- "@react-aria/utils": "3.25.2",
- "@react-aria/visually-hidden": "3.8.15",
- "@react-stately/checkbox": "3.6.8",
- "@react-stately/toggle": "3.7.7",
- "@react-types/checkbox": "3.8.3",
- "@react-types/shared": "3.24.1"
+ "@react-aria/checkbox": "3.14.8",
+ "@react-aria/focus": "3.18.4",
+ "@react-aria/interactions": "3.22.4",
+ "@react-aria/utils": "3.25.3",
+ "@react-aria/visually-hidden": "3.8.17",
+ "@react-stately/checkbox": "3.6.9",
+ "@react-stately/toggle": "3.7.8",
+ "@react-types/checkbox": "3.8.4",
+ "@react-types/shared": "3.25.0"
},
"devDependencies": {
"@nextui-org/chip": "workspace:*",
@@ -67,4 +68,4 @@
"react-hook-form": "^7.51.3"
},
"clean-package": "../../../clean-package.config.json"
-}
\ No newline at end of file
+}
diff --git a/packages/components/checkbox/src/checkbox-group.tsx b/packages/components/checkbox/src/checkbox-group.tsx
index 5f4aec3dbd..33ca09d4c0 100644
--- a/packages/components/checkbox/src/checkbox-group.tsx
+++ b/packages/components/checkbox/src/checkbox-group.tsx
@@ -1,5 +1,4 @@
import {forwardRef} from "@nextui-org/system";
-import {useMemo} from "react";
import {CheckboxGroupProvider} from "./checkbox-group-context";
import {UseCheckboxGroupProps, useCheckboxGroup} from "./use-checkbox-group";
@@ -21,16 +20,14 @@ const CheckboxGroup = forwardRef<"div", CheckboxGroupProps>((props, ref) => {
getErrorMessageProps,
} = useCheckboxGroup({...props, ref});
- const errorMessageContent = useMemo(() => errorMessage, [isInvalid]);
-
return (
{label &&
{label}}
{children}
- {isInvalid && errorMessageContent ? (
-
{errorMessageContent}
+ {isInvalid && errorMessage ? (
+
{errorMessage}
) : description ? (
{description}
) : null}
diff --git a/packages/components/checkbox/src/use-checkbox-group.ts b/packages/components/checkbox/src/use-checkbox-group.ts
index ee7eaba973..f0ca24e562 100644
--- a/packages/components/checkbox/src/use-checkbox-group.ts
+++ b/packages/components/checkbox/src/use-checkbox-group.ts
@@ -2,7 +2,6 @@ import type {CheckboxGroupSlots, SlotsToClasses} from "@nextui-org/theme";
import type {AriaCheckboxGroupProps} from "@react-types/checkbox";
import type {Orientation} from "@react-types/shared";
import type {ReactRef} from "@nextui-org/react-utils";
-import type {CheckboxGroupProps} from "@react-types/checkbox";
import type {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
import {useProviderContext} from "@nextui-org/system";
@@ -13,6 +12,7 @@ import {useCheckboxGroup as useReactAriaCheckboxGroup} from "@react-aria/checkbo
import {CheckboxGroupState, useCheckboxGroupState} from "@react-stately/checkbox";
import {filterDOMProps, useDOMRef} from "@nextui-org/react-utils";
import {clsx, safeAriaLabel} from "@nextui-org/shared-utils";
+import {FormContext, useSlottedContext} from "@nextui-org/form";
import {CheckboxProps} from "./index";
@@ -71,6 +71,7 @@ export type ContextType = {
export function useCheckboxGroup(props: UseCheckboxGroupProps) {
const globalContext = useProviderContext();
+ const {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
const {
as,
@@ -89,7 +90,7 @@ export function useCheckboxGroup(props: UseCheckboxGroupProps) {
orientation = "vertical",
lineThrough = false,
isDisabled = false,
- validationBehavior = globalContext?.validationBehavior ?? "aria",
+ validationBehavior = formValidationBehavior ?? globalContext?.validationBehavior ?? "aria",
disableAnimation = globalContext?.disableAnimation ?? false,
isReadOnly,
isRequired,
@@ -105,7 +106,7 @@ export function useCheckboxGroup(props: UseCheckboxGroupProps) {
const domRef = useDOMRef(ref);
- const checkboxGroupProps = useMemo
(() => {
+ const checkboxGroupProps = useMemo(() => {
return {
...otherProps,
value,
@@ -136,7 +137,6 @@ export function useCheckboxGroup(props: UseCheckboxGroupProps) {
]);
const groupState = useCheckboxGroupState(checkboxGroupProps);
-
const {
labelProps,
groupProps,
diff --git a/packages/components/checkbox/src/use-checkbox.ts b/packages/components/checkbox/src/use-checkbox.ts
index 841b93eab6..f9f4baec20 100644
--- a/packages/components/checkbox/src/use-checkbox.ts
+++ b/packages/components/checkbox/src/use-checkbox.ts
@@ -18,6 +18,7 @@ import {
} from "@react-aria/checkbox";
import {useSafeLayoutEffect} from "@nextui-org/use-safe-layout-effect";
import {mergeRefs} from "@nextui-org/react-utils";
+import {FormContext, useSlottedContext} from "@nextui-org/form";
import {useCheckboxGroupContext} from "./checkbox-group-context";
@@ -75,6 +76,7 @@ export type UseCheckboxProps = Omit &
export function useCheckbox(props: UseCheckboxProps = {}) {
const globalContext = useProviderContext();
const groupContext = useCheckboxGroupContext();
+ const {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
const isInGroup = !!groupContext;
const {
@@ -97,7 +99,9 @@ export function useCheckbox(props: UseCheckboxProps = {}) {
validationState,
isInvalid = validationState ? validationState === "invalid" : groupContext?.isInvalid ?? false,
isIndeterminate = false,
- validationBehavior = groupContext?.validationBehavior ?? "aria",
+ validationBehavior = isInGroup
+ ? groupContext.validationBehavior
+ : formValidationBehavior ?? globalContext?.validationBehavior ?? "aria",
defaultSelected,
classNames,
className,
diff --git a/packages/components/checkbox/stories/checkbox-group.stories.tsx b/packages/components/checkbox/stories/checkbox-group.stories.tsx
index 34ad88f04b..6182918020 100644
--- a/packages/components/checkbox/stories/checkbox-group.stories.tsx
+++ b/packages/components/checkbox/stories/checkbox-group.stories.tsx
@@ -4,6 +4,7 @@ import React from "react";
import {Meta} from "@storybook/react";
import {checkbox} from "@nextui-org/theme";
import {button} from "@nextui-org/theme";
+import {Form} from "@nextui-org/form";
import {CheckboxGroup, Checkbox, CheckboxGroupProps} from "../src";
@@ -90,7 +91,7 @@ const InvalidTemplate = (args: CheckboxGroupProps) => {
const FormTemplate = (args: CheckboxGroupProps) => {
return (