Support native form validation#5288
Conversation
# Conflicts: # packages/@react-aria/checkbox/package.json # packages/@react-aria/datepicker/package.json # packages/@react-aria/numberfield/package.json # packages/@react-aria/radio/package.json # packages/@react-aria/textfield/package.json # packages/@react-spectrum/color/package.json # packages/@react-spectrum/datepicker/package.json # packages/@react-spectrum/form/package.json # packages/@react-spectrum/numberfield/package.json # packages/@react-spectrum/searchfield/package.json # packages/@react-spectrum/textfield/package.json # packages/@react-stately/datepicker/src/useDateFieldState.ts
# Conflicts: # packages/@react-spectrum/form/src/Form.tsx # packages/@react-spectrum/searchfield/src/SearchField.tsx
Is this a breaking change??
|
Build successful! 🎉 |
LFDanLu
left a comment
There was a problem hiding this comment.
One thing I noticed when testing the stories, currently looking at the other code changes
| } | ||
|
|
||
| ServerValidation.story = { | ||
| parameters: {description: {data: 'This story is to test that server errors appear after submission, and are cleared when a field is modified.'}} |
There was a problem hiding this comment.
Noticed that the "Favorite Pet" radiogroup validation error after submission doesn't clear when the user modifies the selected radio option via keyboard arrows. It only gets cleared if the user clicks on a different radio option
To reproduce:
- go to https://reactspectrum.blob.core.windows.net/reactspectrum/36a3c4cbf1421edd7cb21466d4fdad61077d1ca4/storybook/index.html?path=/story/form--server-validation&providerSwitcher-express=false and hit submit
- Tab navigate to the "Favorite Pet" radio group and change the selected radio via the keyboard arrows. Note the error doesn't clear
| // (FocusScope attempts to restore focus to the tray input when tapping outside the tray due to "contain") | ||
| // Have to do this manually since React doesn't call onBlur when a component is unmounted: https://github.com/facebook/react/issues/12363 | ||
| return () => { | ||
| if (!state.isOpen && state.isFocused) { |
There was a problem hiding this comment.
comment above this line needs correcting, we do this before unmount now
There was a problem hiding this comment.
hmm I already changed it to say "When the tray closes" instead of "unmounts". Do you mean the line about "React doesn't call onBlur when a component is unmounted"? I think that part is still true?
| }, [fn]); | ||
| return useCallback((...args) => { | ||
| const f = ref.current; | ||
| // @ts-ignore |
There was a problem hiding this comment.
i couldn't solve this one either :(
| onChange = () => {} | ||
| validationBehavior = 'aria' | ||
| }: AriaTextFieldOptions<TextFieldIntrinsicElements> = props; | ||
| let [value, setValue] = useControlledState<string>(props.value, props.defaultValue || '', props.onChange); |
There was a problem hiding this comment.
this is a bit odd, i haven't seen useControlledState in an aria package before. I assume this is because textfield doesn't have a corresponding stately hook? should it? I guess that would be breaking...
|
Build successful! 🎉 |
|
Build successful! 🎉 |
|
Build successful! 🎉 |
|
## API Changes
unknown top level export { type: 'any' } @react-aria/checkboxCheckboxAria CheckboxAria {
inputProps: InputHTMLAttributes<HTMLInputElement>
isDisabled: boolean
- isInvalid: boolean
isPressed: boolean
isReadOnly: boolean
isSelected: boolean
}it changed:
@react-aria/interactionsInteractOutsideProps InteractOutsideProps {
isDisabled?: boolean
- onInteractOutside?: (SyntheticEvent) => void
- onInteractOutsideStart?: (SyntheticEvent) => void
+ onInteractOutside?: (PointerEvent) => void
+ onInteractOutsideStart?: (PointerEvent) => void
ref: RefObject<Element>
}it changed:
@react-aria/tagAriaTagGroupProps AriaTagGroupProps<T> {
+ errorMessage?: ReactNode
onRemove?: (Set<Key>) => void
selectionBehavior?: SelectionBehavior
}@react-aria/utilsisVirtualPointerEvent-useEffectEvent {
- fn: any
- returnVal: undefined
-}
+useFormReset-
+useEffectEvent<T extends Function> {
+ fn: T
+ returnVal: undefined
+}@react-spectrum/formForm SpectrumFormProps {
action?: string
children: ReactElement<SpectrumLabelableProps> | Array<ReactElement<SpectrumLabelableProps>>
encType?: 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/plain'
isDisabled?: boolean
isEmphasized?: boolean
isQuiet?: boolean
isReadOnly?: boolean
isRequired?: boolean
method?: 'get' | 'post'
onSubmit?: FormEventHandler
target?: '_blank' | '_self' | '_parent' | '_top'
+ validationBehavior?: 'aria' | 'native' = 'aria'
+ validationErrors?: ValidationErrors
validationState?: ValidationState = 'valid'
}@react-spectrum/labelField HelpText {
descriptionProps?: HTMLAttributes<HTMLElement>
+ errorMessage?: ReactNode
errorMessageProps?: HTMLAttributes<HTMLElement>
}@react-stately/checkboxCheckboxGroupState CheckboxGroupState {
addValue: (string) => void
isDisabled: boolean
isInvalid: boolean
isReadOnly: boolean
+ isRequired: boolean
isSelected: (string) => boolean
removeValue: (string) => void
+ setInvalid: (string, ValidationResult) => void
setValue: (Array<string>) => void
toggleValue: (string) => void
value: readonly Array<string>
}it changed:
@react-stately/radioRadioGroupState RadioGroupState {
isDisabled: boolean
isInvalid: boolean
isReadOnly: boolean
isRequired: boolean
lastFocusedValue: string | null
- selectedValue: string | null | undefined
+ selectedValue: string | null
setLastFocusedValue: (string) => void
setSelectedValue: (string) => void
}it changed:
@react-aria/formuseFormValidation-
+useFormValidation<T> {
+ props: Validation<T>
+ state: FormValidationState
+ ref: RefObject<ValidatableElement>
+ returnVal: undefined
+}@react-stately/formFormValidationContext-
+useFormValidationState<T> {
+ props: FormValidationProps<T>
+ returnVal: undefined
+}useFormValidationState-
+DEFAULT_VALIDATION_RESULT {
+ isInvalid: any
+ validationDetails: VALID_VALIDITY_STATE
+ validationErrors: any
+}DEFAULT_VALIDATION_RESULTchanged by:
-
+VALID_VALIDITY_STATE {
+ badInput: any
+ customError: any
+ patternMismatch: any
+ rangeOverflow: any
+ rangeUnderflow: any
+ stepMismatch: any
+ tooLong: any
+ tooShort: any
+ typeMismatch: any
+ valid: any
+ valueMissing: any
+}VALID_VALIDITY_STATE-
+mergeValidation {
+ results: Array<ValidationResult>
+ returnVal: undefined
+}it changed:
privateValidationStateProp-
+FormValidationState {
+ commitValidation: () => void
+ displayValidation: ValidationResult
+ realtimeValidation: ValidationResult
+ resetValidation: () => void
+ updateValidation: (ValidationResult) => void
+} |
| tabIndex = undefined; | ||
| } | ||
|
|
||
| let {name, descriptionId, errorMessageId, validationBehavior} = radioGroupData.get(state)!; |
There was a problem hiding this comment.
This seems to break when the useRadio and useRadioGroup hook are used in separate components.
<RadioGroup>
<RadioGroupItem />
</RadioGroup>I think this is because in this setup the item is rendered first and then the parent is rendered. This worked before because the undefined values weren't being destructured.
There was a problem hiding this comment.
This is actually a common misconception. Parents are rendered first, but children's effects run first. See https://stackoverflow.com/questions/53781632/whats-useeffect-execution-order-and-its-internal-clean-up-logic-when-requestani for some examples.
Can you reproduce this in a codesandbox? Because we have an example in our docs where the hooks are in two different components and it looks like what you've outlined, but it's working for us https://react-spectrum.adobe.com/react-aria/useRadioGroup.html#example
Are you sure you've passed the correct state object from radio group to each radio?
There was a problem hiding this comment.
hey, thanks for the quick response!
You are right, I wasn't passing the correct state object. In addition to the state from useRadioGroupState I was passing extra fields through the context and then destructuring them when accessing the context. So this wasn't working for me prior either, it just wasn't throwing an error haha. After updating to use the same state object throughout it's working 👍
Here's a codesandbox showing the error I was seeing.
Thanks again for the quick response!!
| export type ValidationErrors = Record<string, ValidationError>; | ||
| export type ValidationFunction<T> = (value: T) => ValidationError | true | null | undefined; | ||
|
|
||
| export interface Validation<T> { |
There was a problem hiding this comment.
This was a breaking change for my application.
Supersedes #4640
This implements support for native HTML form validation across all of our form components. It adds the following APIs:
validationBehaviorprop can be set to"aria"(the default) or"native". When set to"native", we use the nativerequiredattribute rather thanaria-requiredso that form submission is blocked by the browser. In addition, we track other native validation messages such aspatternortype="email"and display all validation errors as Spectrum help text rather than the browser's builtin UI.validateprop can be set to a function to perform custom validation. This receives the current value of the field and can return an error message (or multiple) if it is invalid. This is displayed immediately (in realtime) ifvalidationBehavior="aria", or after the field value is committed (e.g. blurred) whenvalidationBehavior="native". This is in addition to our existingisInvalidanderrorMessageprops which are always displayed in realtime.errorMessageprop now accepts a function in addition to a string. The function receives the current validation details for the field and returns the validation error to display. This allows customizing native validation messages provided by the browser.Formcomponent accepts avalidationErrorsprop, which can be set to an object mapping fieldnameattributes to error messages. This is useful to display validation errors that come from the server. The fields receive these messages from the form via context. After the user modifies the value in the field, the server validation errors for that field are cleared until thevalidationErrorsprop changes (i.e. after the next submit).Implementation
Currently this is implemented in the hooks and in React Spectrum. It will come to React Aria Components in a followup PR. The implementation consists of two parts:
useFormValidationStatein@react-stately/form, and used by all of our stately hooks. This tracks both the realtime validation state and validation state currently displayed to the user, and combines all of the errors from all of the above sources together. It includes methods to update and commit the validation state when the user changes the value.useFormValidationin@react-aria/form, used by all of our aria hooks. This synchronizes the validation state with the native input element, and triggers state updates when various browser events occur.Test instructions
See the "Native validation" and "Server validation" stories that have been added under "Form" for the new behavior, and smoke test other form/component stories.