fix(form): use native as default validation behavior (#4425)

* fix(form): use native as default validation behavior

* docs(form): delete explicit validationBehavior=native

* test(form): adjusted form test validation behaviors

* chore(form): adjusted stories with forms

* chore(changeset): changed form default validation behavior to native

* chore(changeset): removed packages with only test changes

* chore(changeset): change to patch

* chore(changeset): update package name

* refactor(docs): update package name

* refactor(docs): update to heroui

---------

Co-authored-by: աӄա <wingkwong.code@gmail.com>
This commit is contained in:
Peterl561 2025-01-30 21:54:56 +08:00 committed by GitHub
parent be15943a00
commit a66476d60c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 106 additions and 76 deletions

View File

@ -0,0 +1,5 @@
---
"@heroui/form": patch
---
changed form default validation behavior to native

View File

@ -13,7 +13,7 @@ export default function App() {
};
return (
<Form className="w-full max-w-xs" validationBehavior="native" onSubmit={onSubmit}>
<Form className="w-full max-w-xs" onSubmit={onSubmit}>
<Input
isRequired
errorMessage="Please enter a valid email"

View File

@ -6,7 +6,7 @@ export default function App() {
};
return (
<Form className="w-full max-w-xs" validationBehavior="native" onSubmit={onSubmit}>
<Form className="w-full max-w-xs" onSubmit={onSubmit}>
<Input
isRequired
errorMessage={({validationDetails}) => {

View File

@ -0,0 +1,30 @@
import {Form, Input, Button} from "@heroui-org/react";
export default function App() {
const onSubmit = (e) => {
e.preventDefault();
};
return (
<Form className="w-full max-w-xs" validationBehavior="aria" onSubmit={onSubmit}>
<Input
isRequired
label="Username"
labelPlacement="outside"
name="username"
placeholder="Enter your username"
type="text"
validate={(value) => {
if (value.length < 3) {
return "Username must be at least 3 characters long";
}
return value === "admin" ? "Nice try!" : null;
}}
/>
<Button type="submit" variant="bordered">
Submit
</Button>
</Form>
);
}

View File

@ -0,0 +1,9 @@
import App from "./custom-validation-aria.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -6,7 +6,7 @@ export default function App() {
};
return (
<Form className="w-full max-w-xs" validationBehavior="native" onSubmit={onSubmit}>
<Form className="w-full max-w-xs" onSubmit={onSubmit}>
<Input
isRequired
label="Username"

View File

@ -59,7 +59,6 @@ export default function App() {
return (
<Form
className="w-full justify-center items-center space-y-4"
validationBehavior="native"
validationErrors={errors}
onReset={() => setSubmitted(null)}
onSubmit={onSubmit}

View File

@ -6,7 +6,6 @@ export default function App() {
return (
<Form
className="w-full max-w-xs flex flex-col gap-4"
validationBehavior="native"
onReset={() => setAction("reset")}
onSubmit={(e) => {
e.preventDefault();

View File

@ -12,7 +12,7 @@ export default function App() {
};
return (
<Form className="w-full max-w-xs" validationBehavior="native" onSubmit={onSubmit}>
<Form className="w-full max-w-xs" onSubmit={onSubmit}>
<Input
isRequired
errorMessage="Please enter a valid email"

View File

@ -4,6 +4,7 @@ import controlled from "./controlled";
import nativeValidation from "./native-validation";
import customErrorMessages from "./custom-error-messages";
import customValidation from "./custom-validation";
import customValidationAria from "./custom-validation-aria";
import realTimeValidation from "./real-time-validation";
import serverValidation from "./server-validation";
import events from "./events";
@ -15,6 +16,7 @@ export const formContent = {
nativeValidation,
customErrorMessages,
customValidation,
customValidationAria,
realTimeValidation,
serverValidation,
events,

View File

@ -6,7 +6,7 @@ export default function App() {
};
return (
<Form className="w-full max-w-xs" validationBehavior="native" onSubmit={onSubmit}>
<Form className="w-full max-w-xs" onSubmit={onSubmit}>
<Input
isRequired
errorMessage="Please enter a valid email"

View File

@ -12,7 +12,7 @@ export default function App() {
};
return (
<Form className="w-full max-w-xs" validationBehavior="native" onSubmit={onSubmit}>
<Form className="w-full max-w-xs" onSubmit={onSubmit}>
<Input
isRequired
errorMessage="Please enter a valid email"

View File

@ -6,7 +6,6 @@ export default function App() {
return (
<Form
className="flex w-full flex-col items-start gap-4"
validationBehavior="native"
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
@ -21,7 +20,6 @@ export default function App() {
length={4}
name="otp"
placeholder="Enter code"
validationBehavior="native"
/>
<Button size="sm" type="submit" variant="bordered">
Submit

View File

@ -20,7 +20,6 @@ export default function App() {
length={4}
name="otp"
placeholder="Enter code"
validationBehavior="native"
/>
<Button size="sm" type="submit" variant="bordered">
Submit

View File

@ -11,7 +11,7 @@ export default function App() {
};
return (
<Form className="w-full max-w-xs" validationBehavior="native" onSubmit={onSubmit}>
<Form className="w-full max-w-xs" onSubmit={onSubmit}>
<Input
isRequired
errorMessage={({validationDetails, validationErrors}) => {

View File

@ -11,7 +11,7 @@ export default function App() {
};
return (
<Form className="w-full max-w-xs" validationBehavior="native" onSubmit={onSubmit}>
<Form className="w-full max-w-xs" onSubmit={onSubmit}>
<Input
isRequired
label="Username"

View File

@ -23,7 +23,7 @@ export default function App() {
}
return (
<Form className="w-full max-w-xs" validationBehavior="native" onSubmit={onSubmit}>
<Form className="w-full max-w-xs" onSubmit={onSubmit}>
<Input
errorMessage={() => (
<ul>

View File

@ -16,12 +16,7 @@ export default function App() {
};
return (
<Form
className="w-full max-w-xs"
validationBehavior="native"
validationErrors={errors}
onSubmit={onSubmit}
>
<Form className="w-full max-w-xs" validationErrors={errors} onSubmit={onSubmit}>
<Input
isRequired
isDisabled={isLoading}

View File

@ -67,11 +67,11 @@ Here's the supported locales. By default, It is `en-US`.
```tsx
const localeValues = [
'fr-FR', 'fr-CA', 'de-DE', 'en-US', 'en-GB', 'ja-JP',
'da-DK', 'nl-NL', 'fi-FI', 'it-IT', 'nb-NO', 'es-ES',
'sv-SE', 'pt-BR', 'zh-CN', 'zh-TW', 'ko-KR', 'bg-BG',
'hr-HR', 'cs-CZ', 'et-EE', 'hu-HU', 'lv-LV', 'lt-LT',
'pl-PL', 'ro-RO', 'ru-RU', 'sr-SP', 'sk-SK', 'sl-SI',
'tr-TR', 'uk-UA', 'ar-AE', 'ar-DZ', 'AR-EG', 'ar-SA',
'da-DK', 'nl-NL', 'fi-FI', 'it-IT', 'nb-NO', 'es-ES',
'sv-SE', 'pt-BR', 'zh-CN', 'zh-TW', 'ko-KR', 'bg-BG',
'hr-HR', 'cs-CZ', 'et-EE', 'hu-HU', 'lv-LV', 'lt-LT',
'pl-PL', 'ro-RO', 'ru-RU', 'sr-SP', 'sk-SK', 'sl-SI',
'tr-TR', 'uk-UA', 'ar-AE', 'ar-DZ', 'AR-EG', 'ar-SA',
'el-GR', 'he-IL', 'fa-AF', 'am-ET', 'hi-IN', 'th-TH'
];
```
@ -112,7 +112,7 @@ interface AppProviderProps {
`createCalendar`
- **Description**:
- **Description**:
This function helps to reduce the bundle size by providing a custom calendar system.
By default, this includes all calendar systems supported by `@internationalized/date`. However,
@ -182,14 +182,14 @@ interface AppProviderProps {
`validationBehavior`
- **Description**: Whether to use native HTML form validation to prevent form submission when the value is missing or invalid,
- **Description**: Whether to use native HTML form validation to prevent form submission when the value is missing or invalid,
or mark the field as required or invalid via ARIA.
- **Type**: `native | aria`
- **Default**: `aria`
- **Default**: `native`
`reducedMotion`
- **Description**: Controls the motion preferences for the entire application, allowing developers to respect user settings for reduced motion.
- **Description**: Controls the motion preferences for the entire application, allowing developers to respect user settings for reduced motion.
The available options are:
- `"user"`: Adapts to the user's device settings for reduced motion.
- `"always"`: Disables all animations.

View File

@ -72,11 +72,12 @@ See the [Forms](/docs/guide/forms) guide to learn more about form validation, in
### Validation Behavior
`Form` validation uses ARIA attributes by default, but can be switched to native HTML validation by setting `validationBehavior="native"`. ARIA validation shows realtime errors without blocking submission. This can be set at the form or field level.
`Form` validation uses native validation behavior by default, but can be switched to ARIA validation by setting `validationBehavior="aria"`. ARIA validation shows realtime errors without blocking submission. This can be set at the form or field level.
To set the default behavior at the app level, you can change the form defaults for your entire app using [HeroUI Provider](/docs/api-references/heroui-provider).
```tsx
<Form validationBehavior="native">
<Form validationBehavior="aria">
<Input
isRequired
name="username"
@ -95,7 +96,7 @@ See the [Forms](/docs/guide/forms) guide to learn more about form validation, in
</Form>
```
<CodeDemo title="Validation Behavior" files={formContent.customValidation} />
<CodeDemo title="Validation Behavior" files={formContent.customValidationAria} />
## Accessibility
@ -123,7 +124,7 @@ See the [Forms](/docs/guide/forms) guide to learn more about form validation, in
type: "'native' | 'aria'",
description:
"Whether to use native HTML form validation to prevent form submission when a field value is missing or invalid, or mark fields as required or invalid via ARIA.",
default: "aria",
default: "native",
},
{
attribute: "validationErrors",

View File

@ -127,7 +127,7 @@ You can use the `value` and `onValueChange` properties to control the input valu
### With Form
`Input` can be used with a `Form` component to leverage form state management. By default, `Form` components use `validationBehavior="aria"`, which will not block form submission if any inputs are invalid. For more on form and validation behaviors, see the [Forms](/docs/guide/forms) guide.
`Input` can be used with a `Form` component to leverage form state management. For more on form and validation behaviors, see the [Forms](/docs/guide/forms) guide.
#### Built-in Validation

View File

@ -65,7 +65,7 @@ function Example() {
};
return (
<Form onSubmit={onSubmit} validationBehavior="native">
<Form onSubmit={onSubmit}>
<Input
isRequired
errorMessage="Please enter a valid email"
@ -111,7 +111,7 @@ function Example() {
};
return (
<Form className="w-full max-w-xs" validationBehavior="native" onSubmit={onSubmit}>
<Form className="w-full max-w-xs" onSubmit={onSubmit}>
<Input
isRequired
errorMessage="Please enter a valid email"
@ -155,7 +155,7 @@ export default function App() {
};
return (
<Form validationBehavior="native" onSubmit={onSubmit}>
<Form onSubmit={onSubmit}>
<Input
isRequired
errorMessage={({validationDetails}) => {
@ -208,7 +208,7 @@ export default function App() {
};
return (
<Form validationBehavior="native" onSubmit={onSubmit}>
<Form onSubmit={onSubmit}>
<Input
isRequired
label="Email"
@ -225,8 +225,8 @@ export default function App() {
}
```
To enable native validation, set `validationBehavior="native"`.
By default, `validationBehavior="aria"` is set, which only marks the field as required or invalid for assistive technologies, without preventing form submission.
To enable ARIA validation, set `validationBehavior="aria"`.
When`validationBehavior="aria"` is set, fields are only marked as required or invalid for assistive technologies, without preventing form submission.
You can change the form defaults for your entire app using [HeroUI Provider](/docs/api-references/heroui-provider).
Supported constraints include:
@ -256,7 +256,7 @@ export default function App() {
};
return (
<Form validationBehavior="native" onSubmit={onSubmit}>
<Form onSubmit={onSubmit}>
<Input
isRequired
label="Username"

View File

@ -702,8 +702,8 @@ describe("Autocomplete", () => {
describe("validationBehavior=native", () => {
it("supports isRequired", async () => {
const {getByTestId, getByRole, findByRole} = render(
<Form data-testid="form">
<AutocompleteExample isRequired validationBehavior="native" />
<Form data-testid="form" validationBehavior="native">
<AutocompleteExample isRequired />
</Form>,
);
@ -744,8 +744,8 @@ describe("Autocomplete", () => {
};
return (
<Form validationErrors={serverErrors} onSubmit={onSubmit}>
<AutocompleteExample data-testid="input" name="value" validationBehavior="native" />
<Form validationBehavior="native" validationErrors={serverErrors} onSubmit={onSubmit}>
<AutocompleteExample data-testid="input" name="value" />
<button data-testid="submit" type="submit">
Submit
</button>
@ -881,7 +881,7 @@ describe("Autocomplete", () => {
it("supports server validation", async () => {
const {getByTestId, getByRole} = render(
<Form validationErrors={{value: "Invalid value"}}>
<Form validationBehavior="aria" validationErrors={{value: "Invalid value"}}>
<AutocompleteExample data-testid="input" name="value" />
</Form>,
);

View File

@ -294,12 +294,8 @@ describe("Checkbox.Group", () => {
};
return (
<Form validationErrors={serverErrors} onSubmit={onSubmit}>
<CheckboxGroup
label="Agree to the following"
name="terms"
validationBehavior="native"
>
<Form validationBehavior="native" validationErrors={serverErrors} onSubmit={onSubmit}>
<CheckboxGroup label="Agree to the following" name="terms">
<Checkbox value="terms">Terms and conditions</Checkbox>
<Checkbox value="cookies">Cookies</Checkbox>
<Checkbox value="privacy">Privacy policy</Checkbox>

View File

@ -153,12 +153,7 @@ const ServerValidationTemplate = (args: CheckboxGroupProps) => {
validationErrors={serverErrors}
onSubmit={onSubmit}
>
<CheckboxGroup
{...args}
label="Agree to the following"
name="terms"
validationBehavior="native"
>
<CheckboxGroup {...args} label="Agree to the following" name="terms">
<Checkbox value="terms">Terms and conditions</Checkbox>
<Checkbox value="cookies">Cookies</Checkbox>
<Checkbox value="privacy">Privacy policy</Checkbox>

View File

@ -180,7 +180,7 @@ const WithFormTemplate = (args: CheckboxProps) => {
};
return (
<Form validationBehavior="native" validationErrors={errors} onSubmit={onSubmit}>
<Form validationErrors={errors} onSubmit={onSubmit}>
<Checkbox
isRequired
classNames={{

View File

@ -794,13 +794,12 @@ describe("DatePicker", () => {
it("supports validate function", async () => {
const {getByRole, getByTestId} = render(
<Form data-testid="form">
<Form data-testid="form" validationBehavior="native">
<DatePicker
defaultValue={new CalendarDate(2020, 2, 3)}
label="Value"
name="date"
validate={(v) => (v.year < 2022 ? "Invalid value" : null)}
validationBehavior="native"
/>
</Form>,
);
@ -842,8 +841,8 @@ describe("DatePicker", () => {
};
return (
<Form validationErrors={serverErrors} onSubmit={onSubmit}>
<DatePicker label="Value" name="date" validationBehavior="native" />
<Form validationBehavior="native" validationErrors={serverErrors} onSubmit={onSubmit}>
<DatePicker label="Value" name="date" />
<button data-testid="submit" type="submit">
Submit
</button>
@ -882,7 +881,7 @@ describe("DatePicker", () => {
describe("validationBehavior=aria", () => {
it("supports minValue and maxValue", async () => {
const {getByRole} = render(
<Form data-testid="form">
<Form data-testid="form" validationBehavior="aria">
<DatePicker
defaultValue={new CalendarDate(2019, 2, 3)}
label="Date"
@ -915,7 +914,7 @@ describe("DatePicker", () => {
it("supports validate function", async () => {
const {getByRole} = render(
<Form data-testid="form">
<Form data-testid="form" validationBehavior="aria">
<DatePicker
defaultValue={new CalendarDate(2020, 2, 3)}
label="Value"
@ -942,7 +941,7 @@ describe("DatePicker", () => {
it("supports server validation", async () => {
const {getByRole} = render(
<Form validationErrors={{value: "Invalid value"}}>
<Form validationBehavior="aria" validationErrors={{value: "Invalid value"}}>
<DatePicker defaultValue={new CalendarDate(2020, 2, 3)} label="Value" name="value" />
</Form>,
);

View File

@ -8,7 +8,7 @@ import {Form as AriaForm, FormProps} from "./base-form";
export const Form = forwardRef(function Form(props: FormProps, ref: ForwardedRef<HTMLFormElement>) {
const globalContext = useProviderContext();
const validationBehavior =
props.validationBehavior ?? globalContext?.validationBehavior ?? "aria";
props.validationBehavior ?? globalContext?.validationBehavior ?? "native";
return <AriaForm {...props} ref={ref} validationBehavior={validationBehavior} />;
});

View File

@ -129,7 +129,6 @@ const RequiredTemplate = (args) => {
length={4}
name="otp"
placeholder="Enter code"
validationBehavior="native"
{...args}
/>
<Button size="sm" type="submit" variant="bordered">

View File

@ -310,8 +310,8 @@ describe("Input with React Hook Form", () => {
describe("validationBehavior=native", () => {
it("supports isRequired", async () => {
const {getByTestId} = render(
<Form data-testid="form">
<Input isRequired data-testid="input" label="Name" validationBehavior="native" />
<Form data-testid="form" validationBehavior="native">
<Input isRequired data-testid="input" label="Name" />
</Form>,
);
@ -344,13 +344,12 @@ describe("Input with React Hook Form", () => {
it("supports validate function", async () => {
const {getByTestId} = render(
<Form data-testid="form">
<Form data-testid="form" validationBehavior="native">
<Input
data-testid="input"
defaultValue="Foo"
label="Name"
validate={(v) => (v === "Foo" ? "Invalid name" : null)}
validationBehavior="native"
/>
</Form>,
);
@ -391,8 +390,13 @@ describe("Input with React Hook Form", () => {
};
return (
<Form data-testid="form" validationErrors={serverErrors} onSubmit={onSubmit}>
<Input data-testid="input" label="Name" name="name" validationBehavior="native" />
<Form
data-testid="form"
validationBehavior="native"
validationErrors={serverErrors}
onSubmit={onSubmit}
>
<Input data-testid="input" label="Name" name="name" />
<button data-testid="submit" type="submit">
Submit
</button>
@ -442,7 +446,7 @@ describe("Input with React Hook Form", () => {
describe('validationBehavior="aria"', () => {
it("supports validate function", async () => {
const {getByTestId} = render(
<Form data-testid="form">
<Form data-testid="form" validationBehavior="aria">
<Input
data-testid="input"
defaultValue="Foo"
@ -470,7 +474,7 @@ describe("Input with React Hook Form", () => {
it("supports server validation", async () => {
const {getByTestId} = render(
<Form validationErrors={{name: "Invalid name"}}>
<Form validationBehavior="aria" validationErrors={{name: "Invalid name"}}>
<Input data-testid="input" label="Name" name="name" />
</Form>,
);