From a2822c4eaf1baf7717820f007c4a8f7b61ceacf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Lafont?= Date: Wed, 14 Jun 2023 14:32:29 +0200 Subject: [PATCH 01/11] refactor(form): migrate to react hook form --- packages/form/.jest/helpers.tsx | 2 +- packages/form/package.json | 11 +- .../__stories__/BooleanChecked.stories.tsx | 4 +- .../__stories__/Checked.stories.tsx | 4 +- .../__snapshots__/index.spec.tsx.snap | 26 +- .../CheckboxField/__tests__/index.spec.tsx | 2 +- .../src/components/CheckboxField/index.tsx | 177 +- .../__stories__/Direction.stories.tsx | 6 +- .../__stories__/Required.stories.tsx | 6 +- .../__stories__/Template.stories.tsx | 6 +- .../components/CheckboxGroupField/index.tsx | 110 +- .../MinMaxWithTimeField.stories.tsx | 3 - .../form/src/components/DateField/index.tsx | 185 +- .../Form/__stories__/Playground.stories.tsx | 82 +- .../form/src/components/Form/defaultErrors.ts | 21 + packages/form/src/components/Form/index.tsx | 197 +- .../form/src/components/FormSpy/index.tsx | 24 + .../src/components/NumberInputField/index.tsx | 135 +- .../__stories__/Checked.stories.tsx | 4 +- .../form/src/components/RadioField/index.tsx | 96 +- .../__stories__/Required.stories.tsx | 6 +- .../__stories__/Template.stories.tsx | 6 +- .../src/components/RadioGroupField/index.tsx | 103 +- .../src/components/SelectInputField/index.tsx | 209 +- .../__stories__/Checked.stories.tsx | 4 +- .../components/SelectableCardField/index.tsx | 112 +- packages/form/src/components/Submit/index.tsx | 27 +- .../src/components/SubmitErrorAlert/index.tsx | 26 +- .../src/components/TagInputField/index.tsx | 96 +- .../src/components/TextInputField/index.tsx | 391 +- .../form/src/components/TimeField/index.tsx | 127 +- .../__stories__/ActAsRadio.stories.tsx | 1 - .../__stories__/Checked.stories.tsx | 2 - .../form/src/components/ToggleField/index.tsx | 111 +- .../__stories__/Required.stories.tsx | 4 +- .../__stories__/Template.stories.tsx | 4 +- .../src/components/ToggleGroupField/index.tsx | 114 +- packages/form/src/components/index.ts | 1 + packages/form/src/constants.ts | 1 + packages/form/src/helpers/index.ts | 1 - packages/form/src/helpers/pickValidators.ts | 14 - .../src/hooks/__tests__/useFormField.spec.tsx | 20 - .../hooks/__tests__/useOnFieldChange.spec.tsx | 10 +- .../src/hooks/__tests__/useValidation.spec.ts | 51 - packages/form/src/hooks/index.ts | 6 +- packages/form/src/hooks/useField.ts | 47 + packages/form/src/hooks/useFieldArray.ts | 43 + packages/form/src/hooks/useForm.ts | 24 + packages/form/src/hooks/useFormField.ts | 92 - packages/form/src/hooks/useFormState.ts | 29 + packages/form/src/hooks/useOnFieldChange.ts | 49 +- packages/form/src/hooks/useValidation.ts | 42 - packages/form/src/index.ts | 26 +- packages/form/src/mocks/mockErrors.ts | 32 +- .../form/src/providers/ErrorContext/index.tsx | 81 +- packages/form/src/types.ts | 103 +- .../form/src/validators/__tests__/max.spec.ts | 26 - .../validators/__tests__/maxLength.spec.ts | 26 - .../form/src/validators/__tests__/min.spec.ts | 25 - .../validators/__tests__/minLength.spec.ts | 25 - .../src/validators/__tests__/regex.spec.ts | 30 - .../src/validators/__tests__/required.spec.ts | 24 - packages/form/src/validators/index.ts | 14 +- packages/form/src/validators/max.ts | 6 - packages/form/src/validators/maxDate.ts | 8 +- packages/form/src/validators/maxLength.ts | 6 - packages/form/src/validators/min.ts | 6 - packages/form/src/validators/minDate.ts | 8 +- packages/form/src/validators/minLength.ts | 6 - packages/form/src/validators/regex.ts | 17 - packages/form/src/validators/required.ts | 6 - packages/form/src/validators/types.ts | 5 - .../ThemeGenerator/FormContent/index.tsx | 4 +- .../Tools/ThemeGenerator/index.tsx | 16 +- pnpm-lock.yaml | 4764 +++++++---------- 75 files changed, 3471 insertions(+), 4667 deletions(-) create mode 100644 packages/form/src/components/Form/defaultErrors.ts create mode 100644 packages/form/src/components/FormSpy/index.tsx create mode 100644 packages/form/src/constants.ts delete mode 100644 packages/form/src/helpers/index.ts delete mode 100644 packages/form/src/helpers/pickValidators.ts delete mode 100644 packages/form/src/hooks/__tests__/useFormField.spec.tsx delete mode 100644 packages/form/src/hooks/__tests__/useValidation.spec.ts create mode 100644 packages/form/src/hooks/useField.ts create mode 100644 packages/form/src/hooks/useFieldArray.ts create mode 100644 packages/form/src/hooks/useForm.ts delete mode 100644 packages/form/src/hooks/useFormField.ts create mode 100644 packages/form/src/hooks/useFormState.ts delete mode 100644 packages/form/src/hooks/useValidation.ts delete mode 100644 packages/form/src/validators/__tests__/max.spec.ts delete mode 100644 packages/form/src/validators/__tests__/maxLength.spec.ts delete mode 100644 packages/form/src/validators/__tests__/min.spec.ts delete mode 100644 packages/form/src/validators/__tests__/minLength.spec.ts delete mode 100644 packages/form/src/validators/__tests__/regex.spec.ts delete mode 100644 packages/form/src/validators/__tests__/required.spec.ts delete mode 100644 packages/form/src/validators/max.ts delete mode 100644 packages/form/src/validators/maxLength.ts delete mode 100644 packages/form/src/validators/min.ts delete mode 100644 packages/form/src/validators/minLength.ts delete mode 100644 packages/form/src/validators/regex.ts delete mode 100644 packages/form/src/validators/required.ts delete mode 100644 packages/form/src/validators/types.ts diff --git a/packages/form/.jest/helpers.tsx b/packages/form/.jest/helpers.tsx index 6a2ebebcd9..9a4e96ebe8 100644 --- a/packages/form/.jest/helpers.tsx +++ b/packages/form/.jest/helpers.tsx @@ -34,7 +34,7 @@ export const shouldMatchEmotionSnapshotFormWrapper = ( errors={mockErrors} initialValues={initialValues} > - {() => children} + {children} , options, ) diff --git a/packages/form/package.json b/packages/form/package.json index 703c8e62db..33f4fc96b9 100644 --- a/packages/form/package.json +++ b/packages/form/package.json @@ -29,6 +29,10 @@ "linux" ], "sideEffects": false, + "main": "dist/index.js", + "module": "dist/index.js", + "jsnext:main": "dist/index.js", + "types": "dist/index.d.ts", "type": "module", "exports": { "types": "./dist/index.d.ts", @@ -54,10 +58,7 @@ "@emotion/react": "11.11.1", "@emotion/styled": "11.11.0", "@ultraviolet/ui": "workspace:*", - "final-form": "4.20.10", - "final-form-arrays": "3.0.2", - "react-final-form": "6.5.9", - "react-final-form-arrays": "3.1.4", - "react-select": "5.7.7" + "react-hook-form": "^7.47.0", + "react-select": "5.7.5" } } diff --git a/packages/form/src/components/CheckboxField/__stories__/BooleanChecked.stories.tsx b/packages/form/src/components/CheckboxField/__stories__/BooleanChecked.stories.tsx index 004e129950..0b2a617101 100644 --- a/packages/form/src/components/CheckboxField/__stories__/BooleanChecked.stories.tsx +++ b/packages/form/src/components/CheckboxField/__stories__/BooleanChecked.stories.tsx @@ -1,9 +1,9 @@ import type { StoryFn } from '@storybook/react' +import type { FormErrors } from '../../../types' import { CheckboxField } from '..' -import type { FormProps } from '../../Form' import { Form } from '../../Form' -export const BooleanChecked: StoryFn = ({ errors }) => ( +export const BooleanChecked: StoryFn<{ errors: FormErrors }> = ({ errors }) => (
{}} errors={errors} initialValues={{ foo: true }}> Default Checked Boolean Item
diff --git a/packages/form/src/components/CheckboxField/__stories__/Checked.stories.tsx b/packages/form/src/components/CheckboxField/__stories__/Checked.stories.tsx index 8399cf6999..6c82ff9309 100644 --- a/packages/form/src/components/CheckboxField/__stories__/Checked.stories.tsx +++ b/packages/form/src/components/CheckboxField/__stories__/Checked.stories.tsx @@ -1,9 +1,9 @@ import type { StoryFn } from '@storybook/react' +import type { FormErrors } from '../../../types' import { CheckboxField } from '..' -import type { FormProps } from '../..' import { Form } from '../../Form' -export const Checked: StoryFn = ({ errors }) => ( +export const Checked: StoryFn<{ errors: FormErrors }> = ({ errors }) => (
{}} errors={errors} initialValues={{ foo: ['bar'] }}> Checked Item diff --git a/packages/form/src/components/CheckboxField/__tests__/__snapshots__/index.spec.tsx.snap b/packages/form/src/components/CheckboxField/__tests__/__snapshots__/index.spec.tsx.snap index 4375b93091..d2ebe223ee 100644 --- a/packages/form/src/components/CheckboxField/__tests__/__snapshots__/index.spec.tsx.snap +++ b/packages/form/src/components/CheckboxField/__tests__/__snapshots__/index.spec.tsx.snap @@ -260,9 +260,7 @@ exports[`CheckboxField should render correctly 1`] = ` justify-content: normal; } - +
+
+
+
+
{ // to trigger error await userEvent.click(screen.getByRole('checkbox', { hidden: true })) await userEvent.click(screen.getByText('Focus')) - const error = screen.getByText(mockErrors.REQUIRED as string) + const error = screen.getByText(mockErrors.required({ label: '' })) expect(error).toBeVisible() }, }, diff --git a/packages/form/src/components/CheckboxField/index.tsx b/packages/form/src/components/CheckboxField/index.tsx index f9718346ca..e18b17805a 100644 --- a/packages/form/src/components/CheckboxField/index.tsx +++ b/packages/form/src/components/CheckboxField/index.tsx @@ -1,105 +1,84 @@ import { Checkbox } from '@ultraviolet/ui' -import type { FieldState } from 'final-form' -import type { ComponentProps, JSX, ReactNode, Ref } from 'react' -import { forwardRef } from 'react' -import { useFormField } from '../../hooks' +import type { ComponentProps, ReactNode } from 'react' +import type { FieldValues } from 'react-hook-form' +import { Controller } from 'react-hook-form' import { useErrors } from '../../providers' import type { BaseFieldProps } from '../../types' -type CheckboxValue = string - -type CheckboxFieldProps = BaseFieldProps & - Partial< - Pick< - ComponentProps, - | 'disabled' - | 'onBlur' - | 'onChange' - | 'onFocus' - | 'progress' - | 'size' - | 'value' - | 'data-testid' - | 'helper' - | 'tooltip' - > - > & { - name: string - label?: string - className?: string - children?: ReactNode - required?: boolean - } - -export const CheckboxField = forwardRef( - ( - { - validate, - name, - label = '', - size, - progress, - disabled, - required, - className, - children, - onChange, - onBlur, - onFocus, - value, - helper, - tooltip, - 'data-testid': dataTestId, - }: CheckboxFieldProps, - ref: Ref, - ): JSX.Element => { - const { getError } = useErrors() - - const { input, meta } = useFormField(name, { - disabled, - required, - type: 'checkbox', - validate, - value, - }) +type CheckboxFieldProps = + BaseFieldProps & + Partial< + Pick< + ComponentProps, + | 'disabled' + | 'onBlur' + | 'onChange' + | 'onFocus' + | 'progress' + | 'size' + | 'value' + | 'data-testid' + | 'helper' + | 'tooltip' + > + > & { + label?: string + className?: string + children?: ReactNode + required?: boolean + } - const error = getError({ - label, - meta: meta as FieldState, - name, - value: input.value ?? input.checked, - }) +export const CheckboxField = ({ + name, + label, + size, + progress, + disabled, + required, + className, + children, + onChange, + onBlur, + onFocus, + rules, + helper, + tooltip, + 'data-testid': dataTestId, +}: CheckboxFieldProps) => { + const { getError } = useErrors() - return ( - { - input.onChange(event) - onChange?.(event) - }} - onBlur={event => { - input.onBlur(event) - onBlur?.(event) - }} - onFocus={event => { - input.onFocus(event) - onFocus?.(event) - }} - size={size} - progress={progress} - disabled={disabled} - checked={input.checked} - error={error} - helper={helper} - ref={ref} - className={className} - value={input.value} - required={required} - data-testid={dataTestId} - tooltip={tooltip} - > - {children} - - ) - }, -) + return ( + ( + { + field.onChange(event) + onChange?.(event) + }} + onBlur={event => { + field.onBlur() + onBlur?.(event) + }} + onFocus={onFocus} + size={size} + progress={progress} + disabled={disabled} + checked={!!field.value} + error={getError({ label: label ?? '' }, error)} + ref={field.ref} + className={className} + value={field.value} + required={required} + data-testid={dataTestId} + helper={helper} + tooltip={tooltip} + > + {children} + + )} + /> + ) +} diff --git a/packages/form/src/components/CheckboxGroupField/__stories__/Direction.stories.tsx b/packages/form/src/components/CheckboxGroupField/__stories__/Direction.stories.tsx index d7131cc553..f10edc7df9 100644 --- a/packages/form/src/components/CheckboxGroupField/__stories__/Direction.stories.tsx +++ b/packages/form/src/components/CheckboxGroupField/__stories__/Direction.stories.tsx @@ -1,10 +1,10 @@ import type { StoryFn } from '@storybook/react' import { Stack } from '@ultraviolet/ui' -import { useFormState } from 'react-final-form' +import { useFormContext } from 'react-hook-form' import { CheckboxGroupField } from '..' export const DirectionStory: StoryFn = args => { - const { values } = useFormState() + const { watch } = useFormContext() return ( @@ -20,7 +20,7 @@ export const DirectionStory: StoryFn = args => { - Form content: {JSON.stringify(values)} + Form content: {JSON.stringify(watch())} ) diff --git a/packages/form/src/components/CheckboxGroupField/__stories__/Required.stories.tsx b/packages/form/src/components/CheckboxGroupField/__stories__/Required.stories.tsx index ce04ca7c16..dbfaa688e6 100644 --- a/packages/form/src/components/CheckboxGroupField/__stories__/Required.stories.tsx +++ b/packages/form/src/components/CheckboxGroupField/__stories__/Required.stories.tsx @@ -1,11 +1,11 @@ import type { StoryFn } from '@storybook/react' import { Stack } from '@ultraviolet/ui' -import { useFormState } from 'react-final-form' +import { useFormContext } from 'react-hook-form' import { CheckboxGroupField } from '..' import { Submit } from '../..' export const RequiredStory: StoryFn = args => { - const { values } = useFormState() + const { watch } = useFormContext() return ( @@ -22,7 +22,7 @@ export const RequiredStory: StoryFn = args => { Submit - Form content: {JSON.stringify(values)} + Form content: {JSON.stringify(watch())} ) diff --git a/packages/form/src/components/CheckboxGroupField/__stories__/Template.stories.tsx b/packages/form/src/components/CheckboxGroupField/__stories__/Template.stories.tsx index c46bc392f1..df97cfc4a5 100644 --- a/packages/form/src/components/CheckboxGroupField/__stories__/Template.stories.tsx +++ b/packages/form/src/components/CheckboxGroupField/__stories__/Template.stories.tsx @@ -1,10 +1,10 @@ import type { StoryFn } from '@storybook/react' import { Stack } from '@ultraviolet/ui' -import { useFormState } from 'react-final-form' +import { useFormContext } from 'react-hook-form' import { CheckboxGroupField } from '..' const CheckboxGroupFieldStory: StoryFn = args => { - const { values } = useFormState() + const { watch } = useFormContext() return ( @@ -20,7 +20,7 @@ const CheckboxGroupFieldStory: StoryFn = args => { - Form content: {JSON.stringify(values)} + Form content: {JSON.stringify(watch())} ) diff --git a/packages/form/src/components/CheckboxGroupField/index.tsx b/packages/form/src/components/CheckboxGroupField/index.tsx index 98ea121d43..f08e37fdb4 100644 --- a/packages/form/src/components/CheckboxGroupField/index.tsx +++ b/packages/form/src/components/CheckboxGroupField/index.tsx @@ -1,32 +1,29 @@ import { CheckboxGroup } from '@ultraviolet/ui' import type { ComponentProps } from 'react' -import { useFieldArray } from 'react-final-form-arrays' +import type { FieldValues } from 'react-hook-form' +import { Controller } from 'react-hook-form' import { useErrors } from '../../providers' import type { BaseFieldProps } from '../../types' -type CheckboxGroupValue = string[] +type CheckboxGroupFieldProps = + BaseFieldProps & + Partial< + Pick< + ComponentProps, + | 'className' + | 'helper' + | 'onChange' + | 'required' + | 'direction' + | 'children' + | 'value' + | 'error' + | 'legend' + > + > & + Required, 'legend' | 'name'>> -type CheckboxGroupFieldProps< - T = CheckboxGroupValue, - K = string, -> = BaseFieldProps & - Partial< - Pick< - ComponentProps, - | 'className' - | 'helper' - | 'onChange' - | 'required' - | 'direction' - | 'children' - | 'value' - | 'error' - | 'legend' - > - > & - Required, 'legend' | 'name'>> - -export const CheckboxGroupField = ({ +export const CheckboxGroupField = ({ legend, value, className, @@ -34,48 +31,45 @@ export const CheckboxGroupField = ({ direction, children, onChange, + label = '', error: customError, name, required = false, -}: CheckboxGroupFieldProps) => { +}: CheckboxGroupFieldProps) => { const { getError } = useErrors() - const { fields, meta } = useFieldArray(name, { - type: 'checkbox', - value, - validate: localValue => - required && localValue?.length === 0 ? 'Required' : undefined, - }) - - const error = getError({ - label: legend, - meta, - value: fields.value, - name, - }) - return ( - { - if (fields.value?.includes(event.currentTarget.value)) { - fields.remove(fields.value.indexOf(event.currentTarget?.value)) - } else { - fields.push(event.currentTarget.value) - } + ( + { + const fieldValue = field.value as string[] + if (fieldValue?.includes(event.currentTarget.value)) { + field.onChange( + fieldValue?.filter( + currentValue => currentValue === event.currentTarget.value, + ), + ) + } else { + field.onChange([...field.value, event.currentTarget.value]) + } - onChange?.(event) - }} - error={error ?? customError} - className={className} - direction={direction} - helper={helper} - required={required} - > - {children} - + onChange?.(event) + }} + error={getError({ label }, error) ?? customError} + className={className} + direction={direction} + helper={helper} + required={required} + > + {children} + + )} + /> ) } diff --git a/packages/form/src/components/DateField/__stories__/MinMaxWithTimeField.stories.tsx b/packages/form/src/components/DateField/__stories__/MinMaxWithTimeField.stories.tsx index 2293e2cc6c..909c138d0f 100644 --- a/packages/form/src/components/DateField/__stories__/MinMaxWithTimeField.stories.tsx +++ b/packages/form/src/components/DateField/__stories__/MinMaxWithTimeField.stories.tsx @@ -5,8 +5,6 @@ import { DateField } from '..' import { Submit } from '../../Submit' import { TimeField } from '../../TimeField' -const now = new Date() - export const MinMaxDateWithTimeField: StoryFn< ComponentProps > = ({ name, minDate, maxDate, required }) => ( @@ -14,7 +12,6 @@ export const MinMaxDateWithTimeField: StoryFn< diff --git a/packages/form/src/components/DateField/index.tsx b/packages/form/src/components/DateField/index.tsx index 86781ce3c9..530ea2c5ff 100644 --- a/packages/form/src/components/DateField/index.tsx +++ b/packages/form/src/components/DateField/index.tsx @@ -1,39 +1,42 @@ import { DateInput } from '@ultraviolet/ui' -import type { FieldState } from 'final-form' import type { ComponentProps, FocusEvent } from 'react' -import { useFormField } from '../../hooks' +import type { FieldValues } from 'react-hook-form' +import { Controller } from 'react-hook-form' import { useErrors } from '../../providers' import type { BaseFieldProps } from '../../types' +import { maxDateValidator } from '../../validators/maxDate' +import { minDateValidator } from '../../validators/minDate' type DateExtends = Date | [Date | null, Date | null] -type DateFieldProps = BaseFieldProps & - Omit< - ComponentProps, - | 'maxDate' - | 'minDate' - | 'disabled' - | 'required' - | 'locale' - | 'name' - | 'onChange' - | 'onFocus' - | 'onBlur' - | 'autoFocus' - | 'startDate' - | 'endDate' - > & { - name: string - maxDate?: Date - minDate?: Date - disabled?: boolean - required?: boolean - locale?: string - onChange?: (value: Date | null) => void - onBlur?: (event: FocusEvent) => void - onFocus?: (value: FocusEvent) => void - autoFocus?: boolean - } +type DateFieldProps = + BaseFieldProps & + Omit< + ComponentProps, + | 'maxDate' + | 'minDate' + | 'disabled' + | 'required' + | 'locale' + | 'name' + | 'onChange' + | 'onFocus' + | 'onBlur' + | 'autoFocus' + | 'stardDate' + | 'endDate' + > & { + name: string + maxDate?: Date + minDate?: Date + disabled?: boolean + required?: boolean + locale?: string + onChange?: (value: Date | null) => void + onBlur?: (event: FocusEvent) => void + onFocus?: (value: FocusEvent) => void + autoFocus?: boolean + } const parseDate = (value: string | Date): Date => typeof value === 'string' ? new Date(value) : value @@ -41,86 +44,78 @@ const parseDate = (value: string | Date): Date => const isEmpty = (value?: Date | string): boolean => typeof value === 'string' ? value === '' : value === undefined -export const DateField = ({ +export const DateField = ({ required, name, label = '', - validate, + // validate, format, locale, maxDate, minDate, - initialValue, + // initialValue, disabled, - value: inputVal, + // value: inputVal, onChange, onBlur, onFocus, - formatOnBlur, + // formatOnBlur, + rules, autoFocus = false, excludeDates, selectsRange, 'data-testid': dataTestId, -}: DateFieldProps) => { +}: DateFieldProps) => { const { getError } = useErrors() - const { input, meta } = useFormField(name, { - disabled, - formatOnBlur, - initialValue, - maxDate, - minDate, - required, - validate, - value: inputVal, - }) - - const error = getError({ - label, - maxDate, - meta: meta as FieldState, - minDate, - name, - value: input.value, - }) - return ( - (value ? parseDate(value).toLocaleDateString() : '')) - } - locale={locale} - required={required} - value={input.value as Date} - onChange={(val: DateExtends | null) => { - if (val && val instanceof Date) { - onChange?.(val) - const newDate = parseDate(val) - if (isEmpty(input.value as Date)) { - input.onChange(newDate) - - return - } - const currentDate = parseDate(input.value as Date) - newDate.setHours(currentDate.getHours(), currentDate.getMinutes()) - input.onChange(newDate) - } else if (Array.isArray(val)) { - input.onChange(val) - } - }} - onBlur={(e: FocusEvent) => { - input.onBlur(e) - onBlur?.(e) - }} - onFocus={(e: FocusEvent) => { - input.onFocus(e) - onFocus?.(e) + ( + (value ? parseDate(value).toLocaleDateString() : '')) + } + locale={locale} + required={required} + onChange={(val: DateExtends | null) => { + if (val) { + onChange?.(val) + const newDate = parseDate(val) + if (isEmpty(field.value as Date)) { + field.onChange(newDate) + + return + } + const currentDate = parseDate(field.value as Date) + newDate.setHours(currentDate.getHours(), currentDate.getMinutes()) + field.onChange(newDate) + } + }} + onBlur={(e: FocusEvent) => { + field.onBlur() + onBlur?.(e) + }} + onFocus={onFocus} + maxDate={maxDate} + minDate={minDate} + error={getError({ minDate, maxDate, label }, error)} + disabled={disabled} + autoFocus={autoFocus} + excludeDates={excludeDates} + data-testid={dataTestId} + startDate={ selectsRange ? (input.value as [Date | null, Date | null])[0] : undefined @@ -130,13 +125,9 @@ export const DateField = ({ ? (input.value as [Date | null, Date | null])[1] : undefined } - error={error} - disabled={disabled} - autoFocus={autoFocus} - name={input.name} - data-testid={dataTestId} - excludeDates={excludeDates} - selectsRange={selectsRange} + /> + )} +>>>>>>> 3bfea1c6 (refactor(form): migrate to react hook form) /> ) } diff --git a/packages/form/src/components/Form/__stories__/Playground.stories.tsx b/packages/form/src/components/Form/__stories__/Playground.stories.tsx index b289aa107a..787f7f3b47 100644 --- a/packages/form/src/components/Form/__stories__/Playground.stories.tsx +++ b/packages/form/src/components/Form/__stories__/Playground.stories.tsx @@ -1,11 +1,11 @@ import type { StoryFn } from '@storybook/react' -import { Checkbox, Stack } from '@ultraviolet/ui' -import type { ChangeEvent } from 'react' -import { useState } from 'react' +import { Stack } from '@ultraviolet/ui' +import { useForm } from 'react-hook-form' import { CheckboxField, DateField, Form, + NumberInputField, RadioField, SelectInputField, SelectableCardField, @@ -18,20 +18,49 @@ import { } from '../..' import { emailRegex, mockErrors } from '../../../mocks/mockErrors' -export const Playground: StoryFn = args => { - const [state, setState] = useState(false) +type FormValues = { + receiveEmailUpdates: boolean + choice: string + tags: string[] + selectableCard: string + disableName: boolean + email: string +} + +export const Playground: StoryFn = () => { + const methods = useForm({ + mode: 'onChange', + defaultValues: { + receiveEmailUpdates: true, + choice: '2', + tags: ['cloud', 'of', 'choice'], + selectableCard: '1', + disableName: false, + email: 'email', + }, + }) + + const disableName = methods.watch('disableName') + + console.log({ disableName }) return ( - + + errors={mockErrors} + methods={methods} + onRawSubmit={() => + new Promise(rejects => { + setTimeout( + () => rejects({ 'FINAL_FORM/form-error': 'SERVER ERROR' }), + 5000, + ) + }) + } + > - ) => - setState(event.target.checked) - } - > + I'm disabling the field name to remove validation - + @@ -59,17 +88,23 @@ export const Playground: StoryFn = args => { name="name" label="Name" placeholder="John" - required autoComplete="given-name" - disabled={state} + required={!disableName} + disabled={disableName} /> + @@ -95,14 +130,15 @@ export const Playground: StoryFn = args => { } Playground.args = { - errors: mockErrors, - initialValues: { - receiveEmailUpdates: true, - choice: '2', - tags: ['cloud', 'of', 'choice'], - selectableCard: '1', - }, onRawSubmit: values => { console.log('Submit', values) }, + // initialValues: { + // receiveEmailUpdates: true, + // choice: '2', + // tags: ['cloud', 'of', 'choice'], + // selectableCard: '1', + // disableName: false, + // email: 'email' + // } } diff --git a/packages/form/src/components/Form/defaultErrors.ts b/packages/form/src/components/Form/defaultErrors.ts new file mode 100644 index 0000000000..525a617785 --- /dev/null +++ b/packages/form/src/components/Form/defaultErrors.ts @@ -0,0 +1,21 @@ +import type { RequiredErrors } from '../../types' + +export const defaultErrors: RequiredErrors = { + maxDate: () => '', + maxLength: () => '', + minLength: () => '', + max: () => '', + min: () => '', + required: () => '', + pattern: () => '', + deps: () => '', + value: () => '', + onBlur: () => '', + disabled: () => '', + onChange: () => '', + validate: () => '', + setValueAs: () => '', + valueAsDate: () => '', + valueAsNumber: () => '', + shouldUnregister: () => '', +} diff --git a/packages/form/src/components/Form/index.tsx b/packages/form/src/components/Form/index.tsx index b46361289c..b5de6556bf 100644 --- a/packages/form/src/components/Form/index.tsx +++ b/packages/form/src/components/Form/index.tsx @@ -1,75 +1,146 @@ -import arrayMutators from 'final-form-arrays' -import type { JSX, ReactNode } from 'react' +import type { ReactNode } from 'react' +import React, { useMemo } from 'react' import type { - FormRenderProps, - FormProps as ReactFinalFormProps, -} from 'react-final-form' -import { Form as ReactFinalForm } from 'react-final-form' + DefaultValues, + FieldErrors, + FieldPath, + FieldValues, + UseFormHandleSubmit, + UseFormReturn, +} from 'react-hook-form' +import { FormProvider, useForm } from 'react-hook-form' +import { FORM_ERROR } from '../../constants' import { ErrorProvider } from '../../providers' import type { FormErrors } from '../../types' +import { defaultErrors } from './defaultErrors' -export type FormProps = { - children?: - | ((props: FormRenderProps>) => ReactNode) - | ReactNode +type Without = { [P in Exclude]?: never } +type SingleXOR = T | U extends object + ? (Without & U) | (Without & T) + : T | U + +export type XOR = T extends [infer Only] + ? Only + : T extends [infer A, infer B, ...infer Rest] + ? XOR<[SingleXOR, ...Rest]> + : never + +type FormStateReturn = { + values: TFormValues + hasValidationErrors: boolean + errors: FieldErrors + submitting: boolean + pristine: boolean + handleSubmit: () => Promise + submitError: boolean + valid: boolean + form: { + change: UseFormReturn['setValue'] + reset: UseFormReturn['reset'] + submit: () => Promise + } +} + +type OnRawSubmitReturn = + | { [FORM_ERROR]?: string } + | undefined + | null + | string + | void + | boolean + | number + +type FormSubmitContextValue = { + handleSubmit: ReturnType> +} + +export const FormSubmitContext = React.createContext( + {} as FormSubmitContextValue, +) + +export type FormProps = { + children?: ((props: FormStateReturn) => ReactNode) | ReactNode errors: FormErrors - /** - * onRawSubmit is the base onSubmit from final-form - */ - onRawSubmit: ReactFinalFormProps>['onSubmit'] - initialValues?: Partial - validateOnBlur?: ReactFinalFormProps< - FormValues, - Partial - >['validateOnBlur'] - validate?: ReactFinalFormProps>['validate'] - /** - * The form name attribute - */ name?: string - render?: ReactFinalFormProps>['render'] - mutators?: ReactFinalFormProps>['mutators'] - keepDirtyOnReinitialize?: boolean - className?: string -} + validate?: ( + values: TFormValues, + ) => Partial, string>> + onRawSubmit: ( + data: TFormValues, + formState: { + reset: UseFormReturn['reset'] + resetFieldState: UseFormReturn['resetField'] + restart: () => void + change: UseFormReturn['setValue'] + }, + ) => Promise | OnRawSubmitReturn +} & XOR< + [ + { initialValues?: DefaultValues }, + { methods: UseFormReturn }, + ] +> -export const Form = ({ +export const Form = ({ children, - onRawSubmit, - errors, + methods: methodsProp, initialValues, - validateOnBlur, - validate, + errors, + onRawSubmit, name, - render, - mutators, - keepDirtyOnReinitialize, - className, -}: FormProps): JSX.Element => ( - ( - - - {typeof children === 'function' ? children(renderProps) : children} +}: FormProps) => { + const methodsHook = useForm({ + defaultValues: initialValues, + mode: 'onChange', + }) + + const methods = !methodsProp ? methodsHook : methodsProp + + const handleSubmit = methods.handleSubmit(async values => { + const result = await onRawSubmit(values, { + reset: methods.reset, + resetFieldState: methods.resetField, + restart: () => methods.reset(initialValues), + change: methods.setValue, + }) + if (result && typeof result !== 'boolean' && typeof result !== 'number') { + methods.setError('root.submit', { + type: 'custom', + message: typeof result === 'object' ? result[FORM_ERROR] : result, + }) + } + }) + + const formSubmitContextValue = useMemo( + () => ({ handleSubmit }), + [handleSubmit], + ) + + return ( + + + + + {typeof children === 'function' + ? children({ + values: methods.watch(), + hasValidationErrors: !methods.formState.isValid, + errors: methods.formState.errors, + submitting: methods.formState.isSubmitting, + pristine: !methods.formState.isDirty, + handleSubmit, + submitError: !!methods.formState.errors?.root?.['submit'], + valid: methods.formState.isValid, + form: { + change: methods.setValue, + reset: methods.reset, + submit: handleSubmit, + }, + }) + : children} - )) - } - keepDirtyOnReinitialize={keepDirtyOnReinitialize} - /> -) + + + ) +} diff --git a/packages/form/src/components/FormSpy/index.tsx b/packages/form/src/components/FormSpy/index.tsx new file mode 100644 index 0000000000..cc0e528759 --- /dev/null +++ b/packages/form/src/components/FormSpy/index.tsx @@ -0,0 +1,24 @@ +import { useEffect } from 'react' +import type { DeepPartial, FieldValues } from 'react-hook-form' +import { useForm } from 'react-hook-form' + +type FormSpyProps = { + onChange?: (values: { values: DeepPartial }) => void +} + +/** + * @deprecated + */ +export const FormSpy = ({ + onChange, +}: FormSpyProps) => { + const { watch } = useForm() + + useEffect(() => { + const subscription = watch(values => onChange?.({ values })) + + return () => subscription.unsubscribe() + }, [watch, onChange]) + + return null +} diff --git a/packages/form/src/components/NumberInputField/index.tsx b/packages/form/src/components/NumberInputField/index.tsx index 4c404f970a..24180b6462 100644 --- a/packages/form/src/components/NumberInputField/index.tsx +++ b/packages/form/src/components/NumberInputField/index.tsx @@ -1,38 +1,32 @@ import { NumberInput } from '@ultraviolet/ui' import type { ComponentProps, FocusEvent, FocusEventHandler } from 'react' -import { useFormField } from '../../hooks' +import type { FieldValues } from 'react-hook-form' +import { Controller } from 'react-hook-form' import type { BaseFieldProps } from '../../types' -type NumberInputValue = NonNullable['value']> +type NumberInputValueFieldProps = + BaseFieldProps & + Partial< + Pick< + ComponentProps, + | 'disabled' + | 'maxValue' + | 'minValue' + | 'onMaxCrossed' + | 'onMinCrossed' + | 'size' + | 'step' + | 'text' + | 'value' + | 'onChange' + | 'className' + > + > & { + onBlur?: FocusEventHandler + onFocus?: FocusEventHandler + } -type NumberInputValueFieldProps< - T = NumberInputValue, - K = string, -> = BaseFieldProps & - Partial< - Pick< - ComponentProps, - | 'disabled' - | 'maxValue' - | 'minValue' - | 'onMaxCrossed' - | 'onMinCrossed' - | 'size' - | 'step' - | 'text' - | 'value' - | 'onChange' - | 'className' - | 'data-testid' - > - > & { - name: string - required?: boolean - onBlur?: FocusEventHandler - onFocus?: FocusEventHandler - } - -export const NumberInputField = ({ +export const NumberInputField = ({ disabled, maxValue, minValue, @@ -46,48 +40,41 @@ export const NumberInputField = ({ size, step, text, - validate, - value, + // validate, + // value, + rules, className, - 'data-testid': dataTestId, -}: NumberInputValueFieldProps) => { - const { input } = useFormField(name, { - disabled, - required, - type: 'number', - validate, - value, - defaultValue: 0, - max: maxValue, - min: minValue, - }) - - return ( - ) => { - input.onBlur(event) - onBlur?.(event) - }} - onChange={event => { - input.onChange(event) - onChange?.(event) - }} - onFocus={(event: FocusEvent) => { - input.onFocus(event) - onFocus?.(event) - }} - maxValue={maxValue} - minValue={minValue} - onMinCrossed={onMinCrossed} - onMaxCrossed={onMaxCrossed} - size={size} - step={step} - text={text} - value={input.value} - className={className} - data-testid={dataTestId} - /> - ) -} +}: NumberInputValueFieldProps) => ( + ( + ) => { + field.onBlur() + onBlur?.(event) + }} + onChange={event => { + field.onChange(event) + onChange?.(event as number) + }} + onFocus={onFocus} + maxValue={maxValue} + minValue={minValue} + onMinCrossed={onMinCrossed} + onMaxCrossed={onMaxCrossed} + size={size} + step={step} + text={text} + className={className} + /> + )} + /> +) diff --git a/packages/form/src/components/RadioField/__stories__/Checked.stories.tsx b/packages/form/src/components/RadioField/__stories__/Checked.stories.tsx index ba918f3260..b006696e6b 100644 --- a/packages/form/src/components/RadioField/__stories__/Checked.stories.tsx +++ b/packages/form/src/components/RadioField/__stories__/Checked.stories.tsx @@ -1,9 +1,9 @@ import type { StoryFn } from '@storybook/react' +import type { FormErrors } from '../../../types' import { RadioField } from '..' -import type { FormProps } from '../../Form' import { Form } from '../../Form' -export const Checked: StoryFn = ({ errors }) => ( +export const Checked: StoryFn<{ errors: FormErrors }> = ({ errors }) => (
{}} errors={errors} initialValues={{ foo: 'bar' }}> diff --git a/packages/form/src/components/RadioField/index.tsx b/packages/form/src/components/RadioField/index.tsx index cb23c4f194..8c85343b2e 100644 --- a/packages/form/src/components/RadioField/index.tsx +++ b/packages/form/src/components/RadioField/index.tsx @@ -1,13 +1,13 @@ import { Radio } from '@ultraviolet/ui' -import type { FieldState } from 'final-form' import type { ComponentProps, JSX } from 'react' -import { useFormField } from '../../hooks' -import { useErrors } from '../../providers' +import type { FieldValues } from 'react-hook-form' +import { Controller } from 'react-hook-form' import type { BaseFieldProps } from '../../types' -type RadioValue = NonNullable['value']> - -type RadioFieldProps = BaseFieldProps & +type RadioFieldProps = Omit< + BaseFieldProps, + 'label' +> & Partial< Pick< ComponentProps, @@ -27,63 +27,49 @@ type RadioFieldProps = BaseFieldProps & required?: boolean } -export const RadioField = ({ +export const RadioField = ({ className, 'data-testid': dataTestId, disabled, id, - label = '', name, onBlur, + label = '', onChange, onFocus, required, - validate, value, + rules, tooltip, -}: RadioFieldProps): JSX.Element => { - const { getError } = useErrors() - - const { input, meta } = useFormField(name, { - required, - type: 'radio', - validate, - value, - }) - - const error = getError({ - disabled, - label: label as string, - meta: meta as FieldState, - name, - value: input.value, - }) - - return ( - { - input.onChange(event) - onChange?.(event) - }} - onBlur={event => { - input.onBlur(event) - onBlur?.(event) - }} - onFocus={event => { - input.onFocus(event) - onFocus?.(event) - }} - required={required} - value={input.value} - label={label} - tooltip={tooltip} - /> - ) -} +}: RadioFieldProps): JSX.Element => ( + ( + { + field.onChange(value) + onChange?.(event) + }} + onBlur={event => { + field.onBlur() + onBlur?.(event) + }} + onFocus={onFocus} + required={required} + value={value ?? ''} + label={label} + tooltip={tooltip} + /> + )} + /> +) diff --git a/packages/form/src/components/RadioGroupField/__stories__/Required.stories.tsx b/packages/form/src/components/RadioGroupField/__stories__/Required.stories.tsx index 550118c521..079afc2317 100644 --- a/packages/form/src/components/RadioGroupField/__stories__/Required.stories.tsx +++ b/packages/form/src/components/RadioGroupField/__stories__/Required.stories.tsx @@ -1,11 +1,11 @@ import type { StoryFn } from '@storybook/react' import { Stack } from '@ultraviolet/ui' -import { useFormState } from 'react-final-form' +import { useFormContext } from 'react-hook-form' import { RadioGroupField } from '..' import { Submit } from '../..' export const RequiredStory: StoryFn = args => { - const { values } = useFormState() + const { watch } = useFormContext() return ( @@ -15,7 +15,7 @@ export const RequiredStory: StoryFn = args => { Submit - Form content: {JSON.stringify(values)} + Form content: {JSON.stringify(watch())} ) diff --git a/packages/form/src/components/RadioGroupField/__stories__/Template.stories.tsx b/packages/form/src/components/RadioGroupField/__stories__/Template.stories.tsx index 272be76309..eed8b45fa8 100644 --- a/packages/form/src/components/RadioGroupField/__stories__/Template.stories.tsx +++ b/packages/form/src/components/RadioGroupField/__stories__/Template.stories.tsx @@ -1,14 +1,14 @@ import type { StoryFn } from '@storybook/react' import { Stack } from '@ultraviolet/ui' import type { ComponentProps } from 'react' -import { useFormState } from 'react-final-form' +import { useFormContext } from 'react-hook-form' import { Form, RadioGroupField } from '../..' import { mockErrors } from '../../../mocks' const RadioGroupFieldStory: StoryFn< ComponentProps > = args => { - const { values } = useFormState() + const { watch } = useFormContext() return ( @@ -17,7 +17,7 @@ const RadioGroupFieldStory: StoryFn< - Form content: {JSON.stringify(values)} + Form content: {JSON.stringify(watch())} ) diff --git a/packages/form/src/components/RadioGroupField/index.tsx b/packages/form/src/components/RadioGroupField/index.tsx index f1b2f70da2..7a51255445 100644 --- a/packages/form/src/components/RadioGroupField/index.tsx +++ b/packages/form/src/components/RadioGroupField/index.tsx @@ -1,77 +1,70 @@ import { RadioGroup } from '@ultraviolet/ui' -import type { FieldState } from 'final-form' import type { ComponentProps, JSX } from 'react' -import { useFormField } from '../../hooks' +import type { FieldValues } from 'react-hook-form' +import { Controller } from 'react-hook-form' import { useErrors } from '../../providers' import type { BaseFieldProps } from '../../types' -type RadioValue = NonNullable['value']> +type RadioGroupFieldProps = + BaseFieldProps & + Partial< + Pick< + ComponentProps, + | 'onChange' + | 'value' + | 'legend' + | 'children' + | 'required' + | 'name' + | 'error' + | 'helper' + | 'direction' + > + > & { + className?: string + name: string + required?: boolean + } -type RadioGroupFieldProps = BaseFieldProps & - Partial< - Pick< - ComponentProps, - | 'onChange' - | 'value' - | 'legend' - | 'children' - | 'required' - | 'name' - | 'error' - | 'helper' - | 'direction' - > - > & { - className?: string - name: string - required?: boolean - } - -export const RadioGroupField = ({ +export const RadioGroupField = ({ className, legend = '', name, onChange, required, - validate, value, + rules, children, + label = '', error: customError, helper, direction, -}: RadioGroupFieldProps): JSX.Element => { +}: RadioGroupFieldProps): JSX.Element => { const { getError } = useErrors() - const { input, meta } = useFormField(name, { - required, - validate, - value, - }) - - const error = getError({ - label: legend, - meta: meta as FieldState, - name, - value: input.value, - }) - return ( - { - input.onChange(event) - onChange?.(event) - }} - required={required} - value={input.value} - legend={legend} - error={error ?? customError} - helper={helper} - direction={direction} - > - {children} - + ( + { + field.onChange(event) + onChange?.(event) + }} + required={required} + value={value ?? ''} + legend={legend} + error={getError({ label }, error) ?? customError} + helper={helper} + direction={direction} + > + {children} + + )} + /> ) } diff --git a/packages/form/src/components/SelectInputField/index.tsx b/packages/form/src/components/SelectInputField/index.tsx index d168f37c2c..a2517572f0 100644 --- a/packages/form/src/components/SelectInputField/index.tsx +++ b/packages/form/src/components/SelectInputField/index.tsx @@ -1,6 +1,5 @@ import type { CSSObject, Theme, css } from '@emotion/react' import { SelectInput } from '@ultraviolet/ui' -import type { FieldState } from 'final-form' import type { ComponentProps, ForwardedRef, @@ -8,10 +7,10 @@ import type { ReactNode, } from 'react' import { Children, useCallback, useMemo } from 'react' +import type { FieldValues } from 'react-hook-form' +import { Controller } from 'react-hook-form' import type { CommonProps, GroupBase, OptionProps, Props } from 'react-select' import type Select from 'react-select' -import { useFormField } from '../../hooks' -import { useErrors } from '../../providers' import type { BaseFieldProps } from '../../types' // Here we duplicate SelectInput types as they are using interfaces which are not portable @@ -67,8 +66,7 @@ type StateManagedSelect = typeof Select type SelectInputProps = Partial< SelectProps & - SelectStyleProps & - Pick, 'data-testid'> & { + SelectStyleProps & { /** * Name of the animation */ @@ -96,84 +94,81 @@ type SelectInputOptionOrGroup = NonNullable[number] type SelectInputOption = { value: string; label: string } export type SelectInputFieldProps< - T extends SelectInputOptionOrGroup = SelectInputOptionOrGroup, -> = BaseFieldProps & - Pick< - SelectInputProps, - | 'animation' - | 'animationDuration' - | 'animationOnChange' - | 'children' - | 'className' - | 'disabled' - | 'error' - | 'id' - | 'inputId' - | 'isClearable' - | 'isLoading' - | 'isSearchable' - | 'menuPortalTarget' - | 'onBlur' - | 'onChange' - | 'onFocus' - | 'options' - | 'placeholder' - | 'readOnly' - | 'required' - | 'value' - | 'noTopLabel' - | 'noOptionsMessage' - | 'customStyle' - | 'data-testid' + TFieldValues extends FieldValues = FieldValues, +> = BaseFieldProps & + Partial< + Pick< + SelectInputProps, + | 'animation' + | 'animationDuration' + | 'animationOnChange' + | 'children' + | 'className' + | 'disabled' + | 'error' + | 'id' + | 'inputId' + | 'isClearable' + | 'isLoading' + | 'isSearchable' + | 'menuPortalTarget' + | 'onBlur' + | 'onChange' + | 'onFocus' + | 'options' + | 'placeholder' + | 'readOnly' + | 'required' + | 'value' + | 'noTopLabel' + | 'noOptionsMessage' + | 'customStyle' + > > & { label?: string - maxLength?: number - minLength?: number + multiple?: boolean + parse?: (value: unknown, name?: string) => unknown + format?: (value: unknown, name: string) => unknown name: string } -const identity = (x: T) => x +const identity = (x: unknown) => x +// const identity = (x: T) => x -export const SelectInputField = < - T extends SelectInputOptionOrGroup = SelectInputOptionOrGroup, ->({ +export const SelectInputField = ({ animation, animationDuration, animationOnChange, children, className, disabled, - error: errorProp, - format: formatProp = identity as NonNullable['format']>, - formatOnBlur, + // error: errorProp, + format: formatProp = identity, + // formatOnBlur, id, inputId, isClearable, isLoading, isSearchable, - label = '', - maxLength, + // label = '', + // maxLength, menuPortalTarget, - minLength, + // minLength, multiple, name, onBlur, onChange, onFocus, options: optionsProp, - parse: parseProp = identity as NonNullable['parse']>, + parse: parseProp = identity, placeholder, readOnly, required, - value, + rules, noTopLabel, noOptionsMessage, customStyle, - validate, - 'data-testid': dataTestId, -}: SelectInputFieldProps) => { - const { getError } = useErrors() - +}: SelectInputFieldProps) => { const options = useMemo( () => optionsProp || @@ -198,7 +193,7 @@ export const SelectInputField = < ) const format = useCallback( - (val: T) => { + (val: unknown) => { if (multiple) return formatProp(val, name) as SelectInputOption const find = (opts: SelectInputOptionOrGroup[], valueToFind: string) => @@ -235,75 +230,57 @@ export const SelectInputField = < } } - return formatProp(selected as T, name) as SelectInputOption + return formatProp(selected, name) as SelectInputOption }, [formatProp, multiple, name, options], ) - const { input, meta } = useFormField( - name, - { - disabled, - format, - formatOnBlur, - maxLength, - minLength: minLength || required ? 1 : undefined, - multiple, - parse, - required, - value, - validate, - }, - ) - - const error = getError({ - errorProp, - label, - meta: meta as FieldState, - name, - value: input.value, - }) - return ( - { - input.onBlur(event) - onBlur?.(event) - }} - onChange={(event, action) => { - input.onChange(event) - onChange?.(event, action) + rules={{ + required, + ...rules, }} - onFocus={event => { - input.onFocus(event) - onFocus?.(event) - }} - options={options} - placeholder={placeholder} - readOnly={readOnly} - value={input.value} - noTopLabel={noTopLabel} - required={required} - noOptionsMessage={noOptionsMessage} - data-testid={dataTestId} - > - {children} - + render={({ field, fieldState: { error } }) => ( + { + field.onBlur() + onBlur?.(event) + }} + onChange={(event, action) => { + field.onChange(parse(event)) + onChange?.(event, action) + }} + onFocus={onFocus} + options={options} + placeholder={placeholder} + readOnly={readOnly} + noTopLabel={noTopLabel} + required={required} + // value={value} + value={format(field.value)} + noOptionsMessage={noOptionsMessage} + > + {children} + + )} + /> ) } diff --git a/packages/form/src/components/SelectableCardField/__stories__/Checked.stories.tsx b/packages/form/src/components/SelectableCardField/__stories__/Checked.stories.tsx index 3d3c774461..fefcedd723 100644 --- a/packages/form/src/components/SelectableCardField/__stories__/Checked.stories.tsx +++ b/packages/form/src/components/SelectableCardField/__stories__/Checked.stories.tsx @@ -1,10 +1,10 @@ import type { StoryFn } from '@storybook/react' import { Stack } from '@ultraviolet/ui' +import type { FormErrors } from '../../../types' import { SelectableCardField } from '..' -import type { FormProps } from '../../Form' import { Form } from '../../Form' -export const Checked: StoryFn = ({ errors }) => ( +export const Checked: StoryFn<{ errors: FormErrors }> = ({ errors }) => (
{}} errors={errors} initialValues={{ foo: 'bar' }}> diff --git a/packages/form/src/components/SelectableCardField/index.tsx b/packages/form/src/components/SelectableCardField/index.tsx index 9ed80009e6..890d64714a 100644 --- a/packages/form/src/components/SelectableCardField/index.tsx +++ b/packages/form/src/components/SelectableCardField/index.tsx @@ -1,18 +1,13 @@ import { SelectableCard } from '@ultraviolet/ui' -import type { FieldState } from 'final-form' -import type { ComponentProps, JSX } from 'react' -import { useFormField } from '../../hooks' -import { useErrors } from '../../providers' +import type { ComponentProps } from 'react' +import type { FieldValues } from 'react-hook-form' +import { Controller } from 'react-hook-form' import type { BaseFieldProps } from '../../types' -type SelectableCardValue = NonNullable< - ComponentProps['value'] -> - -type SelectableCardFieldProps< - T = SelectableCardValue, - K = string, -> = BaseFieldProps & +type SelectableCardFieldProps = Omit< + BaseFieldProps, + 'label' +> & Partial< Pick< ComponentProps, @@ -28,15 +23,12 @@ type SelectableCardFieldProps< | 'name' | 'tooltip' | 'label' - | 'data-testid' > > & { - name: string - required?: boolean className?: string } -export const SelectableCardField = ({ +export const SelectableCardField = ({ name, value, onChange, @@ -48,57 +40,43 @@ export const SelectableCardField = ({ onFocus, onBlur, required, - validate, tooltip, id, label, - 'data-testid': dataTestId, -}: SelectableCardFieldProps): JSX.Element => { - const { getError } = useErrors() - - const { input, meta } = useFormField(name, { - disabled, - required, - type: type ?? 'radio', - validate, - value, - }) - - const error = getError({ - label: name, - meta: meta as FieldState, - name, - value: input.value, - }) - - return ( - { - input.onChange(event) - onChange?.(event) - }} - onBlur={event => { - input.onBlur(event) - onBlur?.(event) - }} - onFocus={event => { - input.onFocus(event) - onFocus?.(event) - }} - type={type} - value={input.value} - id={id} - tooltip={tooltip} - label={label} - data-testid={dataTestId} - > - {children} - - ) -} + rules, +}: SelectableCardFieldProps) => ( + ( + { + field.onChange(event) + onChange?.(event) + }} + onBlur={event => { + field.onBlur() + onBlur?.(event) + }} + onFocus={event => { + onFocus?.(event) + }} + type={type} + id={id} + tooltip={tooltip} + label={label} + value={value ?? ''} + > + {children} + + )} + /> +) diff --git a/packages/form/src/components/Submit/index.tsx b/packages/form/src/components/Submit/index.tsx index c846c97a7a..ed0f371d79 100644 --- a/packages/form/src/components/Submit/index.tsx +++ b/packages/form/src/components/Submit/index.tsx @@ -1,7 +1,6 @@ import { Button } from '@ultraviolet/ui' -import type { ComponentProps, JSX, ReactNode } from 'react' -import { useEffect, useState } from 'react' -import { useFormState } from 'react-final-form' +import type { ComponentProps, ReactNode } from 'react' +import { useFormState } from 'react-hook-form' type SubmitProps = { children?: ReactNode @@ -29,24 +28,10 @@ export const Submit = ({ tooltip, fullWidth, onClick, -}: SubmitProps): JSX.Element => { - const { invalid, submitting, hasValidationErrors, dirtySinceLastSubmit } = - useFormState({ - subscription: { - dirtySinceLastSubmit: true, - hasValidationErrors: true, - invalid: true, - submitting: true, - }, - }) - const [isLoading, setIsLoading] = useState(true) - const isDisabled = - disabled || - submitting || - isLoading || - (invalid && hasValidationErrors && !dirtySinceLastSubmit) +}: SubmitProps) => { + const { isSubmitting, isValid } = useFormState() - useEffect(() => setIsLoading(false), []) + const isDisabled = disabled || isSubmitting || !isValid return ( - - - - tags-2 - - - diff --git a/packages/form/src/components/ToggleField/__tests__/index.spec.tsx b/packages/form/src/components/ToggleField/__tests__/index.spec.tsx index 7b3aa64847..f1ba8d9f2d 100644 --- a/packages/form/src/components/ToggleField/__tests__/index.spec.tsx +++ b/packages/form/src/components/ToggleField/__tests__/index.spec.tsx @@ -11,15 +11,29 @@ describe('ToggleField', () => { shouldMatchEmotionSnapshotFormWrapper()) test('should render correctly checked', () => - shouldMatchEmotionSnapshotFormWrapper(, { - transform: () => { - const element = screen.getByRole('checkbox') - expect(element.checked).toBeTruthy() + shouldMatchEmotionSnapshotFormWrapper( + , + { + transform: () => { + const element = screen.getByRole('checkbox') + expect(element.checked).toBeTruthy() + }, + }, + { + initialValues: { + test: true, + }, }, - })) + )) test('should render correctly with label and checked', () => shouldMatchEmotionSnapshotFormWrapper( - , + , + {}, + { + initialValues: { + test: true, + }, + }, )) })