Skip to content

Commit

Permalink
feat(elements): Consider ValidityState in FieldState (#3594)
Browse files Browse the repository at this point in the history
  • Loading branch information
LekoArts committed Jun 21, 2024
1 parent 36efb5e commit 5aedc29
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 27 deletions.
14 changes: 14 additions & 0 deletions .changeset/grumpy-dancers-thank.md
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
Expand Up @@ -130,7 +130,7 @@ export default function SignInPage() {
<Clerk.Label className='sr-only'>Email</Clerk.Label>
<Clerk.Input
className={`w-full rounded border border-[rgb(37,37,37)] bg-[rgb(12,12,12)] px-4 py-2 placeholder-[rgb(100,100,100)] ${
fieldState === 'invalid' && 'border-red-500'
fieldState === 'error' && 'border-red-500'
}`}
placeholder='Enter your email address'
/>
Expand Down Expand Up @@ -190,9 +190,10 @@ export default function SignInPage() {
<Clerk.Label className='sr-only'>Email</Clerk.Label>
<Clerk.Input
className={`w-full rounded border border-[rgb(37,37,37)] bg-[rgb(12,12,12)] px-4 py-2 placeholder-[rgb(100,100,100)] ${
fieldState === 'invalid' && 'border-red-500'
fieldState === 'error' && 'border-red-500'
}`}
placeholder='Enter your email address'
type='email'
/>
<Clerk.FieldError className='block w-full font-mono text-red-400' />
</>
Expand Down
77 changes: 52 additions & 25 deletions packages/elements/src/react/common/form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -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
*/
Expand Down Expand Up @@ -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
Expand All @@ -149,7 +166,7 @@ const useForm = ({ flowActor }: { flowActor?: BaseActorRef<{ type: 'SUBMIT' }> }

return {
props: {
[`data-${validity}`]: true,
...(errors.length > 0 ? { 'data-global-error': true } : {}),
onSubmit,
},
};
Expand All @@ -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,
'data-hidden': shouldBeHidden ? true : undefined,
serverInvalid: hasError,
},
Expand All @@ -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.');
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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>
</RadixField>
);
});
Expand Down Expand Up @@ -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);
}
Expand Down

0 comments on commit 5aedc29

Please sign in to comment.