diff --git a/.changeset/proud-hairs-check.md b/.changeset/proud-hairs-check.md new file mode 100644 index 0000000000..939ebe378f --- /dev/null +++ b/.changeset/proud-hairs-check.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Refactor of internal input group, password field, and checkbox inputs in forms. diff --git a/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/VerifiedDomainPage.tsx b/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/VerifiedDomainPage.tsx index 2dc351302b..c6283731d6 100644 --- a/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/VerifiedDomainPage.tsx +++ b/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/VerifiedDomainPage.tsx @@ -263,7 +263,7 @@ export const VerifiedDomainPage = withCardStateProvider(() => { {allowsEdit && ( - + )} diff --git a/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/VerifyDomainPage.tsx b/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/VerifyDomainPage.tsx index 5429b09f53..c551ab7143 100644 --- a/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/VerifyDomainPage.tsx +++ b/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/VerifyDomainPage.tsx @@ -151,11 +151,11 @@ export const VerifyDomainPage = withCardStateProvider(() => { > - diff --git a/packages/clerk-js/src/ui.retheme/components/SignIn/ResetPassword.tsx b/packages/clerk-js/src/ui.retheme/components/SignIn/ResetPassword.tsx index 27b80d34cb..8e6a6f9676 100644 --- a/packages/clerk-js/src/ui.retheme/components/SignIn/ResetPassword.tsx +++ b/packages/clerk-js/src/ui.retheme/components/SignIn/ResetPassword.tsx @@ -88,7 +88,7 @@ export const _ResetPassword = () => { return console.error(clerkInvalidFAPIResponse(status, supportEmail)); } } catch (e) { - handleError(e, [passwordField, confirmField], card.setError); + return handleError(e, [passwordField, confirmField], card.setError); } }; @@ -117,14 +117,14 @@ export const _ResetPassword = () => { style={{ display: 'none' }} /> - - { if (e.target.value) { @@ -136,7 +136,7 @@ export const _ResetPassword = () => { {!requiresNewPassword && ( - + )} - diff --git a/packages/clerk-js/src/ui.retheme/components/SignIn/SignInStart.tsx b/packages/clerk-js/src/ui.retheme/components/SignIn/SignInStart.tsx index ff34cec6a0..d5b7c2a70e 100644 --- a/packages/clerk-js/src/ui.retheme/components/SignIn/SignInStart.tsx +++ b/packages/clerk-js/src/ui.retheme/components/SignIn/SignInStart.tsx @@ -368,7 +368,7 @@ const InstantPasswordRow = ({ field }: { field?: FormControlState<'password'> }) !field.value && !autofilled ? { opacity: 0, height: 0, pointerEvents: 'none', marginTop: '-1rem' } : undefined } > - { )} {shouldShow('password') && ( - { /> {user.passwordEnabled && ( - )} - - { if (e.target.value) { @@ -170,12 +170,15 @@ export const PasswordPage = withCardStateProvider(() => { } return confirmField.props.onChange(e); }} + isRequired isDisabled={passwordEditDisabled} /> - diff --git a/packages/clerk-js/src/ui.retheme/components/UserProfile/__tests__/PasswordPage.test.tsx b/packages/clerk-js/src/ui.retheme/components/UserProfile/__tests__/PasswordPage.test.tsx index 26c1fd20e1..1d4f7998e5 100644 --- a/packages/clerk-js/src/ui.retheme/components/UserProfile/__tests__/PasswordPage.test.tsx +++ b/packages/clerk-js/src/ui.retheme/components/UserProfile/__tests__/PasswordPage.test.tsx @@ -79,7 +79,7 @@ describe('PasswordPage', () => { expect(screen.getByLabelText(/new password/i)).toBeDisabled(); expect(screen.getByLabelText(/confirm password/i)).toBeDisabled(); - expect(screen.getByRole('checkbox', { name: 'Sign out of all other devices' })).toBeDisabled(); + expect(screen.getByRole('checkbox', { name: /sign out of all other devices/i })).toBeDisabled(); expect( screen.getByText( @@ -113,7 +113,7 @@ describe('PasswordPage', () => { expect(screen.getByLabelText(/new password/i)).not.toBeDisabled(); expect(screen.getByLabelText(/confirm password/i)).not.toBeDisabled(); - expect(screen.getByRole('checkbox', { name: 'Sign out of all other devices' })).not.toBeDisabled(); + expect(screen.getByRole('checkbox', { name: /sign out of all other devices/i })).not.toBeDisabled(); expect( screen.queryByText( @@ -149,7 +149,7 @@ describe('PasswordPage', () => { expect(screen.getByLabelText(/current password/i)).toBeDisabled(); expect(screen.getByLabelText(/new password/i)).toBeDisabled(); expect(screen.getByLabelText(/confirm password/i)).toBeDisabled(); - expect(screen.getByRole('checkbox', { name: 'Sign out of all other devices' })).toBeDisabled(); + expect(screen.getByRole('checkbox', { name: /sign out of all other devices/i })).toBeDisabled(); expect( screen.getByText( @@ -185,7 +185,7 @@ describe('PasswordPage', () => { expect(screen.getByLabelText(/current password/i)).not.toBeDisabled(); expect(screen.getByLabelText(/new password/i)).not.toBeDisabled(); expect(screen.getByLabelText(/confirm password/i)).not.toBeDisabled(); - expect(screen.getByRole('checkbox', { name: 'Sign out of all other devices' })).not.toBeDisabled(); + expect(screen.getByRole('checkbox', { name: /sign out of all other devices/i })).not.toBeDisabled(); expect( screen.queryByText( diff --git a/packages/clerk-js/src/ui.retheme/elements/FieldControl.tsx b/packages/clerk-js/src/ui.retheme/elements/FieldControl.tsx index cebfec3819..8ffecae983 100644 --- a/packages/clerk-js/src/ui.retheme/elements/FieldControl.tsx +++ b/packages/clerk-js/src/ui.retheme/elements/FieldControl.tsx @@ -22,7 +22,9 @@ import { useFormControlFeedback } from '../utils'; import { useCardState } from './contexts'; import type { FormFeedbackProps } from './FormControl'; import { FormFeedback } from './FormControl'; -import { RadioItem } from './RadioGroup'; +import { InputGroup } from './InputGroup'; +import { PasswordInput } from './PasswordInput'; +import { RadioItem, RadioLabel } from './RadioGroup'; type FormControlProps = Omit, 'label' | 'placeholder' | 'disabled' | 'required'> & ReturnType>['props']; @@ -37,7 +39,6 @@ const Root = (props: PropsWithChildren) => { setInfo, setSuccess, setWarning, - setHasPassedComplexity, clearFeedback, feedbackType, feedback, @@ -63,7 +64,6 @@ const Root = (props: PropsWithChildren) => { setSuccess={setSuccess} setWarning={setWarning} setInfo={setInfo} - setHasPassedComplexity={setHasPassedComplexity} clearFeedback={clearFeedback} sx={sx} > @@ -199,6 +199,66 @@ const FieldFeedback = (props: Pick) => ); }; +const PasswordInputElement = forwardRef((_, ref) => { + const { t } = useLocalizations(); + const formField = useFormField(); + const { placeholder, ...inputProps } = sanitizeInputProps(formField, [ + 'validatePassword', + 'setError', + 'setWarning', + 'setSuccess', + 'setInfo', + 'setHasPassedComplexity', + ]); + + return ( + // @ts-expect-error + + ); +}); + +const CheckboxIndicator = forwardRef((_, ref) => { + const formField = useFormField(); + const { placeholder, ...inputProps } = sanitizeInputProps(formField); + + return ( + ({ + width: 'fit-content', + marginTop: t.space.$0x5, + })} + type='checkbox' + /> + ); +}); + +const CheckboxLabel = (props: { description?: string | LocalizationKey }) => { + const { label, id } = useFormField(); + + if (!label) { + return null; + } + + return ( + + ); +}; + const InputElement = forwardRef((_, ref) => { const { t } = useLocalizations(); const formField = useFormField(); @@ -214,12 +274,40 @@ const InputElement = forwardRef((_, ref) => { ); }); +const InputGroupElement = forwardRef< + HTMLInputElement, + { + groupPrefix?: string; + groupSuffix?: string; + } +>((props, ref) => { + const { t } = useLocalizations(); + const formField = useFormField(); + const { placeholder, ...inputProps } = sanitizeInputProps(formField); + + return ( + + ); +}); + export const Field = { Root: Root, Label: FieldLabel, LabelRow: FieldLabelRow, Input: InputElement, + PasswordInput: PasswordInputElement, + InputGroup: InputGroupElement, RadioItem: RadioItem, + CheckboxIndicator: CheckboxIndicator, + CheckboxLabel: CheckboxLabel, Action: FieldAction, AsOptional: FieldOptionalLabel, LabelIcon: FieldLabelIcon, diff --git a/packages/clerk-js/src/ui.retheme/elements/Form.tsx b/packages/clerk-js/src/ui.retheme/elements/Form.tsx index aded9dfa07..caa4bf0198 100644 --- a/packages/clerk-js/src/ui.retheme/elements/Form.tsx +++ b/packages/clerk-js/src/ui.retheme/elements/Form.tsx @@ -156,6 +156,43 @@ const PlainInput = (props: CommonInputProps) => { ); }; +const PasswordInput = (props: CommonInputProps) => { + return ( + + + + ); +}; + +const InputGroup = ( + props: CommonInputProps & { + groupPrefix?: string; + groupSuffix?: string; + }, +) => { + const { groupSuffix, groupPrefix, ...fieldProps } = props; + return ( + + + + ); +}; + +const Checkbox = ( + props: CommonFieldRootProps & { + description?: string | LocalizationKey; + }, +) => { + return ( + + + + + + + ); +}; + const RadioGroup = ( props: Omit, 'infoText' | 'type' | 'validatePassword' | 'label' | 'placeholder'>, ) => { @@ -189,7 +226,10 @@ export const Form = { */ Control: FormControl, PlainInput, + PasswordInput, + InputGroup, RadioGroup, + Checkbox, SubmitButton: FormSubmit, ResetButton: FormReset, }; diff --git a/packages/clerk-js/src/ui.retheme/elements/FormControl.tsx b/packages/clerk-js/src/ui.retheme/elements/FormControl.tsx index b075ce8ccd..228e943801 100644 --- a/packages/clerk-js/src/ui.retheme/elements/FormControl.tsx +++ b/packages/clerk-js/src/ui.retheme/elements/FormControl.tsx @@ -22,15 +22,13 @@ import { } from '../customizables'; import type { ElementDescriptor } from '../customizables/elementDescriptors'; import { usePrefersReducedMotion } from '../hooks'; +import { sanitizeInputProps } from '../primitives/hooks'; import type { PropsOfComponent, ThemableCssProp } from '../styledSystem'; import { animations } from '../styledSystem'; import type { FeedbackType } from '../utils'; import { useFormControlFeedback } from '../utils'; import { useCardState } from './contexts'; -import { InputGroup } from './InputGroup'; -import { PasswordInput } from './PasswordInput'; import { PhoneInput } from './PhoneInput'; -import { RadioGroup } from './RadioGroup'; type FormControlProps = Omit, 'label' | 'placeholder'> & { id: FieldId; @@ -43,45 +41,23 @@ type FormControlProps = Omit, 'label' | 'placehol placeholder?: string | LocalizationKey; actionLabel?: string | LocalizationKey; icon?: React.ComponentType; - validatePassword?: boolean; setError: (error: string | ClerkAPIError | undefined) => void; setWarning: (warning: string) => void; setInfo: (info: string) => void; setSuccess: (message: string) => void; feedback: string; feedbackType: FeedbackType; - setHasPassedComplexity: (b: boolean) => void; clearFeedback: () => void; hasPassedComplexity: boolean; infoText?: string | LocalizationKey; - radioOptions?: { - value: string; - label: string | LocalizationKey; - description?: string | LocalizationKey; - }[]; - groupPreffix?: string; - groupSuffix?: string; }; // TODO: Convert this into a Component? -const getInputElementForType = ({ - type, - groupPreffix, - groupSuffix, -}: { - type: FormControlProps['type']; - groupPreffix: string | undefined; - groupSuffix: string | undefined; -}) => { +const getInputElementForType = ({ type }: { type: FormControlProps['type'] }) => { const CustomInputs = { - password: PasswordInput, tel: PhoneInput, - radio: RadioGroup, }; - if (groupPreffix || groupSuffix) { - return InputGroup; - } if (!type) { return Input; } @@ -283,50 +259,18 @@ export const FormControl = forwardRef { - const propMap = { - password: restInputProps, - radio: { - ...inputProps, - radioOptions, - }, - }; - - if (groupPreffix || groupSuffix) { - return { - ...inputProps, - groupPreffix, - groupSuffix, - }; - } - - if (!props.type) { - return inputProps; - } - const type = props.type as keyof typeof propMap; - return propMap[type] || inputProps; - }, [restInputProps]); - const InputElement = getInputElementForType({ type: props.type, - groupPreffix, - groupSuffix, }); - const isCheckbox = props.type === 'checkbox'; - const { debounced: debouncedState } = useFormControlFeedback({ feedback, feedbackType, isFocused }); const ActionLabel = actionLabel ? ( @@ -400,13 +344,7 @@ export const FormControl = forwardRef { - inputElementProps.onFocus?.(e); - }} - onBlur={e => { - inputElementProps.onBlur?.(e); - }} + {...sanitizedInputProps} ref={ref} placeholder={t(placeholder)} /> @@ -424,49 +362,25 @@ export const FormControl = forwardRef - {isCheckbox ? ( - - {Input} - ({ - marginBottom: isCheckbox ? 0 : theme.space.$1, - marginLeft: !isCheckbox ? 0 : theme.space.$1, - })} - > - {FieldLabel} - {Icon} - {HintLabel} - {ActionLabel} - - - ) : ( - <> - ({ - marginBottom: isCheckbox ? 0 : theme.space.$1, - marginLeft: !isCheckbox ? 0 : theme.space.$1, - })} - > - {FieldLabel} - {Icon} - {HintLabel} - {ActionLabel} - - {Input} - - )} + ({ + marginBottom: theme.space.$1, + marginLeft: 0, + })} + > + {FieldLabel} + {Icon} + {HintLabel} + {ActionLabel} + + {Input} ; export const InputGroup = forwardRef< HTMLInputElement, InputGroupProps & { - groupPreffix?: string; + groupPrefix?: string; groupSuffix?: string; } >((props, ref) => { - const { sx, groupPreffix, groupSuffix, ...rest } = props; + const { sx, groupPrefix, groupSuffix, ...rest } = props; - const inputBorder = groupPreffix + const inputBorder = groupPrefix ? { borderTopLeftRadius: '0', borderBottomLeftRadius: '0', @@ -47,7 +47,7 @@ export const InputGroup = forwardRef< borderColor: theme.colors.$blackAlpha300, // we use this value in the Input primitive })} > - {groupPreffix && {groupPreffix}} + {groupPrefix && {groupPrefix}} & { validatePassword?: boolean; + setError: (error: string | ClerkAPIError | undefined) => void; + setWarning: (warning: string) => void; + setSuccess: (message: string) => void; + setInfo: (info: string) => void; + setHasPassedComplexity: (b: boolean) => void; }; export const PasswordInput = forwardRef((props, ref) => { const [hidden, setHidden] = React.useState(true); - const { id, onChange: onChangeProp, validatePassword: validatePasswordProp = false, ...rest } = props; + const { + id, + onChange: onChangeProp, + validatePassword: validatePasswordProp = false, + setInfo, + setSuccess, + setWarning, + setError, + setHasPassedComplexity, + ...rest + } = props; const inputRef = useRef(null); const [timeoutState, setTimeoutState] = useState | null>(null); @@ -25,25 +40,23 @@ export const PasswordInput = forwardRef((p userSettings: { passwordSettings }, } = useEnvironment(); - const formControlProps = useFormControl(); const { t } = useLocalizations(); const { validatePassword } = usePassword( { ...passwordSettings, validatePassword: validatePasswordProp }, { - onValidationSuccess: () => - formControlProps?.setSuccess?.(t(localizationKeys('unstable__errors.zxcvbn.goodPassword'))), - onValidationError: message => formControlProps?.setError?.(message), - onValidationWarning: message => formControlProps?.setWarning?.(message), + onValidationSuccess: () => setSuccess(t(localizationKeys('unstable__errors.zxcvbn.goodPassword'))), + onValidationError: message => setError(message), + onValidationWarning: message => setWarning(message), onValidationInfo: message => { if (inputRef.current === document.activeElement) { - formControlProps?.setInfo?.(message); + setInfo(message); } else { // Turn the suggestion into an error if not focused. - formControlProps?.setError?.(message); + setError(message); } }, - onValidationComplexity: hasPassed => formControlProps?.setHasPassedComplexity?.(hasPassed), + onValidationComplexity: hasPassed => setHasPassedComplexity(hasPassed), }, ); @@ -79,7 +92,7 @@ export const PasswordInput = forwardRef((p // Call validate password because to calculate the new feedbackType as the element is now focused validatePassword(e.target.value); }} - //@ts-expect-error + //@ts-expect-error Type mismatch between ForwardRef and RefObject due to null ref={mergeRefs(ref, inputRef)} type={hidden ? 'password' : 'text'} sx={theme => ({ paddingRight: theme.space.$10 })} diff --git a/packages/clerk-js/src/ui.retheme/elements/RadioGroup.tsx b/packages/clerk-js/src/ui.retheme/elements/RadioGroup.tsx index 2d857c0ce1..0b2d4461a9 100644 --- a/packages/clerk-js/src/ui.retheme/elements/RadioGroup.tsx +++ b/packages/clerk-js/src/ui.retheme/elements/RadioGroup.tsx @@ -1,98 +1,8 @@ import { forwardRef, useId } from 'react'; import type { LocalizationKey } from '../customizables'; -import { Col, descriptors, Flex, FormLabel, Input, Text } from '../customizables'; +import { descriptors, Flex, FormLabel, Input, Text } from '../customizables'; import { sanitizeInputProps, useFormField } from '../primitives/hooks'; -import type { PropsOfComponent } from '../styledSystem'; - -/** - * @deprecated - */ -export const RadioGroup = ( - props: PropsOfComponent & { - radioOptions?: { - value: string; - label: string | LocalizationKey; - description?: string | LocalizationKey; - }[]; - }, -) => { - const { radioOptions, ...rest } = props; - return ( - - {radioOptions?.map(r => ( - - ))} - - ); -}; - -/** - * @deprecated - */ -const RadioGroupItem = (props: { - inputProps: PropsOfComponent; - value: string; - label: string | LocalizationKey; - description?: string | LocalizationKey; -}) => { - const id = useId(); - return ( - - ({ - width: 'fit-content', - marginTop: t.space.$0x5, - }), - props.inputProps.sx, - ]} - type='radio' - value={props.value} - checked={props.value === props.inputProps.value} - /> - - ({ - padding: `${t.space.$none} ${t.space.$2}`, - display: 'flex', - flexDirection: 'column', - })} - > - - - {props.description && ( - - )} - - - ); -}; const RadioIndicator = forwardRef((props, ref) => { const formField = useFormField(); diff --git a/packages/clerk-js/src/ui.retheme/elements/__tests__/RadioGroup.test.tsx b/packages/clerk-js/src/ui.retheme/elements/__tests__/RadioGroup.test.tsx index aafcca099d..dd0d8ea76c 100644 --- a/packages/clerk-js/src/ui.retheme/elements/__tests__/RadioGroup.test.tsx +++ b/packages/clerk-js/src/ui.retheme/elements/__tests__/RadioGroup.test.tsx @@ -29,28 +29,6 @@ const createField = (...params: Parameters) => { }; }; -// TODO: Remove this once FormControl is no longer used -const createFormControl = (...params: Parameters) => { - const MockFieldWrapper = withCardStateProvider((props: Partial[0]>) => { - const field = useFormControl(...params); - - return ( - <> - {/* @ts-ignore*/} - - - - ); - }); - - return { - Field: MockFieldWrapper, - }; -}; - describe('RadioGroup', () => { it('renders the component', async () => { const { wrapper } = await createFixtures(); @@ -204,148 +182,3 @@ describe('RadioGroup', () => { }); }); }); - -/** - * This tests ensure that the deprecated FormControl and RadioGroup continue to behave the same and nothing broke during the refactoring. - */ -describe('Form control as text', () => { - it('renders the component', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('some-radio', '', { - type: 'radio', - radioOptions: [ - { value: 'one', label: 'One' }, - { value: 'two', label: 'Two' }, - ], - }); - - const { getAllByRole } = render(, { wrapper }); - - const radios = getAllByRole('radio'); - expect(radios[0]).toHaveAttribute('value', 'one'); - expect(radios[0].nextSibling).toHaveTextContent('One'); - expect(radios[1]).toHaveAttribute('value', 'two'); - expect(radios[1].nextSibling).toHaveTextContent('Two'); - - radios.forEach(radio => { - expect(radio).not.toBeChecked(); - expect(radio).toHaveAttribute('name', 'some-radio'); - expect(radio).not.toHaveAttribute('required'); - expect(radio).not.toHaveAttribute('disabled'); - }); - - expect(radios[1]).not.toBeChecked(); - }); - - it('renders the component with default value', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('some-radio', 'two', { - type: 'radio', - radioOptions: [ - { value: 'one', label: 'One' }, - { value: 'two', label: 'Two' }, - ], - }); - - const { getAllByRole } = render(, { wrapper }); - - const radios = getAllByRole('radio'); - expect(radios[0]).toHaveAttribute('value', 'one'); - expect(radios[0].nextSibling).toHaveTextContent('One'); - expect(radios[1]).toHaveAttribute('value', 'two'); - expect(radios[1].nextSibling).toHaveTextContent('Two'); - - radios.forEach(radio => { - expect(radio).toHaveAttribute('type', 'radio'); - expect(radio).not.toHaveAttribute('required'); - expect(radio).not.toHaveAttribute('disabled'); - expect(radio).toHaveAttribute('aria-required', 'false'); - expect(radio).toHaveAttribute('aria-disabled', 'false'); - }); - - expect(radios[0]).not.toBeChecked(); - expect(radios[1]).toBeChecked(); - }); - - it('disabled', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('some-radio', 'two', { - type: 'radio', - radioOptions: [ - { value: 'one', label: 'One' }, - { value: 'two', label: 'Two' }, - ], - }); - - const { getAllByRole } = render(, { wrapper }); - - const radios = getAllByRole('radio'); - radios.forEach(radio => { - expect(radio).not.toHaveAttribute('required'); - expect(radio).toHaveAttribute('disabled'); - expect(radio).toHaveAttribute('aria-disabled', 'true'); - }); - }); - - it('required', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('some-radio', 'two', { - type: 'radio', - radioOptions: [ - { value: 'one', label: 'One' }, - { value: 'two', label: 'Two' }, - ], - }); - - const { getAllByRole } = render(, { wrapper }); - - const radios = getAllByRole('radio'); - radios.forEach(radio => { - expect(radio).toHaveAttribute('required'); - expect(radio).toHaveAttribute('aria-required', 'true'); - }); - }); - - it('with error', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('some-radio', 'two', { - type: 'radio', - radioOptions: [ - { value: 'one', label: 'One' }, - { value: 'two', label: 'Two' }, - ], - }); - - const { getAllByRole, getByRole, getByText } = render(, { wrapper }); - - await act(() => userEvent.click(getByRole('button', { name: /set error/i }))); - - await waitFor(() => { - const radios = getAllByRole('radio'); - radios.forEach(radio => { - expect(radio).toHaveAttribute('aria-invalid', 'true'); - expect(radio).toHaveAttribute('aria-describedby', 'error-some-radio'); - }); - expect(getByText('some error')).toBeInTheDocument(); - }); - }); - - it('with info', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('some-radio', '', { - type: 'radio', - radioOptions: [ - { value: 'one', label: 'One' }, - { value: 'two', label: 'Two' }, - ], - infoText: 'some info', - }); - - const { getByLabelText, getByText } = render(, { wrapper }); - - await act(() => fireEvent.focus(getByLabelText('One'))); - await waitFor(() => { - expect(getByText('some info')).toBeInTheDocument(); - }); - }); -}); diff --git a/packages/clerk-js/src/ui.retheme/primitives/FormControl.tsx b/packages/clerk-js/src/ui.retheme/primitives/FormControl.tsx index 9056e551f8..55dd23794d 100644 --- a/packages/clerk-js/src/ui.retheme/primitives/FormControl.tsx +++ b/packages/clerk-js/src/ui.retheme/primitives/FormControl.tsx @@ -10,18 +10,7 @@ import { FormControlContextProvider } from './hooks'; * Field.Root is just a Provider */ export const FormControl = (props: React.PropsWithChildren) => { - const { - hasError, - id, - isRequired, - setError, - setInfo, - clearFeedback, - setSuccess, - setWarning, - setHasPassedComplexity, - ...rest - } = props; + const { hasError, id, isRequired, setError, setInfo, clearFeedback, setSuccess, setWarning, ...rest } = props; return ( ) => clearFeedback, setSuccess, setWarning, - setHasPassedComplexity, }} > {props.children} diff --git a/packages/clerk-js/src/ui.retheme/primitives/hooks/useFormControl.tsx b/packages/clerk-js/src/ui.retheme/primitives/hooks/useFormControl.tsx index 98f10eea84..12caafe294 100644 --- a/packages/clerk-js/src/ui.retheme/primitives/hooks/useFormControl.tsx +++ b/packages/clerk-js/src/ui.retheme/primitives/hooks/useFormControl.tsx @@ -20,7 +20,6 @@ export type FormControlProps = { setSuccess: (message: string) => void; setWarning: (warning: string) => void; setInfo: (info: string) => void; - setHasPassedComplexity: (b: boolean) => void; clearFeedback: () => void; }; @@ -48,7 +47,6 @@ export const FormControlContextProvider = (props: React.PropsWithChildren{props.children}; }; diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/VerifiedDomainPage.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/VerifiedDomainPage.tsx index 2dc351302b..c6283731d6 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/VerifiedDomainPage.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/VerifiedDomainPage.tsx @@ -263,7 +263,7 @@ export const VerifiedDomainPage = withCardStateProvider(() => { {allowsEdit && ( - + )} diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/VerifyDomainPage.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/VerifyDomainPage.tsx index 5429b09f53..c551ab7143 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/VerifyDomainPage.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/VerifyDomainPage.tsx @@ -151,11 +151,11 @@ export const VerifyDomainPage = withCardStateProvider(() => { > - diff --git a/packages/clerk-js/src/ui/components/SignIn/ResetPassword.tsx b/packages/clerk-js/src/ui/components/SignIn/ResetPassword.tsx index 27b80d34cb..8e6a6f9676 100644 --- a/packages/clerk-js/src/ui/components/SignIn/ResetPassword.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/ResetPassword.tsx @@ -88,7 +88,7 @@ export const _ResetPassword = () => { return console.error(clerkInvalidFAPIResponse(status, supportEmail)); } } catch (e) { - handleError(e, [passwordField, confirmField], card.setError); + return handleError(e, [passwordField, confirmField], card.setError); } }; @@ -117,14 +117,14 @@ export const _ResetPassword = () => { style={{ display: 'none' }} /> - - { if (e.target.value) { @@ -136,7 +136,7 @@ export const _ResetPassword = () => { {!requiresNewPassword && ( - + )} - diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx index ff34cec6a0..d5b7c2a70e 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx @@ -368,7 +368,7 @@ const InstantPasswordRow = ({ field }: { field?: FormControlState<'password'> }) !field.value && !autofilled ? { opacity: 0, height: 0, pointerEvents: 'none', marginTop: '-1rem' } : undefined } > - { )} {shouldShow('password') && ( - { /> {user.passwordEnabled && ( - )} - - { if (e.target.value) { @@ -170,12 +170,15 @@ export const PasswordPage = withCardStateProvider(() => { } return confirmField.props.onChange(e); }} + isRequired isDisabled={passwordEditDisabled} /> - diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordPage.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordPage.test.tsx index c881829a3e..f0a5fbb0f3 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordPage.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordPage.test.tsx @@ -77,7 +77,7 @@ describe('PasswordPage', () => { expect(screen.getByLabelText(/new password/i)).toBeDisabled(); expect(screen.getByLabelText(/confirm password/i)).toBeDisabled(); - expect(screen.getByRole('checkbox', { name: 'Sign out of all other devices' })).toBeDisabled(); + expect(screen.getByRole('checkbox', { name: /sign out of all other devices/i })).toBeDisabled(); expect( screen.getByText( @@ -111,7 +111,7 @@ describe('PasswordPage', () => { expect(screen.getByLabelText(/new password/i)).not.toBeDisabled(); expect(screen.getByLabelText(/confirm password/i)).not.toBeDisabled(); - expect(screen.getByRole('checkbox', { name: 'Sign out of all other devices' })).not.toBeDisabled(); + expect(screen.getByRole('checkbox', { name: /sign out of all other devices/i })).not.toBeDisabled(); expect( screen.queryByText( @@ -147,7 +147,7 @@ describe('PasswordPage', () => { expect(screen.getByLabelText(/current password/i)).toBeDisabled(); expect(screen.getByLabelText(/new password/i)).toBeDisabled(); expect(screen.getByLabelText(/confirm password/i)).toBeDisabled(); - expect(screen.getByRole('checkbox', { name: 'Sign out of all other devices' })).toBeDisabled(); + expect(screen.getByRole('checkbox', { name: /sign out of all other devices/i })).toBeDisabled(); expect( screen.getByText( @@ -183,7 +183,7 @@ describe('PasswordPage', () => { expect(screen.getByLabelText(/current password/i)).not.toBeDisabled(); expect(screen.getByLabelText(/new password/i)).not.toBeDisabled(); expect(screen.getByLabelText(/confirm password/i)).not.toBeDisabled(); - expect(screen.getByRole('checkbox', { name: 'Sign out of all other devices' })).not.toBeDisabled(); + expect(screen.getByRole('checkbox', { name: /sign out of all other devices/i })).not.toBeDisabled(); expect( screen.queryByText( diff --git a/packages/clerk-js/src/ui/elements/FieldControl.tsx b/packages/clerk-js/src/ui/elements/FieldControl.tsx index cebfec3819..8ffecae983 100644 --- a/packages/clerk-js/src/ui/elements/FieldControl.tsx +++ b/packages/clerk-js/src/ui/elements/FieldControl.tsx @@ -22,7 +22,9 @@ import { useFormControlFeedback } from '../utils'; import { useCardState } from './contexts'; import type { FormFeedbackProps } from './FormControl'; import { FormFeedback } from './FormControl'; -import { RadioItem } from './RadioGroup'; +import { InputGroup } from './InputGroup'; +import { PasswordInput } from './PasswordInput'; +import { RadioItem, RadioLabel } from './RadioGroup'; type FormControlProps = Omit, 'label' | 'placeholder' | 'disabled' | 'required'> & ReturnType>['props']; @@ -37,7 +39,6 @@ const Root = (props: PropsWithChildren) => { setInfo, setSuccess, setWarning, - setHasPassedComplexity, clearFeedback, feedbackType, feedback, @@ -63,7 +64,6 @@ const Root = (props: PropsWithChildren) => { setSuccess={setSuccess} setWarning={setWarning} setInfo={setInfo} - setHasPassedComplexity={setHasPassedComplexity} clearFeedback={clearFeedback} sx={sx} > @@ -199,6 +199,66 @@ const FieldFeedback = (props: Pick) => ); }; +const PasswordInputElement = forwardRef((_, ref) => { + const { t } = useLocalizations(); + const formField = useFormField(); + const { placeholder, ...inputProps } = sanitizeInputProps(formField, [ + 'validatePassword', + 'setError', + 'setWarning', + 'setSuccess', + 'setInfo', + 'setHasPassedComplexity', + ]); + + return ( + // @ts-expect-error + + ); +}); + +const CheckboxIndicator = forwardRef((_, ref) => { + const formField = useFormField(); + const { placeholder, ...inputProps } = sanitizeInputProps(formField); + + return ( + ({ + width: 'fit-content', + marginTop: t.space.$0x5, + })} + type='checkbox' + /> + ); +}); + +const CheckboxLabel = (props: { description?: string | LocalizationKey }) => { + const { label, id } = useFormField(); + + if (!label) { + return null; + } + + return ( + + ); +}; + const InputElement = forwardRef((_, ref) => { const { t } = useLocalizations(); const formField = useFormField(); @@ -214,12 +274,40 @@ const InputElement = forwardRef((_, ref) => { ); }); +const InputGroupElement = forwardRef< + HTMLInputElement, + { + groupPrefix?: string; + groupSuffix?: string; + } +>((props, ref) => { + const { t } = useLocalizations(); + const formField = useFormField(); + const { placeholder, ...inputProps } = sanitizeInputProps(formField); + + return ( + + ); +}); + export const Field = { Root: Root, Label: FieldLabel, LabelRow: FieldLabelRow, Input: InputElement, + PasswordInput: PasswordInputElement, + InputGroup: InputGroupElement, RadioItem: RadioItem, + CheckboxIndicator: CheckboxIndicator, + CheckboxLabel: CheckboxLabel, Action: FieldAction, AsOptional: FieldOptionalLabel, LabelIcon: FieldLabelIcon, diff --git a/packages/clerk-js/src/ui/elements/Form.tsx b/packages/clerk-js/src/ui/elements/Form.tsx index aded9dfa07..caa4bf0198 100644 --- a/packages/clerk-js/src/ui/elements/Form.tsx +++ b/packages/clerk-js/src/ui/elements/Form.tsx @@ -156,6 +156,43 @@ const PlainInput = (props: CommonInputProps) => { ); }; +const PasswordInput = (props: CommonInputProps) => { + return ( + + + + ); +}; + +const InputGroup = ( + props: CommonInputProps & { + groupPrefix?: string; + groupSuffix?: string; + }, +) => { + const { groupSuffix, groupPrefix, ...fieldProps } = props; + return ( + + + + ); +}; + +const Checkbox = ( + props: CommonFieldRootProps & { + description?: string | LocalizationKey; + }, +) => { + return ( + + + + + + + ); +}; + const RadioGroup = ( props: Omit, 'infoText' | 'type' | 'validatePassword' | 'label' | 'placeholder'>, ) => { @@ -189,7 +226,10 @@ export const Form = { */ Control: FormControl, PlainInput, + PasswordInput, + InputGroup, RadioGroup, + Checkbox, 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 b075ce8ccd..228e943801 100644 --- a/packages/clerk-js/src/ui/elements/FormControl.tsx +++ b/packages/clerk-js/src/ui/elements/FormControl.tsx @@ -22,15 +22,13 @@ import { } from '../customizables'; import type { ElementDescriptor } from '../customizables/elementDescriptors'; import { usePrefersReducedMotion } from '../hooks'; +import { sanitizeInputProps } from '../primitives/hooks'; import type { PropsOfComponent, ThemableCssProp } from '../styledSystem'; import { animations } from '../styledSystem'; import type { FeedbackType } from '../utils'; import { useFormControlFeedback } from '../utils'; import { useCardState } from './contexts'; -import { InputGroup } from './InputGroup'; -import { PasswordInput } from './PasswordInput'; import { PhoneInput } from './PhoneInput'; -import { RadioGroup } from './RadioGroup'; type FormControlProps = Omit, 'label' | 'placeholder'> & { id: FieldId; @@ -43,45 +41,23 @@ type FormControlProps = Omit, 'label' | 'placehol placeholder?: string | LocalizationKey; actionLabel?: string | LocalizationKey; icon?: React.ComponentType; - validatePassword?: boolean; setError: (error: string | ClerkAPIError | undefined) => void; setWarning: (warning: string) => void; setInfo: (info: string) => void; setSuccess: (message: string) => void; feedback: string; feedbackType: FeedbackType; - setHasPassedComplexity: (b: boolean) => void; clearFeedback: () => void; hasPassedComplexity: boolean; infoText?: string | LocalizationKey; - radioOptions?: { - value: string; - label: string | LocalizationKey; - description?: string | LocalizationKey; - }[]; - groupPreffix?: string; - groupSuffix?: string; }; // TODO: Convert this into a Component? -const getInputElementForType = ({ - type, - groupPreffix, - groupSuffix, -}: { - type: FormControlProps['type']; - groupPreffix: string | undefined; - groupSuffix: string | undefined; -}) => { +const getInputElementForType = ({ type }: { type: FormControlProps['type'] }) => { const CustomInputs = { - password: PasswordInput, tel: PhoneInput, - radio: RadioGroup, }; - if (groupPreffix || groupSuffix) { - return InputGroup; - } if (!type) { return Input; } @@ -283,50 +259,18 @@ export const FormControl = forwardRef { - const propMap = { - password: restInputProps, - radio: { - ...inputProps, - radioOptions, - }, - }; - - if (groupPreffix || groupSuffix) { - return { - ...inputProps, - groupPreffix, - groupSuffix, - }; - } - - if (!props.type) { - return inputProps; - } - const type = props.type as keyof typeof propMap; - return propMap[type] || inputProps; - }, [restInputProps]); - const InputElement = getInputElementForType({ type: props.type, - groupPreffix, - groupSuffix, }); - const isCheckbox = props.type === 'checkbox'; - const { debounced: debouncedState } = useFormControlFeedback({ feedback, feedbackType, isFocused }); const ActionLabel = actionLabel ? ( @@ -400,13 +344,7 @@ export const FormControl = forwardRef { - inputElementProps.onFocus?.(e); - }} - onBlur={e => { - inputElementProps.onBlur?.(e); - }} + {...sanitizedInputProps} ref={ref} placeholder={t(placeholder)} /> @@ -424,49 +362,25 @@ export const FormControl = forwardRef - {isCheckbox ? ( - - {Input} - ({ - marginBottom: isCheckbox ? 0 : theme.space.$1, - marginLeft: !isCheckbox ? 0 : theme.space.$1, - })} - > - {FieldLabel} - {Icon} - {HintLabel} - {ActionLabel} - - - ) : ( - <> - ({ - marginBottom: isCheckbox ? 0 : theme.space.$1, - marginLeft: !isCheckbox ? 0 : theme.space.$1, - })} - > - {FieldLabel} - {Icon} - {HintLabel} - {ActionLabel} - - {Input} - - )} + ({ + marginBottom: theme.space.$1, + marginLeft: 0, + })} + > + {FieldLabel} + {Icon} + {HintLabel} + {ActionLabel} + + {Input} ; export const InputGroup = forwardRef< HTMLInputElement, InputGroupProps & { - groupPreffix?: string; + groupPrefix?: string; groupSuffix?: string; } >((props, ref) => { - const { sx, groupPreffix, groupSuffix, ...rest } = props; + const { sx, groupPrefix, groupSuffix, ...rest } = props; - const inputBorder = groupPreffix + const inputBorder = groupPrefix ? { borderTopLeftRadius: '0', borderBottomLeftRadius: '0', @@ -47,7 +47,7 @@ export const InputGroup = forwardRef< borderColor: theme.colors.$blackAlpha300, // we use this value in the Input primitive })} > - {groupPreffix && {groupPreffix}} + {groupPrefix && {groupPrefix}} & { validatePassword?: boolean; + setError: (error: string | ClerkAPIError | undefined) => void; + setWarning: (warning: string) => void; + setSuccess: (message: string) => void; + setInfo: (info: string) => void; + setHasPassedComplexity: (b: boolean) => void; }; export const PasswordInput = forwardRef((props, ref) => { const [hidden, setHidden] = useState(true); - const { id, onChange: onChangeProp, validatePassword: validatePasswordProp = false, ...rest } = props; + const { + id, + onChange: onChangeProp, + validatePassword: validatePasswordProp = false, + setInfo, + setSuccess, + setWarning, + setError, + setHasPassedComplexity, + ...rest + } = props; const inputRef = useRef(null); const [timeoutState, setTimeoutState] = useState | null>(null); @@ -25,25 +40,23 @@ export const PasswordInput = forwardRef((p userSettings: { passwordSettings }, } = useEnvironment(); - const formControlProps = useFormControl(); const { t } = useLocalizations(); const { validatePassword } = usePassword( { ...passwordSettings, validatePassword: validatePasswordProp }, { - onValidationSuccess: () => - formControlProps?.setSuccess?.(t(localizationKeys('unstable__errors.zxcvbn.goodPassword'))), - onValidationError: message => formControlProps?.setError?.(message), - onValidationWarning: message => formControlProps?.setWarning?.(message), + onValidationSuccess: () => setSuccess(t(localizationKeys('unstable__errors.zxcvbn.goodPassword'))), + onValidationError: message => setError(message), + onValidationWarning: message => setWarning(message), onValidationInfo: message => { if (inputRef.current === document.activeElement) { - formControlProps?.setInfo?.(message); + setInfo(message); } else { // Turn the suggestion into an error if not focused. - formControlProps?.setError?.(message); + setError(message); } }, - onValidationComplexity: hasPassed => formControlProps?.setHasPassedComplexity?.(hasPassed), + onValidationComplexity: hasPassed => setHasPassedComplexity(hasPassed), }, ); @@ -79,7 +92,7 @@ export const PasswordInput = forwardRef((p // Call validate password because to calculate the new feedbackType as the element is now focused validatePassword(e.target.value); }} - //@ts-expect-error + //@ts-expect-error Type mismatch between ForwardRef and RefObject due to null ref={mergeRefs(ref, inputRef)} type={hidden ? 'password' : 'text'} sx={theme => ({ paddingRight: theme.space.$10 })} diff --git a/packages/clerk-js/src/ui/elements/RadioGroup.tsx b/packages/clerk-js/src/ui/elements/RadioGroup.tsx index 2d857c0ce1..0b2d4461a9 100644 --- a/packages/clerk-js/src/ui/elements/RadioGroup.tsx +++ b/packages/clerk-js/src/ui/elements/RadioGroup.tsx @@ -1,98 +1,8 @@ import { forwardRef, useId } from 'react'; import type { LocalizationKey } from '../customizables'; -import { Col, descriptors, Flex, FormLabel, Input, Text } from '../customizables'; +import { descriptors, Flex, FormLabel, Input, Text } from '../customizables'; import { sanitizeInputProps, useFormField } from '../primitives/hooks'; -import type { PropsOfComponent } from '../styledSystem'; - -/** - * @deprecated - */ -export const RadioGroup = ( - props: PropsOfComponent & { - radioOptions?: { - value: string; - label: string | LocalizationKey; - description?: string | LocalizationKey; - }[]; - }, -) => { - const { radioOptions, ...rest } = props; - return ( - - {radioOptions?.map(r => ( - - ))} - - ); -}; - -/** - * @deprecated - */ -const RadioGroupItem = (props: { - inputProps: PropsOfComponent; - value: string; - label: string | LocalizationKey; - description?: string | LocalizationKey; -}) => { - const id = useId(); - return ( - - ({ - width: 'fit-content', - marginTop: t.space.$0x5, - }), - props.inputProps.sx, - ]} - type='radio' - value={props.value} - checked={props.value === props.inputProps.value} - /> - - ({ - padding: `${t.space.$none} ${t.space.$2}`, - display: 'flex', - flexDirection: 'column', - })} - > - - - {props.description && ( - - )} - - - ); -}; const RadioIndicator = forwardRef((props, ref) => { const formField = useFormField(); diff --git a/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx index aafcca099d..dd0d8ea76c 100644 --- a/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx +++ b/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx @@ -29,28 +29,6 @@ const createField = (...params: Parameters) => { }; }; -// TODO: Remove this once FormControl is no longer used -const createFormControl = (...params: Parameters) => { - const MockFieldWrapper = withCardStateProvider((props: Partial[0]>) => { - const field = useFormControl(...params); - - return ( - <> - {/* @ts-ignore*/} - - - - ); - }); - - return { - Field: MockFieldWrapper, - }; -}; - describe('RadioGroup', () => { it('renders the component', async () => { const { wrapper } = await createFixtures(); @@ -204,148 +182,3 @@ describe('RadioGroup', () => { }); }); }); - -/** - * This tests ensure that the deprecated FormControl and RadioGroup continue to behave the same and nothing broke during the refactoring. - */ -describe('Form control as text', () => { - it('renders the component', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('some-radio', '', { - type: 'radio', - radioOptions: [ - { value: 'one', label: 'One' }, - { value: 'two', label: 'Two' }, - ], - }); - - const { getAllByRole } = render(, { wrapper }); - - const radios = getAllByRole('radio'); - expect(radios[0]).toHaveAttribute('value', 'one'); - expect(radios[0].nextSibling).toHaveTextContent('One'); - expect(radios[1]).toHaveAttribute('value', 'two'); - expect(radios[1].nextSibling).toHaveTextContent('Two'); - - radios.forEach(radio => { - expect(radio).not.toBeChecked(); - expect(radio).toHaveAttribute('name', 'some-radio'); - expect(radio).not.toHaveAttribute('required'); - expect(radio).not.toHaveAttribute('disabled'); - }); - - expect(radios[1]).not.toBeChecked(); - }); - - it('renders the component with default value', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('some-radio', 'two', { - type: 'radio', - radioOptions: [ - { value: 'one', label: 'One' }, - { value: 'two', label: 'Two' }, - ], - }); - - const { getAllByRole } = render(, { wrapper }); - - const radios = getAllByRole('radio'); - expect(radios[0]).toHaveAttribute('value', 'one'); - expect(radios[0].nextSibling).toHaveTextContent('One'); - expect(radios[1]).toHaveAttribute('value', 'two'); - expect(radios[1].nextSibling).toHaveTextContent('Two'); - - radios.forEach(radio => { - expect(radio).toHaveAttribute('type', 'radio'); - expect(radio).not.toHaveAttribute('required'); - expect(radio).not.toHaveAttribute('disabled'); - expect(radio).toHaveAttribute('aria-required', 'false'); - expect(radio).toHaveAttribute('aria-disabled', 'false'); - }); - - expect(radios[0]).not.toBeChecked(); - expect(radios[1]).toBeChecked(); - }); - - it('disabled', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('some-radio', 'two', { - type: 'radio', - radioOptions: [ - { value: 'one', label: 'One' }, - { value: 'two', label: 'Two' }, - ], - }); - - const { getAllByRole } = render(, { wrapper }); - - const radios = getAllByRole('radio'); - radios.forEach(radio => { - expect(radio).not.toHaveAttribute('required'); - expect(radio).toHaveAttribute('disabled'); - expect(radio).toHaveAttribute('aria-disabled', 'true'); - }); - }); - - it('required', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('some-radio', 'two', { - type: 'radio', - radioOptions: [ - { value: 'one', label: 'One' }, - { value: 'two', label: 'Two' }, - ], - }); - - const { getAllByRole } = render(, { wrapper }); - - const radios = getAllByRole('radio'); - radios.forEach(radio => { - expect(radio).toHaveAttribute('required'); - expect(radio).toHaveAttribute('aria-required', 'true'); - }); - }); - - it('with error', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('some-radio', 'two', { - type: 'radio', - radioOptions: [ - { value: 'one', label: 'One' }, - { value: 'two', label: 'Two' }, - ], - }); - - const { getAllByRole, getByRole, getByText } = render(, { wrapper }); - - await act(() => userEvent.click(getByRole('button', { name: /set error/i }))); - - await waitFor(() => { - const radios = getAllByRole('radio'); - radios.forEach(radio => { - expect(radio).toHaveAttribute('aria-invalid', 'true'); - expect(radio).toHaveAttribute('aria-describedby', 'error-some-radio'); - }); - expect(getByText('some error')).toBeInTheDocument(); - }); - }); - - it('with info', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('some-radio', '', { - type: 'radio', - radioOptions: [ - { value: 'one', label: 'One' }, - { value: 'two', label: 'Two' }, - ], - infoText: 'some info', - }); - - const { getByLabelText, getByText } = render(, { wrapper }); - - await act(() => fireEvent.focus(getByLabelText('One'))); - await waitFor(() => { - expect(getByText('some info')).toBeInTheDocument(); - }); - }); -}); diff --git a/packages/clerk-js/src/ui/primitives/FormControl.tsx b/packages/clerk-js/src/ui/primitives/FormControl.tsx index 9056e551f8..55dd23794d 100644 --- a/packages/clerk-js/src/ui/primitives/FormControl.tsx +++ b/packages/clerk-js/src/ui/primitives/FormControl.tsx @@ -10,18 +10,7 @@ import { FormControlContextProvider } from './hooks'; * Field.Root is just a Provider */ export const FormControl = (props: React.PropsWithChildren) => { - const { - hasError, - id, - isRequired, - setError, - setInfo, - clearFeedback, - setSuccess, - setWarning, - setHasPassedComplexity, - ...rest - } = props; + const { hasError, id, isRequired, setError, setInfo, clearFeedback, setSuccess, setWarning, ...rest } = props; return ( ) => clearFeedback, setSuccess, setWarning, - setHasPassedComplexity, }} > {props.children} diff --git a/packages/clerk-js/src/ui/primitives/hooks/useFormControl.tsx b/packages/clerk-js/src/ui/primitives/hooks/useFormControl.tsx index 98f10eea84..12caafe294 100644 --- a/packages/clerk-js/src/ui/primitives/hooks/useFormControl.tsx +++ b/packages/clerk-js/src/ui/primitives/hooks/useFormControl.tsx @@ -20,7 +20,6 @@ export type FormControlProps = { setSuccess: (message: string) => void; setWarning: (warning: string) => void; setInfo: (info: string) => void; - setHasPassedComplexity: (b: boolean) => void; clearFeedback: () => void; }; @@ -48,7 +47,6 @@ export const FormControlContextProvider = (props: React.PropsWithChildren{props.children}; };