Skip to content

Commit 94a1176

Browse files
authored
RAC: Add validationBehavior to Form (#6062)
1 parent bceab7b commit 94a1176

File tree

11 files changed

+185
-37
lines changed

11 files changed

+185
-37
lines changed

packages/react-aria-components/src/Checkbox.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {CheckboxGroupState, useCheckboxGroupState, useToggleState} from 'react-s
1414
import {ContextValue, forwardRefType, Provider, RACValidation, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot} from './utils';
1515
import {FieldErrorContext} from './FieldError';
1616
import {filterDOMProps, mergeRefs, useObjectRef} from '@react-aria/utils';
17+
import {FormValidationBehaviorContext} from './Form';
1718
import {LabelContext} from './Label';
1819
import React, {createContext, ForwardedRef, forwardRef, MutableRefObject, useContext} from 'react';
1920
import {TextContext} from './Text';
@@ -111,15 +112,17 @@ export const CheckboxGroupStateContext = createContext<CheckboxGroupState | null
111112

112113
function CheckboxGroup(props: CheckboxGroupProps, ref: ForwardedRef<HTMLDivElement>) {
113114
[props, ref] = useContextProps(props, ref, CheckboxGroupContext);
115+
let formValidationBehavior = useContext(FormValidationBehaviorContext);
116+
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
114117
let state = useCheckboxGroupState({
115118
...props,
116-
validationBehavior: props.validationBehavior ?? 'native'
119+
validationBehavior
117120
});
118121
let [labelRef, label] = useSlot();
119122
let {groupProps, labelProps, descriptionProps, errorMessageProps, ...validation} = useCheckboxGroup({
120123
...props,
121124
label,
122-
validationBehavior: props.validationBehavior ?? 'native'
125+
validationBehavior
123126
}, state);
124127

125128
let renderProps = useRenderProps({
@@ -170,6 +173,8 @@ function Checkbox(props: CheckboxProps, ref: ForwardedRef<HTMLLabelElement>) {
170173
...otherProps
171174
} = props;
172175
[props, ref] = useContextProps(otherProps, ref, CheckboxContext);
176+
let formValidationBehavior = useContext(FormValidationBehaviorContext);
177+
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
173178
let groupState = useContext(CheckboxGroupStateContext);
174179
let inputRef = useObjectRef(mergeRefs(userProvidedInputRef, props.inputRef !== undefined ? props.inputRef : null));
175180
let {labelProps, inputProps, isSelected, isDisabled, isReadOnly, isPressed, isInvalid} = groupState
@@ -187,7 +192,7 @@ function Checkbox(props: CheckboxProps, ref: ForwardedRef<HTMLLabelElement>) {
187192
: useCheckbox({
188193
...props,
189194
children: typeof props.children === 'function' ? true : props.children,
190-
validationBehavior: props.validationBehavior ?? 'native'
195+
validationBehavior
191196
// eslint-disable-next-line react-hooks/rules-of-hooks
192197
}, useToggleState(props), inputRef);
193198
let {isFocused, isFocusVisible, focusProps} = useFocusRing();

packages/react-aria-components/src/ComboBox.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ import {CollectionDocumentContext, useCollectionDocument} from './Collection';
1616
import {ContextValue, forwardRefType, Hidden, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot} from './utils';
1717
import {FieldErrorContext} from './FieldError';
1818
import {filterDOMProps, useResizeObserver} from '@react-aria/utils';
19+
import {FormValidationBehaviorContext} from './Form';
1920
import {GroupContext} from './Group';
2021
import {InputContext} from './Input';
2122
import {LabelContext} from './Label';
2223
import {ListBoxContext, ListStateContext} from './ListBox';
2324
import {OverlayTriggerStateContext} from './Dialog';
2425
import {PopoverContext} from './Popover';
25-
import React, {createContext, ForwardedRef, forwardRef, RefObject, useCallback, useMemo, useRef, useState} from 'react';
26+
import React, {createContext, ForwardedRef, forwardRef, RefObject, useCallback, useContext, useMemo, useRef, useState} from 'react';
2627
import {TextContext} from './Text';
2728

2829
export interface ComboBoxRenderProps {
@@ -113,6 +114,8 @@ function ComboBoxInner<T extends object>({props, collection, comboBoxRef: ref}:
113114
formValue = 'text';
114115
}
115116

117+
let formValidationBehavior = useContext(FormValidationBehaviorContext);
118+
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
116119
let {contains} = useFilter({sensitivity: 'base'});
117120
let state = useComboBoxState({
118121
defaultFilter: props.defaultFilter || contains,
@@ -121,7 +124,7 @@ function ComboBoxInner<T extends object>({props, collection, comboBoxRef: ref}:
121124
items: props.items,
122125
children: undefined,
123126
collection,
124-
validationBehavior: props.validationBehavior ?? 'native'
127+
validationBehavior
125128
});
126129

127130
let buttonRef = useRef<HTMLButtonElement>(null);
@@ -145,7 +148,7 @@ function ComboBoxInner<T extends object>({props, collection, comboBoxRef: ref}:
145148
listBoxRef,
146149
popoverRef,
147150
name: formValue === 'text' ? name : undefined,
148-
validationBehavior: props.validationBehavior ?? 'native'
151+
validationBehavior
149152
}, state);
150153

151154
// Make menu width match input + button

packages/react-aria-components/src/DateField.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {createCalendar} from '@internationalized/date';
1515
import {DateFieldState, DateSegmentType, DateSegment as IDateSegment, TimeFieldState, useDateFieldState, useTimeFieldState} from 'react-stately';
1616
import {FieldErrorContext} from './FieldError';
1717
import {filterDOMProps, useObjectRef} from '@react-aria/utils';
18+
import {FormValidationBehaviorContext} from './Form';
1819
import {Group, GroupContext} from './Group';
1920
import {Input, InputContext} from './Input';
2021
import {LabelContext} from './Label';
@@ -47,12 +48,14 @@ export const TimeFieldStateContext = createContext<TimeFieldState | null>(null);
4748

4849
function DateField<T extends DateValue>(props: DateFieldProps<T>, ref: ForwardedRef<HTMLDivElement>) {
4950
[props, ref] = useContextProps(props, ref, DateFieldContext);
51+
let formValidationBehavior = useContext(FormValidationBehaviorContext);
52+
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
5053
let {locale} = useLocale();
5154
let state = useDateFieldState({
5255
...props,
5356
locale,
5457
createCalendar,
55-
validationBehavior: props.validationBehavior ?? 'native'
58+
validationBehavior
5659
});
5760

5861
let fieldRef = useRef<HTMLDivElement>(null);
@@ -62,7 +65,7 @@ function DateField<T extends DateValue>(props: DateFieldProps<T>, ref: Forwarded
6265
...removeDataAttributes(props),
6366
label,
6467
inputRef,
65-
validationBehavior: props.validationBehavior ?? 'native'
68+
validationBehavior
6669
}, state, fieldRef);
6770

6871
let renderProps = useRenderProps({
@@ -112,11 +115,13 @@ export {_DateField as DateField};
112115

113116
function TimeField<T extends TimeValue>(props: TimeFieldProps<T>, ref: ForwardedRef<HTMLDivElement>) {
114117
[props, ref] = useContextProps(props, ref, TimeFieldContext);
118+
let formValidationBehavior = useContext(FormValidationBehaviorContext);
119+
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
115120
let {locale} = useLocale();
116121
let state = useTimeFieldState({
117122
...props,
118123
locale,
119-
validationBehavior: props.validationBehavior ?? 'native'
124+
validationBehavior
120125
});
121126

122127
let fieldRef = useRef<HTMLDivElement>(null);
@@ -126,7 +131,7 @@ function TimeField<T extends TimeValue>(props: TimeFieldProps<T>, ref: Forwarded
126131
...removeDataAttributes(props),
127132
label,
128133
inputRef,
129-
validationBehavior: props.validationBehavior ?? 'native'
134+
validationBehavior
130135
}, state, fieldRef);
131136

132137
let renderProps = useRenderProps({

packages/react-aria-components/src/DatePicker.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ import {DatePickerState, DatePickerStateOptions, DateRangePickerState, DateRange
1818
import {DialogContext, OverlayTriggerStateContext} from './Dialog';
1919
import {FieldErrorContext} from './FieldError';
2020
import {filterDOMProps, useResizeObserver} from '@react-aria/utils';
21+
import {FormValidationBehaviorContext} from './Form';
2122
import {GroupContext} from './Group';
2223
import {LabelContext} from './Label';
2324
import {PopoverContext} from './Popover';
24-
import React, {createContext, ForwardedRef, forwardRef, useCallback, useRef, useState} from 'react';
25+
import React, {createContext, ForwardedRef, forwardRef, useCallback, useContext, useRef, useState} from 'react';
2526
import {TextContext} from './Text';
2627

2728
export interface DatePickerRenderProps {
@@ -72,9 +73,11 @@ export const DateRangePickerStateContext = createContext<DateRangePickerState |
7273

7374
function DatePicker<T extends DateValue>(props: DatePickerProps<T>, ref: ForwardedRef<HTMLDivElement>) {
7475
[props, ref] = useContextProps(props, ref, DatePickerContext);
76+
let formValidationBehavior = useContext(FormValidationBehaviorContext);
77+
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
7578
let state = useDatePickerState({
7679
...props,
77-
validationBehavior: props.validationBehavior ?? 'native'
80+
validationBehavior
7881
});
7982

8083
let groupRef = useRef<HTMLDivElement>(null);
@@ -92,7 +95,7 @@ function DatePicker<T extends DateValue>(props: DatePickerProps<T>, ref: Forward
9295
} = useDatePicker({
9396
...removeDataAttributes(props),
9497
label,
95-
validationBehavior: props.validationBehavior ?? 'native'
98+
validationBehavior
9699
}, state, groupRef);
97100

98101
// Allows calendar width to match input group
@@ -173,9 +176,11 @@ export {_DatePicker as DatePicker};
173176

174177
function DateRangePicker<T extends DateValue>(props: DateRangePickerProps<T>, ref: ForwardedRef<HTMLDivElement>) {
175178
[props, ref] = useContextProps(props, ref, DateRangePickerContext);
179+
let formValidationBehavior = useContext(FormValidationBehaviorContext);
180+
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
176181
let state = useDateRangePickerState({
177182
...props,
178-
validationBehavior: props.validationBehavior ?? 'native'
183+
validationBehavior
179184
});
180185

181186
let groupRef = useRef<HTMLDivElement>(null);
@@ -194,7 +199,7 @@ function DateRangePicker<T extends DateValue>(props: DateRangePickerProps<T>, re
194199
} = useDateRangePicker({
195200
...removeDataAttributes(props),
196201
label,
197-
validationBehavior: props.validationBehavior ?? 'native'
202+
validationBehavior
198203
}, state, groupRef);
199204

200205
// Allows calendar width to match input group

packages/react-aria-components/src/Form.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,30 @@
1212

1313
import {DOMProps} from './utils';
1414
import {FormValidationContext} from 'react-stately';
15-
import React, {ForwardedRef, forwardRef} from 'react';
15+
import React, {createContext, ForwardedRef, forwardRef} from 'react';
1616
import {FormProps as SharedFormProps} from '@react-types/form';
1717

18-
export interface FormProps extends SharedFormProps, DOMProps {}
18+
export interface FormProps extends SharedFormProps, DOMProps {
19+
/**
20+
* Whether to use native HTML form validation to prevent form submission
21+
* when a field value is missing or invalid, or mark fields as required
22+
* or invalid via ARIA.
23+
* @default 'native'
24+
*/
25+
validationBehavior?: 'aria' | 'native'
26+
}
27+
28+
export const FormValidationBehaviorContext = createContext<FormProps['validationBehavior'] | null>(null);
1929

2030
function Form(props: FormProps, ref: ForwardedRef<HTMLFormElement>) {
21-
let {validationErrors, children, className, ...domProps} = props;
31+
let {validationErrors, validationBehavior = 'native', children, className, ...domProps} = props;
2232
return (
23-
<form {...domProps} ref={ref} className={className || 'react-aria-Form'}>
24-
<FormValidationContext.Provider value={validationErrors ?? {}}>
25-
{children}
26-
</FormValidationContext.Provider>
33+
<form noValidate={validationBehavior !== 'native'} {...domProps} ref={ref} className={className || 'react-aria-Form'}>
34+
<FormValidationBehaviorContext.Provider value={validationBehavior}>
35+
<FormValidationContext.Provider value={validationErrors ?? {}}>
36+
{children}
37+
</FormValidationContext.Provider>
38+
</FormValidationBehaviorContext.Provider>
2739
</form>
2840
);
2941
}

packages/react-aria-components/src/NumberField.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ import {ButtonContext} from './Button';
1515
import {ContextValue, forwardRefType, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot} from './utils';
1616
import {FieldErrorContext} from './FieldError';
1717
import {filterDOMProps} from '@react-aria/utils';
18+
import {FormValidationBehaviorContext} from './Form';
1819
import {GroupContext} from './Group';
1920
import {InputContext} from './Input';
2021
import {InputDOMProps} from '@react-types/shared';
2122
import {LabelContext} from './Label';
2223
import {NumberFieldState, useNumberFieldState} from 'react-stately';
23-
import React, {createContext, ForwardedRef, forwardRef, useRef} from 'react';
24+
import React, {createContext, ForwardedRef, forwardRef, useContext, useRef} from 'react';
2425
import {TextContext} from './Text';
2526

2627
export interface NumberFieldRenderProps {
@@ -47,11 +48,13 @@ export const NumberFieldStateContext = createContext<NumberFieldState | null>(nu
4748

4849
function NumberField(props: NumberFieldProps, ref: ForwardedRef<HTMLDivElement>) {
4950
[props, ref] = useContextProps(props, ref, NumberFieldContext);
51+
let formValidationBehavior = useContext(FormValidationBehaviorContext);
52+
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
5053
let {locale} = useLocale();
5154
let state = useNumberFieldState({
5255
...props,
5356
locale,
54-
validationBehavior: props.validationBehavior ?? 'native'
57+
validationBehavior
5558
});
5659

5760
let inputRef = useRef<HTMLInputElement>(null);
@@ -68,7 +71,7 @@ function NumberField(props: NumberFieldProps, ref: ForwardedRef<HTMLDivElement>)
6871
} = useNumberField({
6972
...removeDataAttributes(props),
7073
label,
71-
validationBehavior: props.validationBehavior ?? 'native'
74+
validationBehavior
7275
}, state, inputRef);
7376

7477
let renderProps = useRenderProps({

packages/react-aria-components/src/RadioGroup.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import {AriaRadioGroupProps, AriaRadioProps, HoverEvents, Orientation, useFocusR
1414
import {ContextValue, forwardRefType, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot} from './utils';
1515
import {FieldErrorContext} from './FieldError';
1616
import {filterDOMProps, mergeProps} from '@react-aria/utils';
17+
import {FormValidationBehaviorContext} from './Form';
1718
import {LabelContext} from './Label';
1819
import {RadioGroupState, useRadioGroupState} from 'react-stately';
19-
import React, {createContext, ForwardedRef, forwardRef, useRef} from 'react';
20+
import React, {createContext, ForwardedRef, forwardRef, useContext, useRef} from 'react';
2021
import {TextContext} from './Text';
2122

2223
export interface RadioGroupProps extends Omit<AriaRadioGroupProps, 'children' | 'label' | 'description' | 'errorMessage' | 'validationState' | 'validationBehavior'>, RACValidation, RenderProps<RadioGroupRenderProps>, SlotProps {}
@@ -108,16 +109,18 @@ export const RadioGroupStateContext = createContext<RadioGroupState | null>(null
108109

109110
function RadioGroup(props: RadioGroupProps, ref: ForwardedRef<HTMLDivElement>) {
110111
[props, ref] = useContextProps(props, ref, RadioGroupContext);
112+
let formValidationBehavior = useContext(FormValidationBehaviorContext);
113+
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
111114
let state = useRadioGroupState({
112115
...props,
113-
validationBehavior: props.validationBehavior ?? 'native'
116+
validationBehavior
114117
});
115118

116119
let [labelRef, label] = useSlot();
117120
let {radioGroupProps, labelProps, descriptionProps, errorMessageProps, ...validation} = useRadioGroup({
118121
...props,
119122
label,
120-
validationBehavior: props.validationBehavior ?? 'native'
123+
validationBehavior
121124
}, state);
122125

123126
let renderProps = useRenderProps({

packages/react-aria-components/src/SearchField.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ import {ButtonContext} from './Button';
1515
import {ContextValue, forwardRefType, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot} from './utils';
1616
import {FieldErrorContext} from './FieldError';
1717
import {filterDOMProps} from '@react-aria/utils';
18+
import {FormValidationBehaviorContext} from './Form';
1819
import {GroupContext} from './Group';
1920
import {InputContext} from './Input';
2021
import {LabelContext} from './Label';
21-
import React, {createContext, ForwardedRef, forwardRef, useRef} from 'react';
22+
import React, {createContext, ForwardedRef, forwardRef, useContext, useRef} from 'react';
2223
import {SearchFieldState, useSearchFieldState} from 'react-stately';
2324
import {TextContext} from './Text';
2425

@@ -50,17 +51,19 @@ export const SearchFieldContext = createContext<ContextValue<SearchFieldProps, H
5051

5152
function SearchField(props: SearchFieldProps, ref: ForwardedRef<HTMLDivElement>) {
5253
[props, ref] = useContextProps(props, ref, SearchFieldContext);
54+
let formValidationBehavior = useContext(FormValidationBehaviorContext);
55+
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
5356
let inputRef = useRef<HTMLInputElement>(null);
5457
let [labelRef, label] = useSlot();
5558
let state = useSearchFieldState({
5659
...props,
57-
validationBehavior: props.validationBehavior ?? 'native'
60+
validationBehavior
5861
});
5962

6063
let {labelProps, inputProps, clearButtonProps, descriptionProps, errorMessageProps, ...validation} = useSearchField({
6164
...removeDataAttributes(props),
6265
label,
63-
validationBehavior: props.validationBehavior ?? 'native'
66+
validationBehavior
6467
}, state, inputRef);
6568

6669
let renderProps = useRenderProps({

packages/react-aria-components/src/Select.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {CollectionDocumentContext, ItemRenderProps, useCollectionDocument} from
1616
import {ContextValue, forwardRefType, Hidden, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils';
1717
import {FieldErrorContext} from './FieldError';
1818
import {filterDOMProps, useResizeObserver} from '@react-aria/utils';
19+
import {FormValidationBehaviorContext} from './Form';
1920
// @ts-ignore
2021
import intlMessages from '../intl/*.json';
2122
import {LabelContext} from './Label';
@@ -66,12 +67,14 @@ export const SelectStateContext = createContext<SelectState<unknown> | null>(nul
6667

6768
function Select<T extends object>(props: SelectProps<T>, ref: ForwardedRef<HTMLDivElement>) {
6869
[props, ref] = useContextProps(props, ref, SelectContext);
70+
let formValidationBehavior = useContext(FormValidationBehaviorContext);
71+
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
6972
let {collection, document} = useCollectionDocument();
7073
let state = useSelectState({
7174
...props,
7275
collection,
7376
children: undefined,
74-
validationBehavior: props.validationBehavior ?? 'native'
77+
validationBehavior
7578
});
7679

7780
let {isFocusVisible, focusProps} = useFocusRing({within: true});
@@ -90,7 +93,7 @@ function Select<T extends object>(props: SelectProps<T>, ref: ForwardedRef<HTMLD
9093
} = useSelect({
9194
...removeDataAttributes(props),
9295
label,
93-
validationBehavior: props.validationBehavior ?? 'native'
96+
validationBehavior
9497
}, state, buttonRef);
9598

9699
// Make menu width match input + button

0 commit comments

Comments
 (0)