Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(select): remove input from hidden-select #4427

Merged
merged 4 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/lemon-cheetahs-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@nextui-org/use-aria-multiselect": patch
"@nextui-org/select": patch
---

fixed validationBehavior=native showing browser ui error for select component (#3913)
fixed select not committing error message when validationBehavior=native
1 change: 0 additions & 1 deletion apps/docs/content/docs/components/select.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,6 @@ the popover and listbox components.
- Keyboard support for opening the listbox using the arrow keys, including automatically focusing the first or last item accordingly.
- Typeahead to allow selecting options by typing text, even without opening the listbox.
- Browser autofill integration via a hidden native `<select>` element.
- Support for mobile form navigation via software keyboard.
- Mobile screen reader listbox dismissal support.

<Spacer y={4} />
Expand Down
277 changes: 233 additions & 44 deletions packages/components/select/__tests__/select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1180,30 +1180,136 @@ describe("validation", () => {
});

describe("validationBehavior=aria", () => {
it("supports isRequired", async () => {
it("supports validate function", async () => {
const onSubmit = jest.fn((e) => e.preventDefault());

const {getByTestId} = render(
<Form data-testid="form" validationBehavior="aria" onSubmit={onSubmit}>
<Select
aria-label="Favorite Animal"
data-testid="trigger"
defaultSelectedKeys={["penguin"]}
label="Favorite Animal"
validate={(v) => (v.includes("penguin") ? "Invalid value" : null)}
>
<SelectItem key="penguin">Penguin</SelectItem>
<SelectItem key="zebra">Zebra</SelectItem>
<SelectItem key="shark">Shark</SelectItem>
</Select>
<button data-testid="submit-button" type="submit">
Submit
</button>
</Form>,
);

const trigger = getByTestId("trigger") as HTMLButtonElement;
const select = document.querySelector("select");
const submit = getByTestId("submit-button");

// aria validation is always valid
expect(select?.validity.valid).toBe(true);
// aria validation validates on initial render
expect(trigger).toHaveAttribute("aria-describedby");
expect(select).toHaveAttribute("aria-invalid", "true");
expect(document.getElementById(trigger.getAttribute("aria-describedby")!)).toHaveTextContent(
"Invalid value",
);

await user.click(trigger);

let listboxItems = document.querySelectorAll("[role='option']");

await user.click(listboxItems[1]); // zebra

await user.click(submit);

expect(select?.validity.valid).toBe(true);
expect(trigger).not.toHaveAttribute("aria-describedby");
expect(select).not.toHaveAttribute("aria-invalid");
});

it("supports server validation", async () => {
function FormRender() {
const [serverErrors, setServerErrors] = React.useState({});
const [serverErrors, setServerErrors] = React.useState({animal: "initial error"});

const onSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const value = formData.get("animal");

if (!value || (value !== "cat" && value !== "dog")) {
setServerErrors({
animal: "Please select a cat or dog",
});
} else {
setServerErrors({});
}

setServerErrors({
animal: "new error",
});
};

return (
<Form data-testid="form" validationErrors={serverErrors} onSubmit={onSubmit}>
<Form
data-testid="form"
validationBehavior="aria"
validationErrors={serverErrors}
onSubmit={onSubmit}
>
<Select
aria-label="Favorite Animal"
data-testid="trigger"
label="Favorite Animal"
name="animal"
>
<SelectItem key="penguin">Penguin</SelectItem>
<SelectItem key="zebra">Zebra</SelectItem>
<SelectItem key="shark">Shark</SelectItem>
</Select>
<button data-testid="submit-button" type="submit">
Submit
</button>
</Form>
);
}

const {getByTestId} = render(<FormRender />);

const trigger = getByTestId("trigger") as HTMLButtonElement;
const select = document.querySelector("select");
const submit = getByTestId("submit-button");

// aria validation is always valid
expect(select?.validity.valid).toBe(true);
expect(trigger).toHaveAttribute("aria-describedby");
expect(select).toHaveAttribute("aria-invalid", "true");
expect(document.getElementById(trigger.getAttribute("aria-describedby")!)).toHaveTextContent(
"initial error",
);

await user.click(trigger);

let listboxItems = document.querySelectorAll("[role='option']");

await user.click(listboxItems[1]); // zebra

expect(select?.validity.valid).toBe(true);
expect(trigger).not.toHaveAttribute("aria-describedby");
expect(select).not.toHaveAttribute("aria-invalid");

await user.click(submit);

expect(select?.validity.valid).toBe(true);
expect(trigger).toHaveAttribute("aria-describedby");
expect(select).toHaveAttribute("aria-invalid");
expect(document.getElementById(trigger.getAttribute("aria-describedby")!)).toHaveTextContent(
"new error",
);
});
});

describe("validationBehavior=native", () => {
it("supports isRequired", async () => {
function FormRender() {
const onSubmit = jest.fn((e) => e.preventDefault());

return (
<Form data-testid="form" validationBehavior="native" onSubmit={onSubmit}>
<Select
isRequired
aria-label="Favorite Animal"
data-testid="select"
data-testid="trigger"
label="Favorite Animal"
name="animal"
>
Expand All @@ -1213,7 +1319,7 @@ describe("validation", () => {
<SelectItem key="zebra">Zebra</SelectItem>
<SelectItem key="shark">Shark</SelectItem>
</Select>
<button data-testid="button" type="submit">
<button data-testid="submit-button" type="submit">
Submit
</button>
</Form>
Expand All @@ -1222,75 +1328,158 @@ describe("validation", () => {

const {getByTestId} = render(<FormRender />);

const select = getByTestId("select");
const input = document.querySelector("input");
const trigger = getByTestId("trigger") as HTMLButtonElement;
const select = document.querySelector("select");
const submit = getByTestId("submit-button");

expect(select).not.toHaveAttribute("aria-describedby");
const button = getByTestId("button");
expect(select?.validity.valid).toBe(false);
expect(select?.validity.valueMissing).toBe(true);
// native validation does not validate until submit
expect(select).toHaveAttribute("required");
expect(trigger).not.toHaveAttribute("aria-describedby");

await user.click(button);
await user.click(submit);

expect(select).toHaveAttribute("aria-describedby");
expect(input).toHaveAttribute("aria-required");
expect(select?.validity.valid).toBe(false);
expect(select?.validity.valueMissing).toBe(true);
expect(trigger).toHaveAttribute("aria-describedby");

expect(document.getElementById(select.getAttribute("aria-describedby")!)).toHaveTextContent(
"Please select a cat or dog",
);
await user.click(trigger);

await user.click(select);
let listboxItems = document.querySelectorAll("[role='option']");

await user.click(listboxItems[0]);

await user.click(button);
await user.click(submit);

expect(select).not.toHaveAttribute("aria-describedby");
expect(select?.validity.valid).toBe(true);
expect(trigger).not.toHaveAttribute("aria-describedby");
});

it("supports validate function", async () => {
const onSubmit = jest.fn((e) => e.preventDefault());

const {getByTestId} = render(
<Form data-testid="form">
<Form data-testid="form" validationBehavior="native" onSubmit={onSubmit}>
<Select
aria-label="Favorite Animal"
data-testid="select"
data-testid="trigger"
defaultSelectedKeys={["penguin"]}
label="Favorite Animal"
validate={(v) => (v.includes("penguin") ? "Invalid value" : null)}
validationBehavior="aria"
>
<SelectItem key="penguin">Penguin</SelectItem>
<SelectItem key="zebra">Zebra</SelectItem>
<SelectItem key="shark">Shark</SelectItem>
</Select>
<button data-testid="button" type="submit">
<button data-testid="submit-button" type="submit">
Submit
</button>
</Form>,
);

const select = getByTestId("select");
const input = document.querySelector("input");
const button = getByTestId("button");
const trigger = getByTestId("trigger") as HTMLButtonElement;
const select = document.querySelector("select");
const submit = getByTestId("submit-button");

expect(select?.validity.valid).toBe(false);
expect(select?.validity.customError).toBe(true);
// native validation does not validate until submit
expect(trigger).not.toHaveAttribute("aria-describedby");
expect(select).not.toHaveAttribute("aria-invalid", "true");

expect(select).toHaveAttribute("aria-describedby");
expect(input).toHaveAttribute("aria-invalid", "true");
await user.click(submit);

expect(document.getElementById(select.getAttribute("aria-describedby")!)).toHaveTextContent(
expect(select?.validity.valid).toBe(false);
expect(select?.validity.customError).toBe(true);
expect(trigger).toHaveAttribute("aria-describedby");
expect(select).toHaveAttribute("aria-invalid", "true");
expect(document.getElementById(trigger.getAttribute("aria-describedby")!)).toHaveTextContent(
"Invalid value",
);

expect(input?.validity.valid).toBe(true);

await user.click(select);
await user.click(trigger);

let listboxItems = document.querySelectorAll("[role='option']");

await user.click(listboxItems[1]); // Select "Zebra"
await user.click(listboxItems[1]); // zebra

await user.click(button);
await user.click(submit);

expect(select).not.toHaveAttribute("aria-describedby");
expect(select?.validity.valid).toBe(true);
expect(trigger).not.toHaveAttribute("aria-describedby");
expect(select).not.toHaveAttribute("aria-invalid");
});

it("supports server validation", async () => {
function FormRender() {
const [serverErrors, setServerErrors] = React.useState({animal: "initial error"});

const onSubmit = (e) => {
e.preventDefault();

setServerErrors({
animal: "new error",
});
};

return (
<Form
data-testid="form"
validationBehavior="native"
validationErrors={serverErrors}
onSubmit={onSubmit}
>
<Select
aria-label="Favorite Animal"
data-testid="trigger"
label="Favorite Animal"
name="animal"
>
<SelectItem key="penguin">Penguin</SelectItem>
<SelectItem key="zebra">Zebra</SelectItem>
<SelectItem key="shark">Shark</SelectItem>
</Select>
<button data-testid="submit-button" type="submit">
Submit
</button>
</Form>
);
}

const {getByTestId} = render(<FormRender />);

const trigger = getByTestId("trigger") as HTMLButtonElement;
const select = document.querySelector("select");
const submit = getByTestId("submit-button");

expect(select?.validity.valid).toBe(false);
expect(select?.validity.customError).toBe(true);
expect(trigger).toHaveAttribute("aria-describedby");
expect(select).toHaveAttribute("aria-invalid", "true");
expect(document.getElementById(trigger.getAttribute("aria-describedby")!)).toHaveTextContent(
"initial error",
);

await user.click(trigger);

let listboxItems = document.querySelectorAll("[role='option']");

await user.click(listboxItems[1]); // zebra

expect(select?.validity.valid).toBe(true);
expect(trigger).not.toHaveAttribute("aria-describedby");
expect(select).not.toHaveAttribute("aria-invalid");

await user.click(submit);

expect(select?.validity.valid).toBe(false);
expect(select?.validity.customError).toBe(true);
expect(trigger).toHaveAttribute("aria-describedby");
expect(select).toHaveAttribute("aria-invalid");
expect(document.getElementById(trigger.getAttribute("aria-describedby")!)).toHaveTextContent(
"new error",
);
});
});
});
Loading
Loading