Skip to content

Commit 313cc05

Browse files
devongovettLFDanLu
andauthored
Make FormContext match our other contexts (#6302)
Co-authored-by: Daniel Lu <dl1644@gmail.com>
1 parent ec7c614 commit 313cc05

File tree

11 files changed

+77
-76
lines changed

11 files changed

+77
-76
lines changed

packages/react-aria-components/docs/Form.mdx

Lines changed: 47 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -162,61 +162,25 @@ See the [Forms](forms.html) guide to learn more about form validation in React A
162162

163163
### Validation behavior
164164

165-
By default, native HTML form validation is used to display errors and block form submission.
165+
By default, native HTML form validation is used to display errors and block form submission. To instead use ARIA attributes for form validation, set the `validationBehavior` prop to "aria". This will not block form submission, and will display validation errors to the user in realtime as the value is edited.
166+
167+
The `validationBehavior` can be set at the form level to apply to all fields, or at the field level to override the form's behavior for a specific field.
166168

167169
```tsx example
168-
<Form>
169-
<TextField name="email" type="email" isRequired>
170-
<Label>Email</Label>
170+
<Form validationBehavior="aria">
171+
<TextField
172+
name="username"
173+
defaultValue="admin"
174+
isRequired
175+
validate={value => value === 'admin' ? 'Nice try.' : null}>
176+
<Label>Username</Label>
171177
<Input />
172178
<FieldError />
173179
</TextField>
174-
<div style={{display: 'flex', gap: 8}}>
175-
<Button type="submit">Submit</Button>
176-
<Button type="reset">Reset</Button>
177-
</div>
180+
<Button type="submit">Submit</Button>
178181
</Form>
179182
```
180183

181-
To instead use ARIA attributes for form validation, set the validationBehavior prop to "aria". This will not block form submission, and will ensure validation errors are displayed to the user in realtime as the value is edited.
182-
183-
This can be set at the form level to apply to all fields, or at the field level to override the form's behavior for a specific field.
184-
185-
```tsx example
186-
const EMAIL_REGEX = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
187-
188-
function Example() {
189-
let [value, setValue] = React.useState('');
190-
return (
191-
<Form validationBehavior="aria" onSubmit={(e) => {e.preventDefault()}}>
192-
<TextField
193-
name="email"
194-
type="email"
195-
value={value}
196-
onChange={setValue}
197-
isRequired
198-
isInvalid={value.length > 0 && !EMAIL_REGEX.test(value)}>
199-
<Label>Email</Label>
200-
<Input />
201-
<FieldError />
202-
</TextField>
203-
<div style={{display: 'flex', gap: 8}}>
204-
<Button type="submit">Submit</Button>
205-
<Button onPress={() => setValue('')}>Reset</Button>
206-
</div>
207-
</Form>
208-
);
209-
}
210-
```
211-
212-
The `validationBehavior` for a Form can be accessed from within any component inside a given From by using the `FormContext`.
213-
214-
```tsx
215-
import {FormContext} from 'react-aria-components';
216-
217-
let {validationBehavior} = useContext(FormContext);
218-
```
219-
220184
### Focus management
221185

222186
By default, after a user submits a form with validation errors, the first invalid field will be focused. You can prevent this by calling `preventDefault` during the `onInvalid` event, and move focus yourself. This example shows how to move focus to an [alert](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/alert_role) element at the top of a form.
@@ -318,6 +282,42 @@ A custom `className` can also be specified on any component. This overrides the
318282

319283
## Advanced customization
320284

285+
### Contexts
286+
287+
All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in [mergeProps](mergeProps.html)).
288+
289+
<ContextTable components={['Form']} docs={docs} />
290+
291+
This example adds a global form submission handler for all forms rendered inside it, which could be used to centralize logic to submit data to an API.
292+
293+
```tsx
294+
let onSubmit = e => {
295+
e.preventDefault();
296+
// Submit form data to an API...
297+
};
298+
299+
<FormContext.Provider value={{onSubmit}}>
300+
<Form>
301+
{/* ... */}
302+
</Form>
303+
</FormContext.Provider>
304+
```
305+
306+
`FormContext` can also be used within any component inside a form to access props from the nearest ancestor form. For example, to access the current `validationBehavior`, use the [useSlottedContext](advanced.html#useslottedcontext) hook.
307+
308+
```tsx
309+
import {FormContext, useSlottedContext} from 'react-aria-components';
310+
311+
function MyFormField() {
312+
let {validationBehavior} = useSlottedContext(FormContext);
313+
// ...
314+
}
315+
316+
<Form validationBehavior="aria">
317+
<MyFormField />
318+
</Form>
319+
```
320+
321321
### Validation context
322322

323323
The `Form` component provides a value for `FormValidationContext`, which allows child elements to receive validation errors from the form. You can provide a value for this context directly in case you need to customize the form element, or reuse an existing form component.

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import {AriaCheckboxGroupProps, AriaCheckboxProps, HoverEvents, mergeProps, useCheckbox, useCheckboxGroup, useCheckboxGroupItem, useFocusRing, useHover, VisuallyHidden} from 'react-aria';
1313
import {CheckboxContext} from './RSPContexts';
1414
import {CheckboxGroupState, useCheckboxGroupState, useToggleState} from 'react-stately';
15-
import {ContextValue, forwardRefType, Provider, RACValidation, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot} from './utils';
15+
import {ContextValue, forwardRefType, Provider, RACValidation, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils';
1616
import {FieldErrorContext} from './FieldError';
1717
import {filterDOMProps, mergeRefs, useObjectRef} from '@react-aria/utils';
1818
import {FormContext} from './Form';
@@ -113,7 +113,7 @@ export const CheckboxGroupStateContext = createContext<CheckboxGroupState | null
113113

114114
function CheckboxGroup(props: CheckboxGroupProps, ref: ForwardedRef<HTMLDivElement>) {
115115
[props, ref] = useContextProps(props, ref, CheckboxGroupContext);
116-
let {validationBehavior: formValidationBehavior} = useContext(FormContext) || {};
116+
let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
117117
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
118118
let state = useCheckboxGroupState({
119119
...props,
@@ -172,7 +172,7 @@ function Checkbox(props: CheckboxProps, ref: ForwardedRef<HTMLLabelElement>) {
172172
...otherProps
173173
} = props;
174174
[props, ref] = useContextProps(otherProps, ref, CheckboxContext);
175-
let {validationBehavior: formValidationBehavior} = useContext(FormContext) || {};
175+
let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
176176
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
177177
let groupState = useContext(CheckboxGroupStateContext);
178178
let inputRef = useObjectRef(mergeRefs(userProvidedInputRef, props.inputRef !== undefined ? props.inputRef : null));

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {AriaComboBoxProps, useComboBox, useFilter} from 'react-aria';
1313
import {ButtonContext} from './Button';
1414
import {Collection, ComboBoxState, Node, useComboBoxState} from 'react-stately';
1515
import {CollectionDocumentContext, useCollectionDocument} from './Collection';
16-
import {ContextValue, forwardRefType, Hidden, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot} from './utils';
16+
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';
1919
import {FormContext} from './Form';
@@ -23,7 +23,7 @@ import {LabelContext} from './Label';
2323
import {ListBoxContext, ListStateContext} from './ListBox';
2424
import {OverlayTriggerStateContext} from './Dialog';
2525
import {PopoverContext} from './Popover';
26-
import React, {createContext, ForwardedRef, forwardRef, RefObject, useCallback, useContext, useMemo, useRef, useState} from 'react';
26+
import React, {createContext, ForwardedRef, forwardRef, RefObject, useCallback, useMemo, useRef, useState} from 'react';
2727
import {TextContext} from './Text';
2828

2929
export interface ComboBoxRenderProps {
@@ -115,7 +115,7 @@ function ComboBoxInner<T extends object>({props, collection, comboBoxRef: ref}:
115115
formValue = 'text';
116116
}
117117

118-
let {validationBehavior: formValidationBehavior} = useContext(FormContext) || {};
118+
let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
119119
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
120120
let {contains} = useFilter({sensitivity: 'base'});
121121
let state = useComboBoxState({

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212
import {AriaDateFieldProps, AriaTimeFieldProps, DateValue, HoverEvents, mergeProps, TimeValue, useDateField, useDateSegment, useFocusRing, useHover, useLocale, useTimeField} from 'react-aria';
13-
import {ContextValue, forwardRefType, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, StyleRenderProps, useContextProps, useRenderProps, useSlot} from './utils';
13+
import {ContextValue, forwardRefType, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, StyleRenderProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils';
1414
import {createCalendar} from '@internationalized/date';
1515
import {DateFieldState, DateSegmentType, DateSegment as IDateSegment, TimeFieldState, useDateFieldState, useTimeFieldState} from 'react-stately';
1616
import {FieldErrorContext} from './FieldError';
@@ -48,7 +48,7 @@ export const TimeFieldStateContext = createContext<TimeFieldState | null>(null);
4848

4949
function DateField<T extends DateValue>(props: DateFieldProps<T>, ref: ForwardedRef<HTMLDivElement>) {
5050
[props, ref] = useContextProps(props, ref, DateFieldContext);
51-
let {validationBehavior: formValidationBehavior} = useContext(FormContext) || {};
51+
let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
5252
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
5353
let {locale} = useLocale();
5454
let state = useDateFieldState({
@@ -115,7 +115,7 @@ export {_DateField as DateField};
115115

116116
function TimeField<T extends TimeValue>(props: TimeFieldProps<T>, ref: ForwardedRef<HTMLDivElement>) {
117117
[props, ref] = useContextProps(props, ref, TimeFieldContext);
118-
let {validationBehavior: formValidationBehavior} = useContext(FormContext) || {};
118+
let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
119119
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
120120
let {locale} = useLocale();
121121
let state = useTimeFieldState({

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import {AriaDatePickerProps, AriaDateRangePickerProps, DateValue, useDatePicker, useDateRangePicker, useFocusRing} from 'react-aria';
1313
import {ButtonContext} from './Button';
1414
import {CalendarContext, RangeCalendarContext} from './Calendar';
15-
import {ContextValue, forwardRefType, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot} from './utils';
15+
import {ContextValue, forwardRefType, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils';
1616
import {DateFieldContext} from './DateField';
1717
import {DatePickerState, DatePickerStateOptions, DateRangePickerState, DateRangePickerStateOptions, useDatePickerState, useDateRangePickerState} from 'react-stately';
1818
import {DialogContext, OverlayTriggerStateContext} from './Dialog';
@@ -22,7 +22,7 @@ import {FormContext} from './Form';
2222
import {GroupContext} from './Group';
2323
import {LabelContext} from './Label';
2424
import {PopoverContext} from './Popover';
25-
import React, {createContext, ForwardedRef, forwardRef, useCallback, useContext, useRef, useState} from 'react';
25+
import React, {createContext, ForwardedRef, forwardRef, useCallback, useRef, useState} from 'react';
2626
import {TextContext} from './Text';
2727

2828
export interface DatePickerRenderProps {
@@ -73,7 +73,7 @@ export const DateRangePickerStateContext = createContext<DateRangePickerState |
7373

7474
function DatePicker<T extends DateValue>(props: DatePickerProps<T>, ref: ForwardedRef<HTMLDivElement>) {
7575
[props, ref] = useContextProps(props, ref, DatePickerContext);
76-
let {validationBehavior: formValidationBehavior} = useContext(FormContext) || {};
76+
let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
7777
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
7878
let state = useDatePickerState({
7979
...props,
@@ -176,7 +176,7 @@ export {_DatePicker as DatePicker};
176176

177177
function DateRangePicker<T extends DateValue>(props: DateRangePickerProps<T>, ref: ForwardedRef<HTMLDivElement>) {
178178
[props, ref] = useContextProps(props, ref, DateRangePickerContext);
179-
let {validationBehavior: formValidationBehavior} = useContext(FormContext) || {};
179+
let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
180180
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
181181
let state = useDateRangePickerState({
182182
...props,

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {DOMProps} from './utils';
13+
import {ContextValue, DOMProps, useContextProps} from './utils';
1414
import {FormValidationContext} from 'react-stately';
1515
import React, {createContext, ForwardedRef, forwardRef} from 'react';
1616
import {FormProps as SharedFormProps} from '@react-types/form';
@@ -25,13 +25,14 @@ export interface FormProps extends SharedFormProps, DOMProps {
2525
validationBehavior?: 'aria' | 'native'
2626
}
2727

28-
export const FormContext = createContext<{validationBehavior: FormProps['validationBehavior']} | null>(null);
28+
export const FormContext = createContext<ContextValue<FormProps, HTMLFormElement>>(null);
2929

3030
function Form(props: FormProps, ref: ForwardedRef<HTMLFormElement>) {
31+
[props, ref] = useContextProps(props, ref, FormContext);
3132
let {validationErrors, validationBehavior = 'native', children, className, ...domProps} = props;
3233
return (
3334
<form noValidate={validationBehavior !== 'native'} {...domProps} ref={ref} className={className || 'react-aria-Form'}>
34-
<FormContext.Provider value={{validationBehavior}}>
35+
<FormContext.Provider value={{...props, validationBehavior}}>
3536
<FormValidationContext.Provider value={validationErrors ?? {}}>
3637
{children}
3738
</FormValidationContext.Provider>

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

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

1313
import {AriaNumberFieldProps, useLocale, useNumberField} from 'react-aria';
1414
import {ButtonContext} from './Button';
15-
import {ContextValue, forwardRefType, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot} from './utils';
15+
import {ContextValue, forwardRefType, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils';
1616
import {FieldErrorContext} from './FieldError';
1717
import {filterDOMProps} from '@react-aria/utils';
1818
import {FormContext} from './Form';
@@ -21,7 +21,7 @@ import {InputContext} from './Input';
2121
import {InputDOMProps} from '@react-types/shared';
2222
import {LabelContext} from './Label';
2323
import {NumberFieldState, useNumberFieldState} from 'react-stately';
24-
import React, {createContext, ForwardedRef, forwardRef, useContext, useRef} from 'react';
24+
import React, {createContext, ForwardedRef, forwardRef, useRef} from 'react';
2525
import {TextContext} from './Text';
2626

2727
export interface NumberFieldRenderProps {
@@ -48,7 +48,7 @@ export const NumberFieldStateContext = createContext<NumberFieldState | null>(nu
4848

4949
function NumberField(props: NumberFieldProps, ref: ForwardedRef<HTMLDivElement>) {
5050
[props, ref] = useContextProps(props, ref, NumberFieldContext);
51-
let {validationBehavior: formValidationBehavior} = useContext(FormContext) || {};
51+
let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
5252
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
5353
let {locale} = useLocale();
5454
let state = useNumberFieldState({

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
*/
1212

1313
import {AriaRadioGroupProps, AriaRadioProps, HoverEvents, Orientation, useFocusRing, useHover, useRadio, useRadioGroup, VisuallyHidden} from 'react-aria';
14-
import {ContextValue, forwardRefType, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot} from './utils';
14+
import {ContextValue, forwardRefType, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils';
1515
import {FieldErrorContext} from './FieldError';
1616
import {filterDOMProps, mergeProps, mergeRefs, useObjectRef} from '@react-aria/utils';
1717
import {FormContext} from './Form';
1818
import {LabelContext} from './Label';
1919
import {RadioGroupState, useRadioGroupState} from 'react-stately';
20-
import React, {createContext, ForwardedRef, forwardRef, MutableRefObject, useContext} from 'react';
20+
import React, {createContext, ForwardedRef, forwardRef, MutableRefObject} from 'react';
2121
import {TextContext} from './Text';
2222

2323
export interface RadioGroupProps extends Omit<AriaRadioGroupProps, 'children' | 'label' | 'description' | 'errorMessage' | 'validationState' | 'validationBehavior'>, RACValidation, RenderProps<RadioGroupRenderProps>, SlotProps {}
@@ -114,7 +114,7 @@ export const RadioGroupStateContext = createContext<RadioGroupState | null>(null
114114

115115
function RadioGroup(props: RadioGroupProps, ref: ForwardedRef<HTMLDivElement>) {
116116
[props, ref] = useContextProps(props, ref, RadioGroupContext);
117-
let {validationBehavior: formValidationBehavior} = useContext(FormContext) || {};
117+
let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
118118
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
119119
let state = useRadioGroupState({
120120
...props,

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@
1212

1313
import {AriaSearchFieldProps, useSearchField} from 'react-aria';
1414
import {ButtonContext} from './Button';
15-
import {ContextValue, forwardRefType, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot} from './utils';
15+
import {ContextValue, forwardRefType, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils';
1616
import {FieldErrorContext} from './FieldError';
1717
import {filterDOMProps} from '@react-aria/utils';
1818
import {FormContext} from './Form';
1919
import {GroupContext} from './Group';
2020
import {InputContext} from './Input';
2121
import {LabelContext} from './Label';
22-
import React, {createContext, ForwardedRef, forwardRef, useContext, useRef} from 'react';
22+
import React, {createContext, ForwardedRef, forwardRef, useRef} from 'react';
2323
import {SearchFieldState, useSearchFieldState} from 'react-stately';
2424
import {TextContext} from './Text';
2525

@@ -51,7 +51,7 @@ export const SearchFieldContext = createContext<ContextValue<SearchFieldProps, H
5151

5252
function SearchField(props: SearchFieldProps, ref: ForwardedRef<HTMLDivElement>) {
5353
[props, ref] = useContextProps(props, ref, SearchFieldContext);
54-
let {validationBehavior: formValidationBehavior} = useContext(FormContext) || {};
54+
let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
5555
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
5656
let inputRef = useRef<HTMLInputElement>(null);
5757
let [labelRef, label] = useSlot();

0 commit comments

Comments
 (0)