From 5ffcc42b4d07d9769db1cb844272cb99567fb01a Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 8 Sep 2023 13:40:27 +0300 Subject: [PATCH 1/3] chore(clerk-js): [Refactor FormControl] Introduce PlainInput --- .../OrganizationProfile/AddDomainPage.tsx | 4 +- .../src/ui/components/SignUp/SignUpForm.tsx | 40 ++- .../components/UserProfile/UsernamePage.tsx | 2 +- .../clerk-js/src/ui/elements/FieldControl.tsx | 262 ++++++++++++++++++ packages/clerk-js/src/ui/elements/Form.tsx | 46 +++ .../clerk-js/src/ui/elements/FormControl.tsx | 8 +- .../clerk-js/src/ui/elements/RadioGroup.tsx | 6 + .../src/ui/primitives/FormControl.tsx | 5 + .../ui/primitives/hooks/useFormControl.tsx | 103 +++++++ 9 files changed, 445 insertions(+), 31 deletions(-) create mode 100644 packages/clerk-js/src/ui/elements/FieldControl.tsx diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/AddDomainPage.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/AddDomainPage.tsx index 472b12fd97c..af8c2b502b0 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/AddDomainPage.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/AddDomainPage.tsx @@ -53,10 +53,10 @@ export const AddDomainPage = withCardStateProvider(() => { > - diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx index a0c584b0489..9bdde9e8f3b 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx @@ -32,44 +32,36 @@ export const SignUpForm = (props: SignUpFormProps) => { {(shouldShow('firstName') || shouldShow('lastName')) && ( {shouldShow('firstName') && ( - )} {shouldShow('lastName') && ( - )} )} {shouldShow('username') && ( - )} {shouldShow('emailAddress') && ( - handleEmailPhoneToggle('phoneNumber') : undefined} /> diff --git a/packages/clerk-js/src/ui/components/UserProfile/UsernamePage.tsx b/packages/clerk-js/src/ui/components/UserProfile/UsernamePage.tsx index 0024f4d96c9..56e5f57471d 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/UsernamePage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/UsernamePage.tsx @@ -38,7 +38,7 @@ export const UsernamePage = withCardStateProvider(() => { > - , 'label' | 'placeholder'> & + ReturnType>['props'] & { + isDisabled?: boolean; + }; + +export const Root = (props: PropsWithChildren) => { + const card = useCardState(); + const { submittedWithEnter } = useFormState(); + const { + id, + errorText, + isRequired, + sx, + setError, + successfulText, + setSuccessful, + hasLostFocus, + enableErrorAfterBlur, + informationText, + isFocused: _isFocused, + setWarning, + setHasPassedComplexity, + hasPassedComplexity, + warningText, + } = props; + + const { debounced: debouncedState } = useFormControlFeedback({ + errorText, + informationText, + enableErrorAfterBlur, + hasPassedComplexity, + isFocused: _isFocused, + hasLostFocus, + successfulText, + warningText, + skipBlur: submittedWithEnter, + }); + + const errorMessage = useFieldMessageVisibility(debouncedState.errorText, delay); + const isDisabled = props.isDisabled || card.isLoading; + + return ( + + {/*Most of our primitives still depend on this provider.*/} + {/*TODO: In follow-up PRs these will be removed*/} + + {props.children} + + + ); +}; + +const FieldAction = ( + props: PropsWithChildren<{ localizationKey?: LocalizationKey | string; onClick?: React.MouseEventHandler }>, +) => { + const { isDisabled, id } = useFormControl(); + + if (!props.localizationKey && !props.children) { + return null; + } + + return ( + { + e.preventDefault(); + props.onClick?.(e); + }} + > + {props.children} + + ); +}; + +const FieldOptionalLabel = () => { + const { isDisabled, id } = useFormControl(); + return ( + + ); +}; + +const FieldLabelIcon = (props: { icon?: React.ComponentType }) => { + const { t } = useLocalizations(); + if (!props.icon) { + return null; + } + + return ( + + ({ + marginLeft: theme.space.$0x5, + color: theme.colors.$blackAlpha400, + width: theme.sizes.$4, + height: theme.sizes.$4, + })} + /> + + ); +}; + +const FieldLabel = (props: PropsWithChildren<{ localizationKey?: LocalizationKey | string }>) => { + const { hasError, isDisabled } = useFormControl(); + const { isRequired, id, label } = useFormField(); + + if (!(props.localizationKey || label) && !props.children) { + return null; + } + + return ( + + {props.children} + + ); +}; + +const FieldLabelRow = (props: PropsWithChildren) => { + const { fieldId } = useFormField(); + return ( + ({ + marginBottom: theme.space.$1, + marginLeft: 0, + })} + > + {props.children} + + ); +}; + +const FieldFeedback = (props: Pick) => { + const { + errorText, + informationText, + enableErrorAfterBlur, + hasPassedComplexity, + isFocused, + hasLostFocus, + successfulText, + warningText, + } = useFormField(); + const { submittedWithEnter } = useFormState(); + const { debounced } = useFormControlFeedback({ + errorText, + informationText, + enableErrorAfterBlur, + hasPassedComplexity, + isFocused, + hasLostFocus, + successfulText, + warningText, + skipBlur: submittedWithEnter, + }); + + return ( + + ); +}; + +const InputElement = forwardRef((_, ref) => { + const { t } = useLocalizations(); + const formField = useFormField(); + const { placeholder, ...inputProps } = sanitizeInputProps(formField); + return ( + + ); +}); + +export const Field = { + Root: Root, + Label: FieldLabel, + LabelRow: FieldLabelRow, + Input: InputElement, + Action: FieldAction, + AsOptional: FieldOptionalLabel, + LabelIcon: FieldLabelIcon, + Feedback: FieldFeedback, +}; diff --git a/packages/clerk-js/src/ui/elements/Form.tsx b/packages/clerk-js/src/ui/elements/Form.tsx index 012b02482b8..90f6d433838 100644 --- a/packages/clerk-js/src/ui/elements/Form.tsx +++ b/packages/clerk-js/src/ui/elements/Form.tsx @@ -1,11 +1,14 @@ import { createContextAndHook } from '@clerk/shared/react'; import type { FieldId } from '@clerk/types'; +import type { PropsWithChildren } from 'react'; import React, { useState } from 'react'; +import type { LocalizationKey } from '../customizables'; import { Button, descriptors, Flex, Form as FormPrim, localizationKeys } from '../customizables'; import { useLoadingStatus } from '../hooks'; import type { PropsOfComponent } from '../styledSystem'; import { useCardState } from './contexts'; +import { Field } from './FieldControl'; import { FormControl } from './FormControl'; const [FormState, useFormState] = createContextAndHook<{ @@ -118,10 +121,53 @@ const FormControlRow = (props: Omit, 'elementId'> ); }; +type CommonFieldRootProps = Omit, 'children'>; + +type CommonInputProps = CommonFieldRootProps & { + isOptional?: boolean; + actionLabel?: string | LocalizationKey; + onActionClicked?: React.MouseEventHandler; + icon?: React.ComponentType; +}; + +const CommonInputWrapper = (props: PropsWithChildren) => { + const { isOptional, icon, actionLabel, children, onActionClicked, ...fieldProps } = props; + return ( + + + + + {!actionLabel && isOptional && } + {actionLabel && ( + + )} + + + {children} + + + ); +}; + +const PlainInput = (props: CommonInputProps) => { + return ( + + + + ); +}; + export const Form = { Root: FormRoot, ControlRow: FormControlRow, + /** + * @deprecated Use Form.PlainInput + */ Control: FormControl, + PlainInput, SubmitButton: FormSubmit, ResetButton: FormReset, }; diff --git a/packages/clerk-js/src/ui/elements/FormControl.tsx b/packages/clerk-js/src/ui/elements/FormControl.tsx index 2c4546a4a90..82d95f028ee 100644 --- a/packages/clerk-js/src/ui/elements/FormControl.tsx +++ b/packages/clerk-js/src/ui/elements/FormControl.tsx @@ -89,7 +89,7 @@ const getInputElementForType = ({ return CustomInputs[customInput] || Input; }; -function useFormTextAnimation() { +export function useFormTextAnimation() { const prefersReducedMotion = usePrefersReducedMotion(); const getFormTextAnimation = useCallback( @@ -117,7 +117,7 @@ function useFormTextAnimation() { }; } -const useCalculateErrorTextHeight = ({ feedback }: { feedback: string }) => { +export const useCalculateErrorTextHeight = ({ feedback }: { feedback: string }) => { const [height, setHeight] = useState(0); const calculateHeight = useCallback( @@ -136,11 +136,11 @@ const useCalculateErrorTextHeight = ({ feedback }: { feedback: string }) => { }; }; -type FormFeedbackDescriptorsKeys = 'error' | 'warning' | 'info' | 'success'; +export type FormFeedbackDescriptorsKeys = 'error' | 'warning' | 'info' | 'success'; type Feedback = { feedback?: string; feedbackType?: FeedbackType; shouldEnter: boolean }; -type FormFeedbackProps = Partial['debounced'] & { id: FieldId }> & { +export type FormFeedbackProps = Partial['debounced'] & { id: FieldId }> & { elementDescriptors?: Partial>; }; diff --git a/packages/clerk-js/src/ui/elements/RadioGroup.tsx b/packages/clerk-js/src/ui/elements/RadioGroup.tsx index f8be9e18b85..10dda2dd772 100644 --- a/packages/clerk-js/src/ui/elements/RadioGroup.tsx +++ b/packages/clerk-js/src/ui/elements/RadioGroup.tsx @@ -4,6 +4,9 @@ import type { LocalizationKey } from '../customizables'; import { Col, descriptors, Flex, FormLabel, Input, Text } from '../customizables'; import type { PropsOfComponent } from '../styledSystem'; +/** + * @deprecated + */ export const RadioGroup = ( props: PropsOfComponent & { radioOptions?: { @@ -30,6 +33,9 @@ export const RadioGroup = ( ); }; +/** + * @deprecated + */ const RadioGroupItem = (props: { inputProps: PropsOfComponent; value: string; diff --git a/packages/clerk-js/src/ui/primitives/FormControl.tsx b/packages/clerk-js/src/ui/primitives/FormControl.tsx index c62d70a56f1..9056e551f81 100644 --- a/packages/clerk-js/src/ui/primitives/FormControl.tsx +++ b/packages/clerk-js/src/ui/primitives/FormControl.tsx @@ -4,6 +4,11 @@ import { Flex } from './Flex'; import type { FormControlProps } from './hooks'; import { FormControlContextProvider } from './hooks'; +/** + * @deprecated Use Field.Root + * Each controlled field should have their own UI wrapper. + * Field.Root is just a Provider + */ export const FormControl = (props: React.PropsWithChildren) => { const { hasError, diff --git a/packages/clerk-js/src/ui/primitives/hooks/useFormControl.tsx b/packages/clerk-js/src/ui/primitives/hooks/useFormControl.tsx index ae03afec60c..e609cf8c153 100644 --- a/packages/clerk-js/src/ui/primitives/hooks/useFormControl.tsx +++ b/packages/clerk-js/src/ui/primitives/hooks/useFormControl.tsx @@ -1,7 +1,13 @@ import { createContextAndHook } from '@clerk/shared/react'; +import type { ClerkAPIError, FieldId } from '@clerk/types'; import type { ClerkAPIError } from '@clerk/types'; import React from 'react'; +import type { useFormControl as useFormControlUtil } from '../../utils/useFormControl'; + +/** + * @deprecated + */ export type FormControlProps = { /** * The custom `id` to use for the form control. This is passed directly to the form element (e.g, Input). @@ -19,11 +25,20 @@ export type FormControlProps = { clearFeedback: () => void; }; +/** + * @deprecated + */ type FormControlContextValue = Required & { errorMessageId: string }; +/** + * @deprecated Use FormFieldContextProvider + */ export const [FormControlContext, , useFormControl] = createContextAndHook('FormControlContext'); +/** + * @deprecated Use FormFieldContextProvider + */ export const FormControlContextProvider = (props: React.PropsWithChildren) => { const { id: propsId, @@ -76,3 +91,91 @@ export const FormControlContextProvider = (props: React.PropsWithChildren{props.children}; }; + +type FormFieldProviderProps = ReturnType>['props']; + +type FormFieldContextValue = Omit & { + errorMessageId?: string; + id?: string; + fieldId?: FieldId; +}; +export const [FormFieldContext, useFormField] = createContextAndHook('FormFieldContext'); + +export const FormFieldContextProvider = (props: React.PropsWithChildren) => { + const { + id: propsId, + isRequired = false, + setError, + setSuccessful, + setWarning, + setHasPassedComplexity, + children, + ...rest + } = props; + // TODO: This shouldnt be targettable + const id = `${propsId}-field`; + + /** + * Track whether the `FormErrorText` has been rendered. + * We use this to append its id the `aria-describedby` of the `input`. + */ + const errorMessageId = rest.errorText ? `error-${propsId}` : ''; + const value = React.useMemo( + () => ({ + isRequired, + id, + fieldId: propsId, + errorMessageId, + setError, + setSuccessful, + setWarning, + setHasPassedComplexity, + }), + [isRequired, id, errorMessageId, setError, setSuccessful, setHasPassedComplexity], + ); + return ( + + {props.children} + + ); +}; + +export const sanitizeInputProps = ( + obj: ReturnType, + keep?: (keyof ReturnType)[], +) => { + const { + radioOptions, + validatePassword, + warningText, + informationText, + hasPassedComplexity, + enableErrorAfterBlur, + isFocused, + hasLostFocus, + successfulText, + errorText, + setHasPassedComplexity, + setWarning, + setSuccessful, + setError, + errorMessageId, + fieldId, + label, + ...inputProps + } = obj; + + keep?.forEach(key => { + // @ts-ignore + inputProps[key] = obj[key]; + }); + + return inputProps; +}; From 2f89b4a4fda449ac2b9eca0995956f1d2d0388ce Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 18 Oct 2023 13:01:03 +0300 Subject: [PATCH 2/3] chore(clerk-js): Improve isDisabled, isRequired after refactor --- .changeset/bright-trainers-sort.md | 5 +++++ .../src/ui/components/SignUp/SignUpForm.tsx | 1 + .../components/UserProfile/UsernamePage.tsx | 2 +- .../clerk-js/src/ui/elements/FieldControl.tsx | 21 ++++++++----------- packages/clerk-js/src/ui/primitives/Input.tsx | 10 ++++++--- .../ui/primitives/hooks/useFormControl.tsx | 11 ++++++++-- 6 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 .changeset/bright-trainers-sort.md diff --git a/.changeset/bright-trainers-sort.md b/.changeset/bright-trainers-sort.md new file mode 100644 index 00000000000..976c66fdc0e --- /dev/null +++ b/.changeset/bright-trainers-sort.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Internal refactoring of form fields, deprecation of Form.Control and introduction of Form.PlainInput. diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx index 9bdde9e8f3b..7a5033c4418 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx @@ -43,6 +43,7 @@ export const SignUpForm = (props: SignUpFormProps) => { {...formState.lastName.props} isRequired={fields.lastName!.required} isOptional={!fields.lastName!.required} + isDisabled={true} /> )} diff --git a/packages/clerk-js/src/ui/components/UserProfile/UsernamePage.tsx b/packages/clerk-js/src/ui/components/UserProfile/UsernamePage.tsx index 56e5f57471d..b2aecc589f5 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/UsernamePage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/UsernamePage.tsx @@ -40,8 +40,8 @@ export const UsernamePage = withCardStateProvider(() => { diff --git a/packages/clerk-js/src/ui/elements/FieldControl.tsx b/packages/clerk-js/src/ui/elements/FieldControl.tsx index 8c979fca84a..692f6912ac9 100644 --- a/packages/clerk-js/src/ui/elements/FieldControl.tsx +++ b/packages/clerk-js/src/ui/elements/FieldControl.tsx @@ -16,7 +16,7 @@ import { useLocalizations, } from '../customizables'; import { useFieldMessageVisibility } from '../hooks'; -import { FormFieldContextProvider, sanitizeInputProps, useFormControl, useFormField } from '../primitives/hooks'; +import { FormFieldContextProvider, sanitizeInputProps, useFormField } from '../primitives/hooks'; import type { PropsOfComponent } from '../styledSystem'; import type { useFormControl as useFormControlUtil } from '../utils'; import { useFormControlFeedback } from '../utils'; @@ -25,10 +25,8 @@ import { useFormState } from './Form'; import type { FormFeedbackProps } from './FormControl'; import { delay, FormFeedback } from './FormControl'; -type FormControlProps = Omit, 'label' | 'placeholder'> & - ReturnType>['props'] & { - isDisabled?: boolean; - }; +type FormControlProps = Omit, 'label' | 'placeholder' | 'disabled' | 'required'> & + ReturnType>['props']; export const Root = (props: PropsWithChildren) => { const card = useCardState(); @@ -67,7 +65,7 @@ export const Root = (props: PropsWithChildren) => { const isDisabled = props.isDisabled || card.isLoading; return ( - + {/*Most of our primitives still depend on this provider.*/} {/*TODO: In follow-up PRs these will be removed*/} ) => { const FieldAction = ( props: PropsWithChildren<{ localizationKey?: LocalizationKey | string; onClick?: React.MouseEventHandler }>, ) => { - const { isDisabled, id } = useFormControl(); + const { fieldId, isDisabled } = useFormField(); if (!props.localizationKey && !props.children) { return null; @@ -102,7 +100,7 @@ const FieldAction = ( { @@ -116,12 +114,12 @@ const FieldAction = ( }; const FieldOptionalLabel = () => { - const { isDisabled, id } = useFormControl(); + const { fieldId, isDisabled } = useFormField(); return ( { }; const FieldLabel = (props: PropsWithChildren<{ localizationKey?: LocalizationKey | string }>) => { - const { hasError, isDisabled } = useFormControl(); - const { isRequired, id, label } = useFormField(); + const { isRequired, id, label, isDisabled, hasError } = useFormField(); if (!(props.localizationKey || label) && !props.children) { return null; diff --git a/packages/clerk-js/src/ui/primitives/Input.tsx b/packages/clerk-js/src/ui/primitives/Input.tsx index 1d6788e6a29..9967fc30b06 100644 --- a/packages/clerk-js/src/ui/primitives/Input.tsx +++ b/packages/clerk-js/src/ui/primitives/Input.tsx @@ -49,6 +49,9 @@ export const Input = React.forwardRef((props, ref) }); const { onChange } = useInput(propsWithoutVariants.onChange); const { isDisabled, hasError, focusRing, isRequired, ...rest } = propsWithoutVariants; + const _disabled = isDisabled || formControlProps.isDisabled; + const _required = isRequired || formControlProps.isRequired; + const _hasError = hasError || formControlProps.hasError; return ( ((props, ref) ref={ref} onChange={onChange} disabled={isDisabled} - required={isRequired || formControlProps.isRequired} + required={_required} id={props.id || formControlProps.id} - aria-invalid={hasError || formControlProps.hasError} + aria-invalid={_hasError} aria-describedby={formControlProps.errorMessageId} - aria-required={formControlProps.isRequired} + aria-required={_required} + aria-disabled={_disabled} css={applyVariants(propsWithoutVariants)} /> ); diff --git a/packages/clerk-js/src/ui/primitives/hooks/useFormControl.tsx b/packages/clerk-js/src/ui/primitives/hooks/useFormControl.tsx index e609cf8c153..d38d65234b6 100644 --- a/packages/clerk-js/src/ui/primitives/hooks/useFormControl.tsx +++ b/packages/clerk-js/src/ui/primitives/hooks/useFormControl.tsx @@ -92,7 +92,10 @@ export const FormControlContextProvider = (props: React.PropsWithChildren{props.children}; }; -type FormFieldProviderProps = ReturnType>['props']; +type FormFieldProviderProps = ReturnType>['props'] & { + hasError?: boolean; + isDisabled?: boolean; +}; type FormFieldContextValue = Omit & { errorMessageId?: string; @@ -105,6 +108,8 @@ export const FormFieldContextProvider = (props: React.PropsWithChildren ({ isRequired, + isDisabled, + hasError, id, fieldId: propsId, errorMessageId, @@ -131,7 +138,7 @@ export const FormFieldContextProvider = (props: React.PropsWithChildren Date: Tue, 17 Oct 2023 06:04:39 -0700 Subject: [PATCH 3/3] chore(clerk-js): Address PR comments --- .../src/ui/components/SignUp/SignUpForm.tsx | 1 - .../clerk-js/src/ui/elements/FieldControl.tsx | 66 +++++-------------- .../clerk-js/src/ui/elements/RadioGroup.tsx | 6 -- .../ui/primitives/hooks/useFormControl.tsx | 42 ++++++++---- .../clerk-js/src/ui/utils/useFormControl.ts | 17 ++++- 5 files changed, 61 insertions(+), 71 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx index 7a5033c4418..9bdde9e8f3b 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx @@ -43,7 +43,6 @@ export const SignUpForm = (props: SignUpFormProps) => { {...formState.lastName.props} isRequired={fields.lastName!.required} isOptional={!fields.lastName!.required} - isDisabled={true} /> )} diff --git a/packages/clerk-js/src/ui/elements/FieldControl.tsx b/packages/clerk-js/src/ui/elements/FieldControl.tsx index 692f6912ac9..55d293dcaf6 100644 --- a/packages/clerk-js/src/ui/elements/FieldControl.tsx +++ b/packages/clerk-js/src/ui/elements/FieldControl.tsx @@ -15,53 +15,36 @@ import { Text, useLocalizations, } from '../customizables'; -import { useFieldMessageVisibility } from '../hooks'; import { FormFieldContextProvider, sanitizeInputProps, useFormField } from '../primitives/hooks'; import type { PropsOfComponent } from '../styledSystem'; import type { useFormControl as useFormControlUtil } from '../utils'; import { useFormControlFeedback } from '../utils'; import { useCardState } from './contexts'; -import { useFormState } from './Form'; import type { FormFeedbackProps } from './FormControl'; -import { delay, FormFeedback } from './FormControl'; +import { FormFeedback } from './FormControl'; type FormControlProps = Omit, 'label' | 'placeholder' | 'disabled' | 'required'> & ReturnType>['props']; -export const Root = (props: PropsWithChildren) => { +const Root = (props: PropsWithChildren) => { const card = useCardState(); - const { submittedWithEnter } = useFormState(); const { id, - errorText, isRequired, sx, setError, - successfulText, - setSuccessful, - hasLostFocus, - enableErrorAfterBlur, - informationText, - isFocused: _isFocused, + setInfo, + setSuccess, setWarning, setHasPassedComplexity, - hasPassedComplexity, - warningText, + clearFeedback, + feedbackType, + feedback, + isFocused, } = props; - const { debounced: debouncedState } = useFormControlFeedback({ - errorText, - informationText, - enableErrorAfterBlur, - hasPassedComplexity, - isFocused: _isFocused, - hasLostFocus, - successfulText, - warningText, - skipBlur: submittedWithEnter, - }); + const { debounced: debouncedState } = useFormControlFeedback({ feedback, feedbackType, isFocused }); - const errorMessage = useFieldMessageVisibility(debouncedState.errorText, delay); const isDisabled = props.isDisabled || card.isLoading; return ( @@ -72,13 +55,15 @@ export const Root = (props: PropsWithChildren) => { elementDescriptor={descriptors.formField} elementId={descriptors.formField.setId(id)} id={id} - hasError={!!errorMessage} + hasError={debouncedState.feedbackType === 'error'} isDisabled={isDisabled} isRequired={isRequired} setError={setError} - setSuccessful={setSuccessful} + setSuccess={setSuccess} setWarning={setWarning} + setInfo={setInfo} setHasPassedComplexity={setHasPassedComplexity} + clearFeedback={clearFeedback} sx={sx} > {props.children} @@ -199,34 +184,15 @@ const FieldLabelRow = (props: PropsWithChildren) => { }; const FieldFeedback = (props: Pick) => { - const { - errorText, - informationText, - enableErrorAfterBlur, - hasPassedComplexity, - isFocused, - hasLostFocus, - successfulText, - warningText, - } = useFormField(); - const { submittedWithEnter } = useFormState(); - const { debounced } = useFormControlFeedback({ - errorText, - informationText, - enableErrorAfterBlur, - hasPassedComplexity, - isFocused, - hasLostFocus, - successfulText, - warningText, - skipBlur: submittedWithEnter, - }); + const { feedback, feedbackType, isFocused, fieldId } = useFormField(); + const { debounced } = useFormControlFeedback({ feedback, feedbackType, isFocused }); return ( ); diff --git a/packages/clerk-js/src/ui/elements/RadioGroup.tsx b/packages/clerk-js/src/ui/elements/RadioGroup.tsx index 10dda2dd772..f8be9e18b85 100644 --- a/packages/clerk-js/src/ui/elements/RadioGroup.tsx +++ b/packages/clerk-js/src/ui/elements/RadioGroup.tsx @@ -4,9 +4,6 @@ import type { LocalizationKey } from '../customizables'; import { Col, descriptors, Flex, FormLabel, Input, Text } from '../customizables'; import type { PropsOfComponent } from '../styledSystem'; -/** - * @deprecated - */ export const RadioGroup = ( props: PropsOfComponent & { radioOptions?: { @@ -33,9 +30,6 @@ export const RadioGroup = ( ); }; -/** - * @deprecated - */ const RadioGroupItem = (props: { inputProps: PropsOfComponent; value: string; diff --git a/packages/clerk-js/src/ui/primitives/hooks/useFormControl.tsx b/packages/clerk-js/src/ui/primitives/hooks/useFormControl.tsx index d38d65234b6..461d11a3e18 100644 --- a/packages/clerk-js/src/ui/primitives/hooks/useFormControl.tsx +++ b/packages/clerk-js/src/ui/primitives/hooks/useFormControl.tsx @@ -1,6 +1,5 @@ import { createContextAndHook } from '@clerk/shared/react'; import type { ClerkAPIError, FieldId } from '@clerk/types'; -import type { ClerkAPIError } from '@clerk/types'; import React from 'react'; import type { useFormControl as useFormControlUtil } from '../../utils/useFormControl'; @@ -111,9 +110,11 @@ export const FormFieldContextProvider = (props: React.PropsWithChildren ({ isRequired, @@ -134,11 +135,26 @@ export const FormFieldContextProvider = (props: React.PropsWithChildren, keep?: (keyof ReturnType)[], ) => { + /* eslint-disable */ const { radioOptions, validatePassword, - warningText, - informationText, hasPassedComplexity, - enableErrorAfterBlur, isFocused, - hasLostFocus, - successfulText, - errorText, + feedback, + feedbackType, setHasPassedComplexity, setWarning, - setSuccessful, + setSuccess, setError, + setInfo, errorMessageId, fieldId, label, ...inputProps } = obj; + /* eslint-enable */ keep?.forEach(key => { + /** + * Ignore error for the index type as we have defined it explicitly above + */ // @ts-ignore inputProps[key] = obj[key]; }); diff --git a/packages/clerk-js/src/ui/utils/useFormControl.ts b/packages/clerk-js/src/ui/utils/useFormControl.ts index 8e043f77be8..8fd9cd98948 100644 --- a/packages/clerk-js/src/ui/utils/useFormControl.ts +++ b/packages/clerk-js/src/ui/utils/useFormControl.ts @@ -48,6 +48,8 @@ type FieldStateProps = { value: string; checked?: boolean; onChange: React.ChangeEventHandler; + onBlur: React.FocusEventHandler; + onFocus: React.FocusEventHandler; feedback: string; feedbackType: FeedbackType; setError: (error: string | ClerkAPIError | undefined) => void; @@ -57,6 +59,7 @@ type FieldStateProps = { setHasPassedComplexity: (b: boolean) => void; clearFeedback: () => void; hasPassedComplexity: boolean; + isFocused: boolean; } & Omit; export type FormControlState = FieldStateProps & { @@ -87,6 +90,7 @@ export const useFormControl = ( const { translateError, t } = useLocalizations(); const [value, setValueInternal] = useState(initialState); + const [isFocused, setFocused] = useState(false); const [checked, setCheckedInternal] = useState(opts?.defaultChecked || false); const [hasPassedComplexity, setHasPassedComplexity] = useState(false); const [feedback, setFeedback] = useState<{ message: string; type: FeedbackType }>({ @@ -130,6 +134,14 @@ export const useFormControl = ( setFeedback({ message: '', type: 'info' }); }; + const onFocus: FormControlState['onFocus'] = () => { + setFocused(true); + }; + + const onBlur: FormControlState['onBlur'] = () => { + setFocused(false); + }; + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { defaultChecked, validatePassword: validatePasswordProp, buildErrorMessage, ...restOpts } = opts; @@ -141,6 +153,8 @@ export const useFormControl = ( setSuccess, setError, onChange, + onBlur, + onFocus, setWarning, feedback: feedback.message || t(opts.infoText), feedbackType: feedback.type, @@ -149,6 +163,7 @@ export const useFormControl = ( hasPassedComplexity, setHasPassedComplexity, validatePassword: opts.type === 'password' ? opts.validatePassword : undefined, + isFocused, ...restOpts, }; @@ -178,10 +193,8 @@ type DebouncingOption = { isFocused?: boolean; delayInMs?: number; }; - export const useFormControlFeedback = (opts?: DebouncingOption): DebouncedFeedback => { const { feedback = '', delayInMs = 100, feedbackType = 'info', isFocused = false } = opts || {}; - const shouldHide = isFocused ? false : ['info', 'warning'].includes(feedbackType); const debouncedState = useDebounce(