diff --git a/packages/clerk-js/src/ui.retheme/customizables/index.ts b/packages/clerk-js/src/ui.retheme/customizables/index.ts index 44c9620c0c..e73e91d9e7 100644 --- a/packages/clerk-js/src/ui.retheme/customizables/index.ts +++ b/packages/clerk-js/src/ui.retheme/customizables/index.ts @@ -25,13 +25,11 @@ export const SimpleButton = makeCustomizable(makeLocalizable(sanitizeDomProps(Pr export const Heading = makeCustomizable(makeLocalizable(sanitizeDomProps(Primitives.Heading))); export const Link = makeCustomizable(makeLocalizable(sanitizeDomProps(Primitives.Link))); export const Text = makeCustomizable(makeLocalizable(sanitizeDomProps(Primitives.Text))); - export const Image = makeCustomizable(sanitizeDomProps(makeResponsive(Primitives.Image))); export const Alert = makeCustomizable(sanitizeDomProps(Primitives.Alert)); export const AlertIcon = makeCustomizable(sanitizeDomProps(Primitives.AlertIcon)); export const Input = makeCustomizable(sanitizeDomProps(Primitives.Input)); -export const FormControl = makeCustomizable(sanitizeDomProps(Primitives.FormControl)); export const FormLabel = makeCustomizable(makeLocalizable(sanitizeDomProps(Primitives.FormLabel))); export const FormErrorText = makeCustomizable(makeLocalizable(sanitizeDomProps(Primitives.FormErrorText))); export const FormSuccessText = makeCustomizable(makeLocalizable(sanitizeDomProps(Primitives.FormSuccessText))); diff --git a/packages/clerk-js/src/ui.retheme/elements/FieldControl.tsx b/packages/clerk-js/src/ui.retheme/elements/FieldControl.tsx index 4a3d842879..98a37cb080 100644 --- a/packages/clerk-js/src/ui.retheme/elements/FieldControl.tsx +++ b/packages/clerk-js/src/ui.retheme/elements/FieldControl.tsx @@ -6,7 +6,6 @@ import type { LocalizationKey } from '../customizables'; import { descriptors, Flex, - FormControl as FormControlPrim, FormLabel, Icon as IconCustomizable, Input, @@ -33,44 +32,25 @@ type FormControlProps = Omit, 'label' | 'placehol const Root = (props: PropsWithChildren) => { const card = useCardState(); - const { - id, - isRequired, - sx, - setError, - setInfo, - setSuccess, - setWarning, - clearFeedback, - feedbackType, - feedback, - isFocused, - } = props; + const { children, feedbackType, feedback, isFocused, isDisabled: isDisabledProp, ...restProps } = props; + /** + * Debounce the feedback before passing it inside the provider. + */ const { debounced: debouncedState } = useFormControlFeedback({ feedback, feedbackType, isFocused }); - const isDisabled = props.isDisabled || card.isLoading; + const isDisabled = isDisabledProp || card.isLoading; return ( - - {/*Most of our primitives still depend on this provider.*/} - {/*TODO: In follow-up PRs these will be removed*/} - - {props.children} - + + {children} ); }; @@ -230,7 +210,7 @@ const PasswordInputElement = forwardRef((_, ref) => { ]); return ( - // @ts-expect-error + // @ts-expect-error Typescript is complaining that `setError`, `setWarning` and the rest of feedback setters are not passed. We are clearly passing thing them from above. ) => { - - ); - }); - - return { - Field: MockFieldWrapper, - }; -}; - describe('PlainInput', () => { it('renders the component', async () => { const { wrapper } = await createFixtures(); @@ -181,137 +159,3 @@ describe('PlainInput', () => { }); }); }); - -/** - * This tests ensure that the deprecated FormControl and PlainInput 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('firstname', 'init value', { - type: 'text', - label: 'some label', - placeholder: 'some placeholder', - }); - - const { getByLabelText } = render(, { wrapper }); - expect(getByLabelText('some label')).toHaveValue('init value'); - expect(getByLabelText('some label')).toHaveAttribute('name', 'firstname'); - expect(getByLabelText('some label')).toHaveAttribute('placeholder', 'some placeholder'); - expect(getByLabelText('some label')).toHaveAttribute('type', 'text'); - expect(getByLabelText('some label')).toHaveAttribute('id', 'firstname-field'); - expect(getByLabelText('some label')).not.toHaveAttribute('disabled'); - expect(getByLabelText('some label')).not.toHaveAttribute('required'); - expect(getByLabelText('some label')).toHaveAttribute('aria-invalid', 'false'); - expect(getByLabelText('some label')).toHaveAttribute('aria-describedby', ''); - expect(getByLabelText('some label')).toHaveAttribute('aria-required', 'false'); - expect(getByLabelText('some label')).toHaveAttribute('aria-disabled', 'false'); - }); - - it('disabled', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('firstname', 'init value', { - type: 'text', - label: 'some label', - placeholder: 'some placeholder', - }); - - const { getByLabelText } = render(, { wrapper }); - expect(getByLabelText('some label')).toHaveValue('init value'); - expect(getByLabelText('some label')).toHaveAttribute('disabled'); - expect(getByLabelText('some label')).toHaveAttribute('aria-disabled', 'true'); - }); - - it('required', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('firstname', 'init value', { - type: 'text', - label: 'some label', - placeholder: 'some placeholder', - }); - - const { getByLabelText, queryByText } = render(, { wrapper }); - expect(getByLabelText('some label')).toHaveValue('init value'); - expect(getByLabelText('some label')).toHaveAttribute('required'); - expect(getByLabelText('some label')).toHaveAttribute('aria-required', 'true'); - expect(queryByText(/optional/i)).not.toBeInTheDocument(); - }); - - it('optional', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('firstname', 'init value', { - type: 'text', - label: 'some label', - placeholder: 'some placeholder', - }); - - const { getByLabelText, getByText } = render(, { wrapper }); - expect(getByLabelText('some label')).not.toHaveAttribute('required'); - expect(getByLabelText('some label')).toHaveAttribute('aria-required', 'false'); - expect(getByText(/optional/i)).toBeInTheDocument(); - }); - - it('with icon', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('firstname', 'init value', { - type: 'text', - label: 'some label', - placeholder: 'some placeholder', - }); - - const Icon = () => this is an icon; - - const { getByAltText } = render(, { wrapper }); - expect(getByAltText(/this is an icon/i)).toBeInTheDocument(); - }); - - it('with action label', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('firstname', 'init value', { - type: 'text', - label: 'some label', - placeholder: 'some placeholder', - }); - - const { getByRole } = render(, { wrapper }); - expect(getByRole('link', { name: /take action/i })).toBeInTheDocument(); - expect(getByRole('link', { name: /take action/i })).not.toHaveAttribute('rel'); - expect(getByRole('link', { name: /take action/i })).not.toHaveAttribute('target'); - expect(getByRole('link', { name: /take action/i })).toHaveAttribute('href', ''); - }); - - it('with error', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('firstname', 'init value', { - type: 'text', - label: 'some label', - placeholder: 'some placeholder', - }); - - const { getByRole, getByLabelText, getByText } = render(, { wrapper }); - - await act(() => userEvent.click(getByRole('button', { name: /set error/i }))); - - await waitFor(() => { - expect(getByLabelText('some label')).toHaveAttribute('aria-invalid', 'true'); - expect(getByLabelText('some label')).toHaveAttribute('aria-describedby', 'error-firstname'); - expect(getByText('some error')).toBeInTheDocument(); - }); - }); - - it('with info', async () => { - const { wrapper } = await createFixtures(); - const { Field } = createFormControl('firstname', 'init value', { - type: 'text', - label: 'some label', - placeholder: 'some placeholder', - infoText: 'some info', - }); - - const { getByLabelText, getByText } = render(, { wrapper }); - await act(() => fireEvent.focus(getByLabelText('some label'))); - 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 deleted file mode 100644 index 55dd23794d..0000000000 --- a/packages/clerk-js/src/ui.retheme/primitives/FormControl.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; - -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, id, isRequired, setError, setInfo, clearFeedback, setSuccess, setWarning, ...rest } = props; - return ( - - - {props.children} - - - ); -}; diff --git a/packages/clerk-js/src/ui.retheme/primitives/FormErrorText.tsx b/packages/clerk-js/src/ui.retheme/primitives/FormErrorText.tsx index 69c94a29fe..39a723bc63 100644 --- a/packages/clerk-js/src/ui.retheme/primitives/FormErrorText.tsx +++ b/packages/clerk-js/src/ui.retheme/primitives/FormErrorText.tsx @@ -4,7 +4,7 @@ import { Icon } from '../customizables'; import { ExclamationCircle } from '../icons'; import type { StyleVariants } from '../styledSystem'; import { animations, createVariants } from '../styledSystem'; -import { useFormControl } from './hooks'; +import { useFormField } from './hooks'; import { Text } from './Text'; const { applyVariants } = createVariants(theme => ({ @@ -23,7 +23,7 @@ const { applyVariants } = createVariants(theme => ({ type FormErrorTextProps = React.PropsWithChildren>; export const FormErrorText = forwardRef((props, ref) => { - const { hasError, errorMessageId } = useFormControl() || {}; + const { hasError, errorMessageId } = useFormField() || {}; if (!hasError && !props.children) { return null; @@ -39,7 +39,7 @@ export const FormErrorText = forwardRef((props, aria-live='polite' id={errorMessageId} {...rest} - css={applyVariants(props) as any} + css={applyVariants(props)} > ((props, ref) => { - const { hasError, errorMessageId } = useFormControl() || {}; + const { hasError, errorMessageId } = useFormField() || {}; if (!hasError && !props.children) { return null; @@ -20,7 +20,7 @@ export const FormInfoText = forwardRef((props, ref) aria-live='polite' id={errorMessageId} {...props} - css={applyVariants(props) as any} + css={applyVariants(props)} /> ); }); diff --git a/packages/clerk-js/src/ui.retheme/primitives/FormLabel.tsx b/packages/clerk-js/src/ui.retheme/primitives/FormLabel.tsx index ad0ea381db..90e3f94b1a 100644 --- a/packages/clerk-js/src/ui.retheme/primitives/FormLabel.tsx +++ b/packages/clerk-js/src/ui.retheme/primitives/FormLabel.tsx @@ -3,7 +3,7 @@ import React from 'react'; import type { PrimitiveProps, RequiredProp, StateProps, StyleVariants } from '../styledSystem'; import { common, createVariants } from '../styledSystem'; import { applyDataStateProps } from './applyDataStateProps'; -import { useFormControl } from './hooks'; +import { useFormField } from './hooks'; const { applyVariants } = createVariants(theme => ({ base: { @@ -19,13 +19,13 @@ type OwnProps = React.PropsWithChildren; type FormLabelProps = PrimitiveProps<'label'> & StyleVariants & OwnProps & RequiredProp; export const FormLabel = (props: FormLabelProps) => { - const { id } = useFormControl(); + const { id: fieldHtmlId } = useFormField(); const { isRequired, htmlFor: htmlForProp, ...rest } = props; return ( diff --git a/packages/clerk-js/src/ui.retheme/primitives/Input.tsx b/packages/clerk-js/src/ui.retheme/primitives/Input.tsx index 8b9ae4b49a..f3d76ff158 100644 --- a/packages/clerk-js/src/ui.retheme/primitives/Input.tsx +++ b/packages/clerk-js/src/ui.retheme/primitives/Input.tsx @@ -2,7 +2,7 @@ import React from 'react'; import type { PrimitiveProps, RequiredProp, StyleVariants } from '../styledSystem'; import { common, createVariants, mqu } from '../styledSystem'; -import { useFormControl } from './hooks'; +import { useFormField } from './hooks'; import { useInput } from './hooks/useInput'; const { applyVariants, filterProps } = createVariants((theme, props) => ({ @@ -42,16 +42,16 @@ type OwnProps = { export type InputProps = PrimitiveProps<'input'> & StyleVariants & OwnProps & RequiredProp; export const Input = React.forwardRef((props, ref) => { - const formControlProps = useFormControl() || {}; + const fieldControlProps = useFormField() || {}; const propsWithoutVariants = filterProps({ ...props, - hasError: props.hasError || formControlProps.hasError, + hasError: props.hasError || fieldControlProps.hasError, }); 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; + const _disabled = isDisabled || fieldControlProps.isDisabled; + const _required = isRequired || fieldControlProps.isRequired; + const _hasError = hasError || fieldControlProps.hasError; return ( ((props, ref) onChange={onChange} disabled={isDisabled} required={_required} - id={props.id || formControlProps.id} + id={props.id || fieldControlProps.id} aria-invalid={_hasError} - aria-describedby={formControlProps.errorMessageId} + aria-describedby={fieldControlProps.errorMessageId} aria-required={_required} aria-disabled={_disabled} - css={applyVariants(propsWithoutVariants) as any} + css={applyVariants(propsWithoutVariants)} /> ); }); 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 12caafe294..97de7f0d7f 100644 --- a/packages/clerk-js/src/ui.retheme/primitives/hooks/useFormControl.tsx +++ b/packages/clerk-js/src/ui.retheme/primitives/hooks/useFormControl.tsx @@ -1,99 +1,26 @@ import { createContextAndHook } from '@clerk/shared/react'; -import type { ClerkAPIError, FieldId } from '@clerk/types'; +import type { FieldId } 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). - * - The form element (e.g. Input) gets the `id` - */ - id: string; - isRequired?: boolean; - hasError?: boolean; - isDisabled?: boolean; - setError: (error: string | ClerkAPIError | undefined) => void; - setSuccess: (message: string) => void; - setWarning: (warning: string) => void; - setInfo: (info: string) => void; - 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, - isRequired = false, - hasError = false, - isDisabled = false, - setError, - setSuccess, - setWarning, - setInfo, - clearFeedback, - } = 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 = hasError ? `error-${propsId}` : ''; - const value = React.useMemo( - () => ({ - value: { - isRequired, - hasError, - id, - errorMessageId, - isDisabled, - setError, - setSuccess, - setWarning, - setInfo, - clearFeedback, - }, - }), - [isRequired, hasError, id, errorMessageId, isDisabled, setError, setSuccess, setInfo, setWarning, clearFeedback], - ); - return {props.children}; -}; - type FormFieldProviderProps = ReturnType>['props'] & { - hasError?: boolean; - isDisabled?: boolean; + isDisabled: boolean; }; type FormFieldContextValue = Omit & { errorMessageId?: string; id?: string; fieldId?: FieldId; + hasError: boolean; }; -export const [FormFieldContext, useFormField] = createContextAndHook('FormFieldContext'); +export const [FormFieldContext, , useFormField] = createContextAndHook('FormFieldContext'); export const FormFieldContextProvider = (props: React.PropsWithChildren) => { const { id: propsId, isRequired = false, isDisabled = false, - hasError = false, setError, setSuccess, setWarning, @@ -101,11 +28,14 @@ export const FormFieldContextProvider = (props: React.PropsWithChildren, keep?: (keyof ReturnType)[], diff --git a/packages/clerk-js/src/ui.retheme/primitives/index.ts b/packages/clerk-js/src/ui.retheme/primitives/index.ts index fa81bf923f..445a2872f7 100644 --- a/packages/clerk-js/src/ui.retheme/primitives/index.ts +++ b/packages/clerk-js/src/ui.retheme/primitives/index.ts @@ -11,7 +11,6 @@ export * from './Image'; export * from './Alert'; export * from './AlertIcon'; export * from './Input'; -export * from './FormControl'; export * from './FormErrorText'; export * from './FormInfoText'; export * from './FormSuccessText'; diff --git a/packages/clerk-js/src/ui/primitives/Input.tsx b/packages/clerk-js/src/ui/primitives/Input.tsx index 1c98e803d6..f3d76ff158 100644 --- a/packages/clerk-js/src/ui/primitives/Input.tsx +++ b/packages/clerk-js/src/ui/primitives/Input.tsx @@ -42,7 +42,6 @@ type OwnProps = { export type InputProps = PrimitiveProps<'input'> & StyleVariants & OwnProps & RequiredProp; export const Input = React.forwardRef((props, ref) => { - // const formControlProps = useFormControl() || {}; const fieldControlProps = useFormField() || {}; const propsWithoutVariants = filterProps({ ...props,