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 6776344eeb..ecc974f825 100644 --- a/packages/components/select/__tests__/select.test.tsx +++ b/packages/components/select/__tests__/select.test.tsx @@ -882,3 +882,58 @@ describe("Select with React Hook Form", () => { expect(onSubmit).toHaveBeenCalledTimes(1); }); }); + +describe("validationBehavior=native", () => { + let user: UserEvent; + + beforeEach(() => { + user = userEvent.setup(); + }); + + it("supports server validation", async () => { + const onSubmit = jest.fn(); + + const {getByTestId} = render( +
+ + +
, + ); + + const button = getByTestId("button"); + const select = getByTestId("select"); + const input = document.querySelector("[name=select]"); + + expect(input).toHaveAttribute("required"); + expect(select).not.toHaveAttribute("aria-describedby"); + + await user.click(button); + + expect(select).toHaveAttribute("aria-describedby"); + expect(input.validity.valid).toBe(false); + expect(onSubmit).toHaveBeenCalledTimes(0); + + expect(document.activeElement).toBe(select); + + await user.keyboard("[ArrowRight]"); + + expect(select).not.toHaveAttribute("aria-describedby"); + expect(input.validity.valid).toBe(true); + + await user.click(button); + + expect(onSubmit).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/components/select/src/hidden-select.tsx b/packages/components/select/src/hidden-select.tsx index d7c74ef43a..9d24dd3949 100644 --- a/packages/components/select/src/hidden-select.tsx +++ b/packages/components/select/src/hidden-select.tsx @@ -92,15 +92,9 @@ export function useHiddenSelect( inputProps: { type: "text", tabIndex: modality == null || state.isFocused || state.isOpen ? -1 : 0, - autoComplete, - value: [...state.selectedKeys].join(",") ?? "", - required: isRequired, style: {fontSize: 16}, onFocus: () => triggerRef.current?.focus(), disabled: isDisabled, - // The onChange is handled by the `select` element. This avoids the `form` with input `value` - // and no `onChange` warning. - onChange: () => {}, }, selectProps: { name, @@ -108,8 +102,8 @@ export function useHiddenSelect( autoComplete, // TODO: Address validation for cases where an option is selected and then deselected. // required: validationBehavior === "native" && isRequired, + required: isRequired, disabled: isDisabled, - 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 8e2d5535c6..a4c371871e 100644 --- a/packages/hooks/use-aria-multiselect/src/use-multiselect-state.ts +++ b/packages/hooks/use-aria-multiselect/src/use-multiselect-state.ts @@ -71,6 +71,8 @@ export function useMultiSelectState(props: MultiSelectProps): M if (props.selectionMode === "single") { triggerState.close(); } + + validationState.commitValidation(); }, }); @@ -101,7 +103,6 @@ export function useMultiSelectState(props: MultiSelectProps): M if (listState.collection.size !== 0) { setFocusStrategy(focusStrategy); triggerState.toggle(); - validationState.commitValidation(); } }, isFocused,