-
Notifications
You must be signed in to change notification settings - Fork 460
feat(elements): Consider ValidityState in FieldState #3594
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
Changes from all commits
2194bf4
697605b
e9b32d3
fe6f118
76391bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| --- | ||
| "@clerk/elements": minor | ||
| --- | ||
|
|
||
| Improve `<FieldState>` and re-organize some data attributes related to validity states. These changes might be breaking changes for you. | ||
|
|
||
| Overview of changes: | ||
|
|
||
| - `<form>` no longer has `data-valid` and `data-invalid` attributes. If there are global errors (same heuristics as `<GlobalError>`) then a `data-global-error` attribute will be present. | ||
| - Fixed a bug where `<Field>` could contain `data-valid` and `data-invalid` at the same time. | ||
| - The field state (accessible through e.g. `<FieldState>`) now also incorporates the field's [ValidityState](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState) into its output. If the `ValidityState` is invalid, the field state will be an `error`. You can access this information in three places: | ||
| 1. `<FieldState>` | ||
| 2. `data-state` attribute on `<Input>` | ||
| 3. `<Field>{(state) => <p>Field's state is {state}</p>}</Field>` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ import { | |
| FormMessage as RadixFormMessage, | ||
| Label as RadixLabel, | ||
| Submit as RadixSubmit, | ||
| ValidityState as RadixValidityState, | ||
| } from '@radix-ui/react-form'; | ||
| import { Slot } from '@radix-ui/react-slot'; | ||
| import * as React from 'react'; | ||
|
|
@@ -43,7 +44,7 @@ import { isReactFragment } from '~/react/utils/is-react-fragment'; | |
|
|
||
| import type { OTPInputProps } from './otp'; | ||
| import { OTP_LENGTH_DEFAULT, OTPInput } from './otp'; | ||
| import { type ClerkFieldId, FIELD_STATES, FIELD_VALIDITY, type FieldStates } from './types'; | ||
| import { type ClerkFieldId, FIELD_STATES, type FieldStates } from './types'; | ||
|
|
||
| /* ------------------------------------------------------------------------------------------------- | ||
| * Context | ||
|
|
@@ -52,26 +53,13 @@ import { type ClerkFieldId, FIELD_STATES, FIELD_VALIDITY, type FieldStates } fro | |
| const FieldContext = React.createContext<Pick<FieldDetails, 'name'> | null>(null); | ||
| const useFieldContext = () => React.useContext(FieldContext); | ||
|
|
||
| const ValidityStateContext = React.createContext<ValidityState | undefined>(undefined); | ||
| const useValidityStateContext = () => React.useContext(ValidityStateContext); | ||
|
|
||
| /* ------------------------------------------------------------------------------------------------- | ||
| * Hooks | ||
| * Utils | ||
| * -----------------------------------------------------------------------------------------------*/ | ||
|
|
||
| const useGlobalErrors = () => { | ||
| const errors = useFormSelector(globalErrorsSelector); | ||
|
|
||
| return { | ||
| errors, | ||
| }; | ||
| }; | ||
|
|
||
| const useFieldFeedback = ({ name }: Partial<Pick<FieldDetails, 'name'>>) => { | ||
| const feedback = useFormSelector(fieldFeedbackSelector(name)); | ||
|
|
||
| return { | ||
| feedback, | ||
| }; | ||
| }; | ||
|
|
||
| const determineInputTypeFromName = (name: FormFieldProps['name']) => { | ||
| if (name === 'password' || name === 'confirmPassword' || name === 'currentPassword' || name === 'newPassword') { | ||
| return 'password' as const; | ||
|
|
@@ -89,6 +77,36 @@ const determineInputTypeFromName = (name: FormFieldProps['name']) => { | |
| return 'text' as const; | ||
| }; | ||
|
|
||
| /** | ||
| * Radix can return the ValidityState object, which contains the validity of the field. We need to merge this with our existing fieldState. | ||
| * When the ValidityState is valid: false, the fieldState should be overriden. Otherwise, it shouldn't change at all. | ||
| * @see https://www.radix-ui.com/primitives/docs/components/form#validitystate | ||
| * @see https://developer.mozilla.org/en-US/docs/Web/API/ValidityState | ||
| */ | ||
| const enrichFieldState = (validity: ValidityState | undefined, fieldState: FieldStates) => { | ||
| return validity?.valid === false ? FIELD_STATES.error : fieldState; | ||
| }; | ||
|
|
||
| /* ------------------------------------------------------------------------------------------------- | ||
| * Hooks | ||
| * -----------------------------------------------------------------------------------------------*/ | ||
|
|
||
| const useGlobalErrors = () => { | ||
| const errors = useFormSelector(globalErrorsSelector); | ||
|
|
||
| return { | ||
| errors, | ||
| }; | ||
| }; | ||
|
|
||
| const useFieldFeedback = ({ name }: Partial<Pick<FieldDetails, 'name'>>) => { | ||
| const feedback = useFormSelector(fieldFeedbackSelector(name)); | ||
|
|
||
| return { | ||
| feedback, | ||
| }; | ||
| }; | ||
|
|
||
| /** | ||
| * Given a field name, determine the current state of the field | ||
| */ | ||
|
|
@@ -133,7 +151,6 @@ const useFieldState = ({ name }: Partial<Pick<FieldDetails, 'name'>>) => { | |
| */ | ||
| const useForm = ({ flowActor }: { flowActor?: BaseActorRef<{ type: 'SUBMIT' }> }) => { | ||
| const { errors } = useGlobalErrors(); | ||
| const validity = errors.length > 0 ? FIELD_VALIDITY.invalid : FIELD_VALIDITY.valid; | ||
|
|
||
| // Register the onSubmit handler for form submission | ||
| // TODO: merge user-provided submit handler | ||
|
|
@@ -149,7 +166,7 @@ const useForm = ({ flowActor }: { flowActor?: BaseActorRef<{ type: 'SUBMIT' }> } | |
|
|
||
| return { | ||
| props: { | ||
| [`data-${validity}`]: true, | ||
| ...(errors.length > 0 ? { 'data-global-error': true } : {}), | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It didn't make much sense to me to have So renaming this to make it more clear that this is about global errors. |
||
| onSubmit, | ||
| }, | ||
| }; | ||
|
|
@@ -161,12 +178,10 @@ const useField = ({ name }: Partial<Pick<FieldDetails, 'name'>>) => { | |
|
|
||
| const shouldBeHidden = false; // TODO: Implement clerk-js utils | ||
| const hasError = feedback ? feedback.type === 'error' : false; | ||
| const validity = hasError ? FIELD_VALIDITY.invalid : FIELD_VALIDITY.valid; | ||
|
|
||
| return { | ||
| hasValue, | ||
| props: { | ||
| [`data-${validity}`]: true, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Radix already adds |
||
| 'data-hidden': shouldBeHidden ? true : undefined, | ||
| serverInvalid: hasError, | ||
| }, | ||
|
|
@@ -186,6 +201,7 @@ const useInput = ({ | |
| const fieldContext = useFieldContext(); | ||
| const name = inputName || fieldContext?.name; | ||
| const { state: fieldState } = useFieldState({ name }); | ||
| const validity = useValidityStateContext(); | ||
|
|
||
| if (!name) { | ||
| throw new Error('Clerk: <Input /> must be wrapped in a <Field> component or have a name prop.'); | ||
|
|
@@ -342,7 +358,7 @@ const useInput = ({ | |
| onFocus, | ||
| 'data-hidden': shouldBeHidden ? true : undefined, | ||
| 'data-has-value': hasValue ? true : undefined, | ||
| 'data-state': fieldState, | ||
| 'data-state': enrichFieldState(validity, fieldState), | ||
| ...props, | ||
| ...rest, | ||
| }, | ||
|
|
@@ -444,7 +460,17 @@ const FieldInner = React.forwardRef<FormFieldElement, FormFieldProps>((props, fo | |
| {...rest} | ||
| ref={forwardedRef} | ||
| > | ||
| {typeof children === 'function' ? children(fieldState) : children} | ||
| <RadixValidityState> | ||
| {validity => { | ||
| const enrichedFieldState = enrichFieldState(validity, fieldState); | ||
|
|
||
| return ( | ||
| <ValidityStateContext.Provider value={validity}> | ||
| {typeof children === 'function' ? children(enrichedFieldState) : children} | ||
| </ValidityStateContext.Provider> | ||
| ); | ||
| }} | ||
| </RadixValidityState> | ||
|
Comment on lines
+463
to
+473
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Main gist of this PR. Using Context here to not have to pass around values through props 👍 |
||
| </RadixField> | ||
| ); | ||
| }); | ||
|
|
@@ -493,11 +519,12 @@ function FieldState({ children }: FieldStateRenderFn) { | |
| const field = useFieldContext(); | ||
| const { feedback } = useFieldFeedback({ name: field?.name }); | ||
| const { state } = useFieldState({ name: field?.name }); | ||
| const validity = useValidityStateContext(); | ||
|
|
||
| const message = feedback?.message instanceof ClerkElementsFieldError ? feedback.message.message : feedback?.message; | ||
| const codes = feedback?.codes; | ||
|
|
||
| const fieldState = { state, message, codes }; | ||
| const fieldState = { state: enrichFieldState(validity, state), message, codes }; | ||
|
|
||
| return children(fieldState); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
validThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also thought about doing this instead:
I guess it's a question of what should take precedence. The validity or our own fieldState