Skip to content

Commit 5aedc29

Browse files
authored
feat(elements): Consider ValidityState in FieldState (clerk#3594)
1 parent 36efb5e commit 5aedc29

3 files changed

Lines changed: 69 additions & 27 deletions

File tree

.changeset/grumpy-dancers-thank.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@clerk/elements": minor
3+
---
4+
5+
Improve `<FieldState>` and re-organize some data attributes related to validity states. These changes might be breaking changes for you.
6+
7+
Overview of changes:
8+
9+
- `<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.
10+
- Fixed a bug where `<Field>` could contain `data-valid` and `data-invalid` at the same time.
11+
- 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:
12+
1. `<FieldState>`
13+
2. `data-state` attribute on `<Input>`
14+
3. `<Field>{(state) => <p>Field's state is {state}</p>}</Field>`

packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export default function SignInPage() {
130130
<Clerk.Label className='sr-only'>Email</Clerk.Label>
131131
<Clerk.Input
132132
className={`w-full rounded border border-[rgb(37,37,37)] bg-[rgb(12,12,12)] px-4 py-2 placeholder-[rgb(100,100,100)] ${
133-
fieldState === 'invalid' && 'border-red-500'
133+
fieldState === 'error' && 'border-red-500'
134134
}`}
135135
placeholder='Enter your email address'
136136
/>
@@ -190,9 +190,10 @@ export default function SignInPage() {
190190
<Clerk.Label className='sr-only'>Email</Clerk.Label>
191191
<Clerk.Input
192192
className={`w-full rounded border border-[rgb(37,37,37)] bg-[rgb(12,12,12)] px-4 py-2 placeholder-[rgb(100,100,100)] ${
193-
fieldState === 'invalid' && 'border-red-500'
193+
fieldState === 'error' && 'border-red-500'
194194
}`}
195195
placeholder='Enter your email address'
196+
type='email'
196197
/>
197198
<Clerk.FieldError className='block w-full font-mono text-red-400' />
198199
</>

packages/elements/src/react/common/form/index.tsx

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
FormMessage as RadixFormMessage,
1919
Label as RadixLabel,
2020
Submit as RadixSubmit,
21+
ValidityState as RadixValidityState,
2122
} from '@radix-ui/react-form';
2223
import { Slot } from '@radix-ui/react-slot';
2324
import * as React from 'react';
@@ -43,7 +44,7 @@ import { isReactFragment } from '~/react/utils/is-react-fragment';
4344

4445
import type { OTPInputProps } from './otp';
4546
import { OTP_LENGTH_DEFAULT, OTPInput } from './otp';
46-
import { type ClerkFieldId, FIELD_STATES, FIELD_VALIDITY, type FieldStates } from './types';
47+
import { type ClerkFieldId, FIELD_STATES, type FieldStates } from './types';
4748

4849
/* -------------------------------------------------------------------------------------------------
4950
* Context
@@ -52,26 +53,13 @@ import { type ClerkFieldId, FIELD_STATES, FIELD_VALIDITY, type FieldStates } fro
5253
const FieldContext = React.createContext<Pick<FieldDetails, 'name'> | null>(null);
5354
const useFieldContext = () => React.useContext(FieldContext);
5455

56+
const ValidityStateContext = React.createContext<ValidityState | undefined>(undefined);
57+
const useValidityStateContext = () => React.useContext(ValidityStateContext);
58+
5559
/* -------------------------------------------------------------------------------------------------
56-
* Hooks
60+
* Utils
5761
* -----------------------------------------------------------------------------------------------*/
5862

59-
const useGlobalErrors = () => {
60-
const errors = useFormSelector(globalErrorsSelector);
61-
62-
return {
63-
errors,
64-
};
65-
};
66-
67-
const useFieldFeedback = ({ name }: Partial<Pick<FieldDetails, 'name'>>) => {
68-
const feedback = useFormSelector(fieldFeedbackSelector(name));
69-
70-
return {
71-
feedback,
72-
};
73-
};
74-
7563
const determineInputTypeFromName = (name: FormFieldProps['name']) => {
7664
if (name === 'password' || name === 'confirmPassword' || name === 'currentPassword' || name === 'newPassword') {
7765
return 'password' as const;
@@ -89,6 +77,36 @@ const determineInputTypeFromName = (name: FormFieldProps['name']) => {
8977
return 'text' as const;
9078
};
9179

80+
/**
81+
* Radix can return the ValidityState object, which contains the validity of the field. We need to merge this with our existing fieldState.
82+
* When the ValidityState is valid: false, the fieldState should be overriden. Otherwise, it shouldn't change at all.
83+
* @see https://www.radix-ui.com/primitives/docs/components/form#validitystate
84+
* @see https://developer.mozilla.org/en-US/docs/Web/API/ValidityState
85+
*/
86+
const enrichFieldState = (validity: ValidityState | undefined, fieldState: FieldStates) => {
87+
return validity?.valid === false ? FIELD_STATES.error : fieldState;
88+
};
89+
90+
/* -------------------------------------------------------------------------------------------------
91+
* Hooks
92+
* -----------------------------------------------------------------------------------------------*/
93+
94+
const useGlobalErrors = () => {
95+
const errors = useFormSelector(globalErrorsSelector);
96+
97+
return {
98+
errors,
99+
};
100+
};
101+
102+
const useFieldFeedback = ({ name }: Partial<Pick<FieldDetails, 'name'>>) => {
103+
const feedback = useFormSelector(fieldFeedbackSelector(name));
104+
105+
return {
106+
feedback,
107+
};
108+
};
109+
92110
/**
93111
* Given a field name, determine the current state of the field
94112
*/
@@ -133,7 +151,6 @@ const useFieldState = ({ name }: Partial<Pick<FieldDetails, 'name'>>) => {
133151
*/
134152
const useForm = ({ flowActor }: { flowActor?: BaseActorRef<{ type: 'SUBMIT' }> }) => {
135153
const { errors } = useGlobalErrors();
136-
const validity = errors.length > 0 ? FIELD_VALIDITY.invalid : FIELD_VALIDITY.valid;
137154

138155
// Register the onSubmit handler for form submission
139156
// TODO: merge user-provided submit handler
@@ -149,7 +166,7 @@ const useForm = ({ flowActor }: { flowActor?: BaseActorRef<{ type: 'SUBMIT' }> }
149166

150167
return {
151168
props: {
152-
[`data-${validity}`]: true,
169+
...(errors.length > 0 ? { 'data-global-error': true } : {}),
153170
onSubmit,
154171
},
155172
};
@@ -161,12 +178,10 @@ const useField = ({ name }: Partial<Pick<FieldDetails, 'name'>>) => {
161178

162179
const shouldBeHidden = false; // TODO: Implement clerk-js utils
163180
const hasError = feedback ? feedback.type === 'error' : false;
164-
const validity = hasError ? FIELD_VALIDITY.invalid : FIELD_VALIDITY.valid;
165181

166182
return {
167183
hasValue,
168184
props: {
169-
[`data-${validity}`]: true,
170185
'data-hidden': shouldBeHidden ? true : undefined,
171186
serverInvalid: hasError,
172187
},
@@ -186,6 +201,7 @@ const useInput = ({
186201
const fieldContext = useFieldContext();
187202
const name = inputName || fieldContext?.name;
188203
const { state: fieldState } = useFieldState({ name });
204+
const validity = useValidityStateContext();
189205

190206
if (!name) {
191207
throw new Error('Clerk: <Input /> must be wrapped in a <Field> component or have a name prop.');
@@ -342,7 +358,7 @@ const useInput = ({
342358
onFocus,
343359
'data-hidden': shouldBeHidden ? true : undefined,
344360
'data-has-value': hasValue ? true : undefined,
345-
'data-state': fieldState,
361+
'data-state': enrichFieldState(validity, fieldState),
346362
...props,
347363
...rest,
348364
},
@@ -444,7 +460,17 @@ const FieldInner = React.forwardRef<FormFieldElement, FormFieldProps>((props, fo
444460
{...rest}
445461
ref={forwardedRef}
446462
>
447-
{typeof children === 'function' ? children(fieldState) : children}
463+
<RadixValidityState>
464+
{validity => {
465+
const enrichedFieldState = enrichFieldState(validity, fieldState);
466+
467+
return (
468+
<ValidityStateContext.Provider value={validity}>
469+
{typeof children === 'function' ? children(enrichedFieldState) : children}
470+
</ValidityStateContext.Provider>
471+
);
472+
}}
473+
</RadixValidityState>
448474
</RadixField>
449475
);
450476
});
@@ -493,11 +519,12 @@ function FieldState({ children }: FieldStateRenderFn) {
493519
const field = useFieldContext();
494520
const { feedback } = useFieldFeedback({ name: field?.name });
495521
const { state } = useFieldState({ name: field?.name });
522+
const validity = useValidityStateContext();
496523

497524
const message = feedback?.message instanceof ClerkElementsFieldError ? feedback.message.message : feedback?.message;
498525
const codes = feedback?.codes;
499526

500-
const fieldState = { state, message, codes };
527+
const fieldState = { state: enrichFieldState(validity, state), message, codes };
501528

502529
return children(fieldState);
503530
}

0 commit comments

Comments
 (0)