diff --git a/.changeset/quick-buses-kick.md b/.changeset/quick-buses-kick.md
new file mode 100644
index 0000000000..0ed0c701c0
--- /dev/null
+++ b/.changeset/quick-buses-kick.md
@@ -0,0 +1,6 @@
+---
+"@nextui-org/select": patch
+"@nextui-org/use-aria-multiselect": patch
+---
+
+Prevent default browser error UI from appearing (#3913).
diff --git a/packages/components/select/__tests__/select.test.tsx b/packages/components/select/__tests__/select.test.tsx
index 13b90369a4..11a794b140 100644
--- a/packages/components/select/__tests__/select.test.tsx
+++ b/packages/components/select/__tests__/select.test.tsx
@@ -1179,6 +1179,181 @@ describe("validation", () => {
user = userEvent.setup();
});
+ describe("validationBehavior=native", () => {
+ it("supports isRequired", async () => {
+ const {getByTestId} = render(
+
,
+ );
+
+ const select = getByTestId("select");
+ const input = document.querySelector("[name=select]");
+
+ expect(input).toHaveAttribute("required");
+ expect(input?.validity.valid).toBe(false);
+ expect(select).not.toHaveAttribute("aria-describedby");
+
+ act(() => {
+ (getByTestId("form") as HTMLFormElement).checkValidity();
+ });
+
+ expect(input?.validity.valid).toBe(false);
+ expect(select).toHaveAttribute("aria-describedby");
+ expect(document.getElementById(select.getAttribute("aria-describedby")!)).toHaveTextContent(
+ "Constraints not satisfied",
+ );
+
+ await user.click(select);
+ await user.click(document.querySelectorAll("[role='option']")[0]);
+ expect(input?.validity.valid).toBe(true);
+ expect(select).not.toHaveAttribute("aria-describedby");
+ });
+
+ it("supports validate function", async () => {
+ const {getByTestId} = render(
+ ,
+ );
+
+ const select = getByTestId("select");
+ const input = document.querySelector("[name=select]");
+
+ expect(input?.validity.valid).toBe(false);
+ expect(select).not.toHaveAttribute("aria-describedby");
+
+ await user.click(getByTestId("submit"));
+ expect(select).toHaveAttribute("aria-describedby");
+ expect(document.getElementById(select.getAttribute("aria-describedby")!)).toHaveTextContent(
+ "Invalid value",
+ );
+
+ await user.click(select);
+ await user.click(document.querySelectorAll("[role='option']")[0]);
+ expect(select).not.toHaveAttribute("aria-describedby");
+ });
+
+ it("supports server validation", async () => {
+ function Test() {
+ const [serverErrors, setServerErrors] = React.useState({});
+ const onSubmit = (e) => {
+ e.preventDefault();
+ setServerErrors({
+ select: "Invalid value.",
+ });
+ };
+
+ return (
+
+ );
+ }
+
+ const {getByTestId} = render();
+
+ const button = getByTestId("submit");
+ const select = getByTestId("select");
+ const input = document.querySelector("[name=select]");
+
+ expect(select).not.toHaveAttribute("aria-describedby");
+
+ await user.click(button);
+ expect(select).toHaveAttribute("aria-describedby");
+ expect(document.getElementById(select.getAttribute("aria-describedby")!)).toHaveTextContent(
+ "Invalid value.",
+ );
+ expect(input?.validity.valid).toBe(false);
+
+ await user.click(select);
+ await user.click(document.querySelectorAll("[role='option']")[0]);
+ expect(select).not.toHaveAttribute("aria-describedby");
+ expect(input?.validity.valid).toBe(true);
+ });
+
+ it("clears validation on reset", async () => {
+ const {getByTestId} = render(
+ ,
+ );
+
+ const select = getByTestId("select");
+ const input = document.querySelector("[name=select]");
+
+ expect(input).toHaveAttribute("required");
+ expect(input?.validity.valid).toBe(false);
+ expect(select).not.toHaveAttribute("aria-describedby");
+
+ act(() => {
+ (getByTestId("form") as HTMLFormElement).checkValidity();
+ });
+
+ expect(select).toHaveAttribute("aria-describedby");
+ expect(document.getElementById(select.getAttribute("aria-describedby")!)).toHaveTextContent(
+ "Constraints not satisfied",
+ );
+
+ await user.click(select);
+ await user.click(document.querySelectorAll("[role='option']")[0]);
+ expect(select).not.toHaveAttribute("aria-describedby");
+
+ await user.click(getByTestId("reset"));
+ expect(select).not.toHaveAttribute("aria-describedby");
+ });
+ });
+
describe("validationBehavior=aria", () => {
it("supports isRequired", async () => {
function FormRender() {
@@ -1223,7 +1398,7 @@ describe("validation", () => {
const {getByTestId} = render();
const select = getByTestId("select");
- const input = document.querySelector("input");
+ const input = document.querySelector("[name=animal]");
expect(select).not.toHaveAttribute("aria-describedby");
const button = getByTestId("button");
@@ -1255,6 +1430,7 @@ describe("validation", () => {
data-testid="select"
defaultSelectedKeys={["penguin"]}
label="Favorite Animal"
+ name="animal"
validate={(v) => (v.includes("penguin") ? "Invalid value" : null)}
validationBehavior="aria"
>
@@ -1269,7 +1445,7 @@ describe("validation", () => {
);
const select = getByTestId("select");
- const input = document.querySelector("input");
+ const input = document.querySelector("[name=animal]");
const button = getByTestId("button");
expect(select).toHaveAttribute("aria-describedby");
@@ -1292,5 +1468,28 @@ describe("validation", () => {
expect(select).not.toHaveAttribute("aria-describedby");
expect(select).not.toHaveAttribute("aria-invalid");
});
+
+ it("supports server validation", async () => {
+ let {getByTestId} = render(
+ ,
+ );
+
+ const select = getByTestId("select");
+
+ expect(select).toHaveAttribute("aria-describedby");
+ expect(document.getElementById(select.getAttribute("aria-describedby")!)).toHaveTextContent(
+ "Invalid value",
+ );
+
+ await user.click(select);
+ await user.click(document.querySelectorAll("[role='option']")[0]);
+ expect(select).not.toHaveAttribute("aria-describedby");
+ });
});
});
diff --git a/packages/components/select/src/hidden-select.tsx b/packages/components/select/src/hidden-select.tsx
index dd99ff5646..b70b7e4588 100644
--- a/packages/components/select/src/hidden-select.tsx
+++ b/packages/components/select/src/hidden-select.tsx
@@ -99,19 +99,16 @@ export function useHiddenSelect(
["data-a11y-ignore"]: "aria-hidden-focus",
},
inputProps: {
- ...commonProps,
type: "text",
tabIndex: modality == null || state.isFocused || state.isOpen ? -1 : 0,
- value: [...state.selectedKeys].join(",") ?? "",
style: {fontSize: 16},
onFocus: () => triggerRef.current?.focus(),
- onChange: () => {}, // The onChange is handled by the `select` element
+ disabled: isDisabled,
},
selectProps: {
...commonProps,
name,
tabIndex: -1,
- size: state.collection.size,
value:
selectionMode === "multiple"
? [...state.selectedKeys].map((k) => String(k))
diff --git a/packages/hooks/use-aria-multiselect/src/use-multiselect-state.ts b/packages/hooks/use-aria-multiselect/src/use-multiselect-state.ts
index d8c14ad820..fabcf407eb 100644
--- a/packages/hooks/use-aria-multiselect/src/use-multiselect-state.ts
+++ b/packages/hooks/use-aria-multiselect/src/use-multiselect-state.ts
@@ -87,6 +87,8 @@ export function useMultiSelectState({
if (props.selectionMode === "single") {
triggerState.close();
}
+
+ validationState.commitValidation();
},
});
@@ -124,7 +126,6 @@ export function useMultiSelectState({
setFocusStrategy(focusStrategy);
triggerState.toggle();
- validationState.commitValidation();
},
isFocused,
setFocused,