From 64af9b3f3344b9c637db4a93968119db35ec2677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Lafont?= Date: Wed, 3 Jul 2024 15:29:05 +0200 Subject: [PATCH] feat(form): remove final-form legacy props and hook (#3780) * feat(form): remove final-form legacy props and hook --- .changeset/khaki-ants-grow.md | 69 +++++++ examples/next-login/src/pages/home/login.tsx | 10 +- examples/next-login/src/pages/home/signup.tsx | 6 +- .../migrations/MigrationFormV3.mdx | 67 +++++++ .../__stories__/BooleanChecked.stories.tsx | 15 +- .../__stories__/Checked.stories.tsx | 17 +- .../__stories__/index.stories.tsx | 4 +- .../CheckboxField/__tests__/index.test.tsx | 14 +- .../src/components/CheckboxField/index.tsx | 16 +- .../__stories__/Direction.stories.tsx | 2 +- .../__stories__/index.stories.tsx | 4 +- .../__tests__/index.test.tsx | 4 +- .../components/CheckboxGroupField/index.tsx | 22 ++- .../DateField/__stories__/index.stories.tsx | 4 +- .../DateField/__tests__/index.test.tsx | 2 +- .../form/src/components/DateField/index.tsx | 19 +- .../Form/__stories__/Playground.stories.tsx | 122 ++++++++---- .../__snapshots__/index.test.tsx.snap | 16 +- .../components/Form/__tests__/index.test.tsx | 31 +-- packages/form/src/components/Form/index.tsx | 180 +++--------------- .../__stories__/index.stories.tsx | 4 +- .../__stories__/index.stories.tsx | 4 +- .../NumberInputField/__tests__/index.test.tsx | 8 +- .../src/components/NumberInputField/index.tsx | 16 +- .../__stories__/index.stories.tsx | 4 +- .../__tests__/index.test.tsx | 2 +- .../components/NumberInputFieldV2/index.tsx | 18 +- .../__stories__/Checked.stories.tsx | 15 +- .../RadioField/__stories__/index.stories.tsx | 4 +- .../RadioField/__tests__/index.test.tsx | 2 +- .../form/src/components/RadioField/index.tsx | 16 +- .../__stories__/index.stories.tsx | 4 +- .../src/components/RadioGroupField/index.tsx | 16 +- .../__stories__/index.stories.tsx | 4 +- .../src/components/SelectInputField/index.tsx | 14 +- .../__stories__/index.stories.tsx | 4 +- .../components/SelectInputFieldV2/index.tsx | 18 +- .../__stories__/Checked.stories.tsx | 29 +-- .../__stories__/index.stories.tsx | 4 +- .../__tests__/index.test.tsx | 2 +- .../components/SelectableCardField/index.tsx | 16 +- .../__stories__/index.stories.tsx | 4 +- .../__tests__/index.test.tsx | 4 +- .../SelectableCardGroupField/index.tsx | 18 +- .../Submit/__stories__/Invalid.stories.tsx | 17 +- .../Submit/__stories__/Playground.stories.tsx | 15 +- .../Submit/__stories__/Submitting.stories.tsx | 34 ++-- .../Submit/__tests__/index.test.tsx | 18 +- .../__stories__/Playground.stories.tsx | 29 +-- .../SubmitErrorAlert/__tests__/index.test.tsx | 13 +- .../__stories__/index.stories.tsx | 4 +- .../__snapshots__/index.test.tsx.snap | 2 +- .../TagInputField/__tests__/index.test.tsx | 4 +- .../src/components/TagInputField/index.tsx | 16 +- .../__stories__/Template.stories.tsx | 3 +- .../__stories__/index.stories.tsx | 23 ++- .../TextAreaField/__tests__/index.test.tsx | 10 +- .../src/components/TextAreaField/index.tsx | 25 +-- .../__stories__/index.stories.tsx | 4 +- .../TextInputField/__tests__/index.test.tsx | 2 +- .../src/components/TextInputField/index.tsx | 43 ++--- .../__stories__/index.stories.tsx | 4 +- .../TextInputFieldV2/__tests__/index.test.tsx | 2 +- .../src/components/TextInputFieldV2/index.tsx | 25 +-- .../TimeField/__stories__/index.stories.tsx | 4 +- .../form/src/components/TimeField/index.tsx | 20 +- .../ToggleField/__stories__/index.stories.tsx | 10 +- .../ToggleField/__tests__/index.test.tsx | 4 +- .../form/src/components/ToggleField/index.tsx | 16 +- .../__stories__/Required.stories.tsx | 2 +- .../__stories__/Template.stories.tsx | 2 +- .../__stories__/index.stories.tsx | 4 +- .../ToggleGroupField/__tests__/index.test.tsx | 4 +- .../src/components/ToggleGroupField/index.tsx | 20 +- .../__stories__/index.stories.tsx | 4 +- .../UnitInputField/__tests__/index.test.tsx | 2 +- .../src/components/UnitInputField/index.tsx | 36 ++-- packages/form/src/components/index.ts | 1 - packages/form/src/constants.ts | 1 - .../__stories__/useOnFieldChange.stories.tsx | 20 +- .../hooks/__tests__/useOnFieldChange.test.tsx | 20 +- packages/form/src/hooks/index.ts | 4 - packages/form/src/hooks/useField.ts | 60 ------ packages/form/src/hooks/useFieldArray.ts | 48 ----- packages/form/src/hooks/useForm.ts | 33 ---- packages/form/src/hooks/useFormState.ts | 42 ---- packages/form/src/hooks/useOnFieldChange.ts | 3 + packages/form/src/index.ts | 39 +--- .../ErrorContext/__tests__/index.test.tsx | 22 ++- packages/form/src/types.ts | 21 +- packages/form/src/utils/validateRegex.ts | 17 ++ packages/ui/CHANGELOG.md | 2 +- .../ThemeGenerator/FormContent/index.tsx | 16 +- .../Tools/ThemeGenerator/index.tsx | 2 +- utils/test/src/vitest/helpers/index.tsx | 28 ++- 95 files changed, 795 insertions(+), 859 deletions(-) create mode 100644 .changeset/khaki-ants-grow.md create mode 100644 packages/form/src/__stories__/migrations/MigrationFormV3.mdx delete mode 100644 packages/form/src/constants.ts delete mode 100644 packages/form/src/hooks/useField.ts delete mode 100644 packages/form/src/hooks/useFieldArray.ts delete mode 100644 packages/form/src/hooks/useForm.ts delete mode 100644 packages/form/src/hooks/useFormState.ts create mode 100644 packages/form/src/utils/validateRegex.ts diff --git a/.changeset/khaki-ants-grow.md b/.changeset/khaki-ants-grow.md new file mode 100644 index 0000000000..e17600827d --- /dev/null +++ b/.changeset/khaki-ants-grow.md @@ -0,0 +1,69 @@ +--- +"@ultraviolet/form": major +--- + +## Accept control prop + +Field can accept a `control` prop. When this prop is provided, the `name` prop must be present in the form and the `onChange` callback is typed. + +## Remove defaultValues + +You must use the `useForm` hook to provide initial values: + +```tsx +// Old +
+ // ... +
+ +// New +const methods = useForm({ defaultValues: { foo: 'bar' }}) + +
+ // ... +
+``` + +## Remove function as child component + +Function as child component must be remove: + +```tsx +// Old +
+ {({ isSubmitting }) => ( + + )} +
+ +// New +const methods = useForm() + +const { isSubmitting } = methods.formState + +
+ +
+``` + +## onRawSubmit renamed to onSubmit + +The `onRawSubmit` is renamed to `onSubmit`. + +The return of the function is now a string if an error occurred. + +```tsx +// Old +
{ + return { [FORM_ERROR]: 'ERROR' } +}}> + // ... +
+ +// New +
{ + return 'ERROR' +}}> + // ... +
+``` diff --git a/examples/next-login/src/pages/home/login.tsx b/examples/next-login/src/pages/home/login.tsx index c8578bb9ad..c398f02495 100644 --- a/examples/next-login/src/pages/home/login.tsx +++ b/examples/next-login/src/pages/home/login.tsx @@ -54,11 +54,7 @@ const LogIn = () => { return ( - - methods={methods} - errors={mockErrors} - onRawSubmit={handleSubmit} - > +
@@ -69,9 +65,7 @@ const LogIn = () => { name="email" required placeholder="example@email.com" - rules={{ - pattern: { value: EMAIL_REGEX, message: 'Invalid format' }, - }} + regex={[EMAIL_REGEX]} /> { methods={methods} errors={mockErrors} - onRawSubmit={handleSubmit} + onSubmit={handleSubmit} > @@ -130,9 +130,7 @@ const SignUp = () => { name="email" required placeholder="example@email.com" - rules={{ - pattern: { value: EMAIL_REGEX, message: 'Invalid format' }, - }} + regex={[EMAIL_REGEX]} className="inputs" /> + +# Migrate Formv2 to Formv3 + +## Remove defaultValues + +You must use the `useForm` hook to provide initial values: + +```tsx +// Old + + // ... + + +// New +const methods = useForm({ defaultValues: { foo: 'bar' }}) + +
+ // ... +
+``` + +## Remove function as child component + +Function as child component must be remove: + +```tsx +// Old +
+ {({ isSubmitting }) => ( + + )} +
+ +// New +const methods = useForm() + +const { isSubmitting } = methods.formState + +
+ +
+``` + +## onRawSubmit renamed to onSubmit + +The `onRawSubmit` is renamed to `onSubmit`. + +The return of the function is now a string if an error occurred. + +```tsx +// Old +
{ + return { [FORM_ERROR]: 'ERROR' } +}}> + // ... +
+ +// New +
{ + return 'ERROR' +}}> + // ... +
+``` diff --git a/packages/form/src/components/CheckboxField/__stories__/BooleanChecked.stories.tsx b/packages/form/src/components/CheckboxField/__stories__/BooleanChecked.stories.tsx index 581921dd85..15eb6949af 100644 --- a/packages/form/src/components/CheckboxField/__stories__/BooleanChecked.stories.tsx +++ b/packages/form/src/components/CheckboxField/__stories__/BooleanChecked.stories.tsx @@ -1,10 +1,15 @@ import type { StoryFn } from '@storybook/react' import { CheckboxField } from '..' +import { useForm } from '../../..' import type { FormErrors } from '../../../types' import { Form } from '../../Form' -export const BooleanChecked: StoryFn<{ errors: FormErrors }> = ({ errors }) => ( -
{}} errors={errors} initialValues={{ foo: true }}> - Default Checked Boolean Item -
-) +export const BooleanChecked: StoryFn<{ errors: FormErrors }> = ({ errors }) => { + const methods = useForm({ defaultValues: { foo: true } }) + + return ( +
{}} errors={errors} methods={methods}> + 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 c06d0bd4db..ac594d65c0 100644 --- a/packages/form/src/components/CheckboxField/__stories__/Checked.stories.tsx +++ b/packages/form/src/components/CheckboxField/__stories__/Checked.stories.tsx @@ -1,11 +1,16 @@ import type { StoryFn } from '@storybook/react' import { CheckboxField } from '..' +import { useForm } from '../../..' import type { FormErrors } from '../../../types' import { Form } from '../../Form' -export const Checked: StoryFn<{ errors: FormErrors }> = ({ errors }) => ( -
{}} errors={errors} initialValues={{ foo: true }}> - Checked Item - Not Checked Item -
-) +export const Checked: StoryFn<{ errors: FormErrors }> = ({ errors }) => { + const methods = useForm({ defaultValues: { foo: true } }) + + return ( +
{}} errors={errors} methods={methods}> + Checked Item + Not Checked Item +
+ ) +} diff --git a/packages/form/src/components/CheckboxField/__stories__/index.stories.tsx b/packages/form/src/components/CheckboxField/__stories__/index.stories.tsx index 6268b08766..a2c67180d1 100644 --- a/packages/form/src/components/CheckboxField/__stories__/index.stories.tsx +++ b/packages/form/src/components/CheckboxField/__stories__/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { CheckboxField, Form } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -24,7 +24,7 @@ export default { } = methods.formState return ( -
{}} errors={mockErrors} methods={methods}> + {}} errors={mockErrors} methods={methods}> diff --git a/packages/form/src/components/CheckboxField/__tests__/index.test.tsx b/packages/form/src/components/CheckboxField/__tests__/index.test.tsx index 87abc8a630..189962f56b 100644 --- a/packages/form/src/components/CheckboxField/__tests__/index.test.tsx +++ b/packages/form/src/components/CheckboxField/__tests__/index.test.tsx @@ -1,6 +1,7 @@ -import { act, screen } from '@testing-library/react' +import { act, renderHook, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { mockFormErrors, renderWithForm, renderWithTheme } from '@utils/test' +import { useForm } from 'react-hook-form' import { describe, expect, test, vi } from 'vitest' import { CheckboxField } from '../..' import { Form } from '../../Form' @@ -24,7 +25,7 @@ describe('CheckboxField', () => { test('should render correctly checked without value', () => { const { asFragment } = renderWithForm(, { - initialValues: { + defaultValues: { checked: true, }, }) @@ -36,7 +37,7 @@ describe('CheckboxField', () => { test('should render correctly not checked without value', () => { const { asFragment } = renderWithForm(, { - initialValues: {}, + defaultValues: {}, }) const input = screen.getByRole('checkbox', { hidden: true }) @@ -71,8 +72,13 @@ describe('CheckboxField', () => { }) test('should render correctly with errors', async () => { + const { result } = renderHook(() => useForm({ mode: 'onChange' })) const { asFragment } = renderWithTheme( - {}} errors={mockFormErrors}> + {}} + errors={mockFormErrors} + methods={result.current} + > Checkbox field error diff --git a/packages/form/src/components/CheckboxField/index.tsx b/packages/form/src/components/CheckboxField/index.tsx index 55acb7c71c..69220815cd 100644 --- a/packages/form/src/components/CheckboxField/index.tsx +++ b/packages/form/src/components/CheckboxField/index.tsx @@ -7,8 +7,8 @@ import type { BaseFieldProps } from '../../types' type CheckboxFieldProps< TFieldValues extends FieldValues, - TName extends FieldPath, -> = Omit, 'value'> & + TFieldName extends FieldPath, +> = Omit, 'value'> & Partial< Pick< ComponentProps, @@ -29,9 +29,10 @@ type CheckboxFieldProps< export const CheckboxField = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ id, + control, name, label, size, @@ -43,23 +44,24 @@ export const CheckboxField = < onChange, onBlur, onFocus, - rules, helper, tooltip, 'data-testid': dataTestId, shouldUnregister = false, -}: CheckboxFieldProps) => { + validate, +}: CheckboxFieldProps) => { const { getError } = useErrors() const { field, fieldState: { error }, - } = useController({ + } = useController({ name, disabled, shouldUnregister, + control, rules: { required, - ...rules, + validate, }, }) diff --git a/packages/form/src/components/CheckboxGroupField/__stories__/Direction.stories.tsx b/packages/form/src/components/CheckboxGroupField/__stories__/Direction.stories.tsx index f10edc7df9..2f7db85abb 100644 --- a/packages/form/src/components/CheckboxGroupField/__stories__/Direction.stories.tsx +++ b/packages/form/src/components/CheckboxGroupField/__stories__/Direction.stories.tsx @@ -1,7 +1,7 @@ import type { StoryFn } from '@storybook/react' import { Stack } from '@ultraviolet/ui' -import { useFormContext } from 'react-hook-form' import { CheckboxGroupField } from '..' +import { useFormContext } from '../../..' export const DirectionStory: StoryFn = args => { const { watch } = useFormContext() diff --git a/packages/form/src/components/CheckboxGroupField/__stories__/index.stories.tsx b/packages/form/src/components/CheckboxGroupField/__stories__/index.stories.tsx index f86da8142a..8b250eb38c 100644 --- a/packages/form/src/components/CheckboxGroupField/__stories__/index.stories.tsx +++ b/packages/form/src/components/CheckboxGroupField/__stories__/index.stories.tsx @@ -1,8 +1,8 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { CheckboxGroupField } from '..' import { Form } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -30,7 +30,7 @@ export default { return ( { + onSubmit={data => { console.log('data', data) }} errors={mockErrors} diff --git a/packages/form/src/components/CheckboxGroupField/__tests__/index.test.tsx b/packages/form/src/components/CheckboxGroupField/__tests__/index.test.tsx index fa488584d3..2053b09599 100644 --- a/packages/form/src/components/CheckboxGroupField/__tests__/index.test.tsx +++ b/packages/form/src/components/CheckboxGroupField/__tests__/index.test.tsx @@ -15,7 +15,7 @@ describe('CheckboxField', () => { , { - initialValues: { + defaultValues: { Checkbox: [], }, }, @@ -51,7 +51,7 @@ describe('CheckboxField', () => { , { - initialValues: { + defaultValues: { test: [], }, }, diff --git a/packages/form/src/components/CheckboxGroupField/index.tsx b/packages/form/src/components/CheckboxGroupField/index.tsx index 38037b68eb..f1367e6a8e 100644 --- a/packages/form/src/components/CheckboxGroupField/index.tsx +++ b/packages/form/src/components/CheckboxGroupField/index.tsx @@ -16,8 +16,8 @@ const arraysContainSameValues = (array1: string[], array2: string[]) => { type CheckboxGroupFieldProps< TFieldValues extends FieldValues, - TName extends FieldPath, -> = BaseFieldProps & + TFieldName extends FieldPath, +> = BaseFieldProps & Partial< Pick< ComponentProps, @@ -40,10 +40,11 @@ type ElementProps = { export const CheckboxGroupField = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ legend, className, + control, helper, direction, children, @@ -53,10 +54,10 @@ export const CheckboxGroupField = < name, required = false, shouldUnregister = false, - rules, -}: CheckboxGroupFieldProps) => { + validate, +}: CheckboxGroupFieldProps) => { const { getError } = useErrors() - const validate = useCallback( + const checkboxValid = useCallback( (value: string[]) => { const requiredChildren = Children.map(children, child => { @@ -87,12 +88,15 @@ export const CheckboxGroupField = < const { field, fieldState: { error }, - } = useController({ + } = useController({ name, + control, shouldUnregister, rules: { - validate, - ...rules, + validate: { + checkboxValid, + ...validate, + }, }, }) diff --git a/packages/form/src/components/DateField/__stories__/index.stories.tsx b/packages/form/src/components/DateField/__stories__/index.stories.tsx index 519bc40e37..3f09b613f4 100644 --- a/packages/form/src/components/DateField/__stories__/index.stories.tsx +++ b/packages/form/src/components/DateField/__stories__/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { DateField, Form } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -24,7 +24,7 @@ export default { } = methods.formState return ( - {}} errors={mockErrors} methods={methods}> + {}} errors={mockErrors} methods={methods}> diff --git a/packages/form/src/components/DateField/__tests__/index.test.tsx b/packages/form/src/components/DateField/__tests__/index.test.tsx index 8e0d9b2753..68945973a1 100644 --- a/packages/form/src/components/DateField/__tests__/index.test.tsx +++ b/packages/form/src/components/DateField/__tests__/index.test.tsx @@ -21,7 +21,7 @@ describe('DateField', () => { const { asFragment } = renderWithForm( , { - initialValues: { + defaultValues: { test: new Date('2022-09-01'), }, }, diff --git a/packages/form/src/components/DateField/index.tsx b/packages/form/src/components/DateField/index.tsx index 12fb14accc..c36769216e 100644 --- a/packages/form/src/components/DateField/index.tsx +++ b/packages/form/src/components/DateField/index.tsx @@ -11,8 +11,8 @@ type DateExtends = Date | [Date | null, Date | null] type DateFieldProps< TFieldValues extends FieldValues, - TName extends FieldPath, -> = BaseFieldProps & + TFieldName extends FieldPath, +> = BaseFieldProps & Omit< ComponentProps, | 'maxDate' @@ -46,10 +46,11 @@ const isEmpty = (value?: Date | string): boolean => export const DateField = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ required, name, + control, label = '', format, locale, @@ -59,29 +60,29 @@ export const DateField = < onChange, onBlur, onFocus, - rules, + validate, autoFocus = false, excludeDates, selectsRange, size, 'data-testid': dataTestId, shouldUnregister = false, -}: DateFieldProps) => { +}: DateFieldProps) => { const { getError } = useErrors() const { field, fieldState: { error }, - } = useController({ + } = useController({ name, + control, shouldUnregister, rules: { - ...rules, + required, validate: { - ...rules?.validate, minDate: minDateValidator(minDate), maxDate: maxDateValidator(maxDate), + ...validate, }, - required, }, }) diff --git a/packages/form/src/components/Form/__stories__/Playground.stories.tsx b/packages/form/src/components/Form/__stories__/Playground.stories.tsx index 517e9eafb8..c3318de180 100644 --- a/packages/form/src/components/Form/__stories__/Playground.stories.tsx +++ b/packages/form/src/components/Form/__stories__/Playground.stories.tsx @@ -1,11 +1,10 @@ import type { StoryFn } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { CheckboxField, DateField, Form, - NumberInputField, + NumberInputFieldV2, RadioField, SelectInputFieldV2, SelectableCardField, @@ -13,11 +12,18 @@ import { SubmitErrorAlert, TagInputField, TextInputField, + TextInputFieldV2, TimeField, ToggleField, } from '../..' +import { useForm } from '../../..' import { emailRegex, mockErrors } from '../../../mocks/mockErrors' +const data = [ + { value: '1', label: '1', disabled: false }, + { value: '2', label: '2', disabled: false }, +] + type FormValues = { receiveEmailUpdates: boolean choice: string @@ -25,14 +31,16 @@ type FormValues = { selectableCard: string disableName: boolean email: string + date: Date + time: Date + name: string + age: number + select: (typeof data)[number]['value'] + taginput: string[] } -const data = [ - { value: '1', label: '1', disabled: false }, - { value: '2', label: '2', disabled: false }, -] export const Playground: StoryFn = () => { - const methods = useForm({ + const methods = useForm({ mode: 'onChange', defaultValues: { receiveEmailUpdates: true, @@ -60,65 +68,102 @@ export const Playground: StoryFn = () => { } = methods.formState return ( - + + onSubmit={() => new Promise(rejects => { - setTimeout( - () => rejects({ 'FINAL_FORM/form-error': 'SERVER ERROR' }), - 5000, - ) + setTimeout(() => rejects('SERVER ERROR'), 5000) }) } > - + I'm disabling the field name to remove validation - - - + + + - - + + - + Selectable Card 1 - + Selectable Card 2 - + Selectable Card 3 - + - = () => { required options={data} searchable={false} + control={methods.control} /> - + - + I'd like to receive news updates - + @@ -180,7 +234,7 @@ export const Playground: StoryFn = () => { } Playground.args = { - onRawSubmit: values => { + onSubmit: values => { console.log('Submit', values) }, } diff --git a/packages/form/src/components/Form/__tests__/__snapshots__/index.test.tsx.snap b/packages/form/src/components/Form/__tests__/__snapshots__/index.test.tsx.snap index 4c21789c8b..6c2704270f 100644 --- a/packages/form/src/components/Form/__tests__/__snapshots__/index.test.tsx.snap +++ b/packages/form/src/components/Form/__tests__/__snapshots__/index.test.tsx.snap @@ -1,19 +1,5 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Form > renders correctly 1`] = ` - -
- - Test - -
-
-`; - exports[`Form > renders correctly with node children 1`] = `
renders correctly with node children 1`] = ` `; -exports[`Form > renders correctly with onRawSubmit 1`] = ` +exports[`Form > renders correctly with onSubmit 1`] = `
{ - test('renders correctly ', () => { - const { asFragment } = renderWithTheme( -
{}} errors={mockFormErrors}> - {() => 'Test'} -
, - ) - expect(asFragment()).toMatchSnapshot() - }) test('renders correctly with node children', () => { + const { result } = renderHook(() => useForm()) const { asFragment } = renderWithTheme( -
{}} errors={mockFormErrors}> + {}} + errors={mockFormErrors} + methods={result.current} + > Test
, ) expect(asFragment()).toMatchSnapshot() }) - test('renders correctly with onRawSubmit', async () => { - const onRawSubmit = vi.fn(() => {}) + test('renders correctly with onSubmit', async () => { + const onSubmit = vi.fn(() => {}) + const { result } = renderHook(() => useForm()) const { asFragment } = renderWithTheme( -
+
, ) await userEvent.click(screen.getByText('Submit')) - await waitFor(() => expect(onRawSubmit).toBeCalledTimes(1)) + await waitFor(() => expect(onSubmit).toBeCalledTimes(1)) expect(asFragment()).toMatchSnapshot() }) }) diff --git a/packages/form/src/components/Form/index.tsx b/packages/form/src/components/Form/index.tsx index d24c19d5eb..8bf0b6564a 100644 --- a/packages/form/src/components/Form/index.tsx +++ b/packages/form/src/components/Form/index.tsx @@ -1,177 +1,53 @@ import type { ReactNode } from 'react' -import React, { useMemo } from 'react' -import type { - DefaultValues, - FieldErrors, - FieldValues, - UseFormHandleSubmit, - UseFormReturn, -} from 'react-hook-form' -import { FormProvider, useForm } from 'react-hook-form' -import { FORM_ERROR } from '../../constants' +import type { FieldValues, UseFormReturn } from 'react-hook-form' +import { FormProvider } from 'react-hook-form' import { ErrorProvider } from '../../providers' import type { FormErrors } from '../../types' import { defaultErrors } from './defaultErrors' -type Without = { [P in Exclude]?: never } -type SingleXOR = T | U extends object - ? (Without & U) | (Without & T) - : T | U +type OnSubmitReturn = string | null | undefined | void -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 +type FormProps = { + children?: ReactNode errors: FormErrors name?: string - onRawSubmit: ( - data: TFormValues, - formState: { - reset: UseFormReturn['reset'] - resetFieldState: UseFormReturn['resetField'] - restart: () => void - change: UseFormReturn['setValue'] - }, - ) => Promise | OnRawSubmitReturn -} & XOR< - [ - { - /** - * @deprecated Use the `methods` prop with [useForm](https://www.react-hook-form.com/api/useform/) instead. - * - * @example - * ```tsx - * const methods = useForm({ - * defaultValues, - * mode: 'onChange' - * }) - * - * return ( - *
- * // ... - *
- * ) - * ``` - */ - initialValues?: DefaultValues - }, - { methods: UseFormReturn }, - ] -> + onSubmit: (data: TFieldValues) => Promise | OnSubmitReturn + methods: UseFormReturn +} -export const Form = ({ +export const Form = ({ children, - methods: methodsProp, - initialValues, + methods, errors, - onRawSubmit, + onSubmit, name, -}: FormProps) => { - const methodsHook = useForm({ - defaultValues: initialValues, - mode: 'onChange', - }) - - const methods = !methodsProp ? methodsHook : methodsProp - +}: FormProps) => { const handleSubmit = methods.handleSubmit(async values => { - const result = await onRawSubmit(values, { - reset: methods.reset, - resetFieldState: methods.resetField, - restart: () => methods.reset(initialValues), - change: methods.setValue, - }) + const result = await onSubmit(values) - if (result === null) { + if (result) { methods.setError('root.submit', { type: 'custom', - }) - - return - } - if (result && typeof result !== 'boolean' && typeof result !== 'number') { - methods.setError('root.submit', { - type: 'custom', - message: typeof result === 'object' ? result[FORM_ERROR] : result, + message: result, }) } }) - const formSubmitContextValue = useMemo( - () => ({ handleSubmit }), - [handleSubmit], - ) - return ( - - -
{ - e.preventDefault() - e.stopPropagation() - await handleSubmit(e) - }} - name={name} - noValidate - > - {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} -
-
-
+ +
{ + e.preventDefault() + e.stopPropagation() + await handleSubmit(e) + }} + name={name} + noValidate + > + {children} +
+
) } diff --git a/packages/form/src/components/KeyValueField/__stories__/index.stories.tsx b/packages/form/src/components/KeyValueField/__stories__/index.stories.tsx index 66de52f8f6..b327d047f1 100644 --- a/packages/form/src/components/KeyValueField/__stories__/index.stories.tsx +++ b/packages/form/src/components/KeyValueField/__stories__/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { Form, KeyValueField } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -24,7 +24,7 @@ export default { } = methods.formState return ( -
{}} errors={mockErrors} methods={methods}> + {}} errors={mockErrors} methods={methods}> diff --git a/packages/form/src/components/NumberInputField/__stories__/index.stories.tsx b/packages/form/src/components/NumberInputField/__stories__/index.stories.tsx index 62f60837f4..aab8c8b034 100644 --- a/packages/form/src/components/NumberInputField/__stories__/index.stories.tsx +++ b/packages/form/src/components/NumberInputField/__stories__/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { Form, NumberInputField } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -28,7 +28,7 @@ export default { } = methods.formState return ( - {}} errors={mockErrors} methods={methods}> + {}} errors={mockErrors} methods={methods}> diff --git a/packages/form/src/components/NumberInputField/__tests__/index.test.tsx b/packages/form/src/components/NumberInputField/__tests__/index.test.tsx index 3f75fba022..12498aa6e6 100644 --- a/packages/form/src/components/NumberInputField/__tests__/index.test.tsx +++ b/packages/form/src/components/NumberInputField/__tests__/index.test.tsx @@ -7,7 +7,7 @@ import { NumberInputField } from '../..' describe('NumberInputField', () => { test('should render correctly', () => { const { asFragment } = renderWithForm(, { - initialValues: { + defaultValues: { test: 0, }, }) @@ -18,7 +18,7 @@ describe('NumberInputField', () => { const { asFragment } = renderWithForm( , { - initialValues: { + defaultValues: { test: 10, }, }, @@ -47,7 +47,7 @@ describe('NumberInputField', () => { onBlur={onBlur} />, { - initialValues: { + defaultValues: { test: 10, }, }, @@ -84,7 +84,7 @@ describe('NumberInputField', () => { onMaxCrossed={onMaxCrossed} />, { - initialValues: { + defaultValues: { test: 10, }, }, diff --git a/packages/form/src/components/NumberInputField/index.tsx b/packages/form/src/components/NumberInputField/index.tsx index 13d0d80c45..0816274f26 100644 --- a/packages/form/src/components/NumberInputField/index.tsx +++ b/packages/form/src/components/NumberInputField/index.tsx @@ -7,8 +7,8 @@ import type { BaseFieldProps } from '../../types' type NumberInputValueFieldProps< TFieldValues extends FieldValues, - TName extends FieldPath, -> = BaseFieldProps & + TFieldName extends FieldPath, +> = BaseFieldProps & Partial< Pick< ComponentProps, @@ -32,7 +32,7 @@ type NumberInputValueFieldProps< */ export const NumberInputField = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ disabled, maxValue, @@ -47,23 +47,25 @@ export const NumberInputField = < size, step, text, - rules, className, label, shouldUnregister = false, -}: NumberInputValueFieldProps) => { + validate, + control, +}: NumberInputValueFieldProps) => { const { getError } = useErrors() const { field, fieldState: { error }, - } = useController({ + } = useController({ name, shouldUnregister, + control, rules: { max: maxValue, min: minValue, required, - ...rules, + validate, }, }) diff --git a/packages/form/src/components/NumberInputFieldV2/__stories__/index.stories.tsx b/packages/form/src/components/NumberInputFieldV2/__stories__/index.stories.tsx index 4035352980..2b9f624633 100644 --- a/packages/form/src/components/NumberInputFieldV2/__stories__/index.stories.tsx +++ b/packages/form/src/components/NumberInputFieldV2/__stories__/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { Form, NumberInputFieldV2 } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -24,7 +24,7 @@ export default { } = methods.formState return ( - {}} errors={mockErrors} methods={methods}> + {}} errors={mockErrors} methods={methods}>
{ renderWithTheme( onSubmit(value)} + onSubmit={value => onSubmit(value)} errors={mockFormErrors} methods={result.current} > diff --git a/packages/form/src/components/NumberInputFieldV2/index.tsx b/packages/form/src/components/NumberInputFieldV2/index.tsx index 1023563515..bff0467706 100644 --- a/packages/form/src/components/NumberInputFieldV2/index.tsx +++ b/packages/form/src/components/NumberInputFieldV2/index.tsx @@ -7,8 +7,8 @@ import type { BaseFieldProps } from '../../types' type NumberInputV2Props< TFieldValues extends FieldValues, - TName extends FieldPath, -> = BaseFieldProps & + TFieldName extends FieldPath, +> = BaseFieldProps & Partial< Pick< ComponentProps, @@ -39,15 +39,14 @@ type NumberInputV2Props< > > & { className?: string - name: string - required?: boolean } export const NumberInputFieldV2 = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ disabled, + control, max = Number.MAX_SAFE_INTEGER, min = 0, name, @@ -64,7 +63,6 @@ export const NumberInputFieldV2 = < placeholder, success, helper, - rules, controls = true, 'aria-label': ariaLabel, 'data-testid': dataTestId, @@ -72,19 +70,21 @@ export const NumberInputFieldV2 = < autoFocus, readOnly, shouldUnregister = false, -}: NumberInputV2Props) => { + validate, +}: NumberInputV2Props) => { const { getError } = useErrors() const { field, fieldState: { error }, - } = useController({ + } = useController({ name, + control, shouldUnregister, rules: { max, min, required, - ...rules, + validate, }, }) diff --git a/packages/form/src/components/RadioField/__stories__/Checked.stories.tsx b/packages/form/src/components/RadioField/__stories__/Checked.stories.tsx index e27dca3c9e..91a18b2da4 100644 --- a/packages/form/src/components/RadioField/__stories__/Checked.stories.tsx +++ b/packages/form/src/components/RadioField/__stories__/Checked.stories.tsx @@ -1,10 +1,15 @@ import type { StoryFn } from '@storybook/react' import { RadioField } from '..' +import { useForm } from '../../..' import type { FormErrors } from '../../../types' import { Form } from '../../Form' -export const Checked: StoryFn<{ errors: FormErrors }> = ({ errors }) => ( - {}} errors={errors} initialValues={{ foo: 'bar' }}> - - -) +export const Checked: StoryFn<{ errors: FormErrors }> = ({ errors }) => { + const methods = useForm({ defaultValues: { foo: 'bar' } }) + + return ( +
{}} errors={errors} methods={methods}> + + + ) +} diff --git a/packages/form/src/components/RadioField/__stories__/index.stories.tsx b/packages/form/src/components/RadioField/__stories__/index.stories.tsx index d60d6dd72e..0fc3c53ac1 100644 --- a/packages/form/src/components/RadioField/__stories__/index.stories.tsx +++ b/packages/form/src/components/RadioField/__stories__/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { Form, RadioField } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -28,7 +28,7 @@ export default { } = methods.formState return ( -
{}} errors={mockErrors} methods={methods}> + {}} errors={mockErrors} methods={methods}> diff --git a/packages/form/src/components/RadioField/__tests__/index.test.tsx b/packages/form/src/components/RadioField/__tests__/index.test.tsx index 3d27f06a12..0e10dc7de7 100644 --- a/packages/form/src/components/RadioField/__tests__/index.test.tsx +++ b/packages/form/src/components/RadioField/__tests__/index.test.tsx @@ -28,7 +28,7 @@ describe('RadioField', () => { test('should render correctly checked', () => { const { asFragment } = renderWithForm( , - { initialValues: { test: 'checked' } }, + { defaultValues: { test: 'checked' } }, ) const input = screen.getByRole('radio', { hidden: true }) expect(input).toBeChecked() diff --git a/packages/form/src/components/RadioField/index.tsx b/packages/form/src/components/RadioField/index.tsx index f0a18ce9bf..989c68e7d1 100644 --- a/packages/form/src/components/RadioField/index.tsx +++ b/packages/form/src/components/RadioField/index.tsx @@ -7,8 +7,8 @@ import type { BaseFieldProps } from '../../types' type RadioFieldProps< TFieldValues extends FieldValues, - TName extends FieldPath, -> = Omit, 'label'> & + TFieldName extends FieldPath, +> = Omit, 'label'> & Partial< Pick< ComponentProps, @@ -29,9 +29,10 @@ type RadioFieldProps< */ export const RadioField = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ className, + control, 'data-testid': dataTestId, disabled, id, @@ -42,20 +43,21 @@ export const RadioField = < onFocus, required, value, - rules, tooltip, shouldUnregister = false, -}: RadioFieldProps) => { + validate, +}: RadioFieldProps) => { const { getError } = useErrors() const { field, fieldState: { error }, - } = useController({ + } = useController({ name, + control, shouldUnregister, rules: { required, - ...rules, + validate, }, }) diff --git a/packages/form/src/components/RadioGroupField/__stories__/index.stories.tsx b/packages/form/src/components/RadioGroupField/__stories__/index.stories.tsx index 94f613323d..0fddfca3d1 100644 --- a/packages/form/src/components/RadioGroupField/__stories__/index.stories.tsx +++ b/packages/form/src/components/RadioGroupField/__stories__/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { Form, RadioGroupField } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -25,7 +25,7 @@ export default { } = methods.formState return ( - {}} errors={mockErrors} methods={methods}> + {}} errors={mockErrors} methods={methods}> diff --git a/packages/form/src/components/RadioGroupField/index.tsx b/packages/form/src/components/RadioGroupField/index.tsx index 7feabfb54c..9d37b3d840 100644 --- a/packages/form/src/components/RadioGroupField/index.tsx +++ b/packages/form/src/components/RadioGroupField/index.tsx @@ -7,8 +7,8 @@ import type { BaseFieldProps } from '../../types' type RadioGroupFieldProps< TFieldValues extends FieldValues, - TName extends FieldPath, -> = BaseFieldProps & + TFieldName extends FieldPath, +> = BaseFieldProps & Partial< Pick< ComponentProps, @@ -20,31 +20,33 @@ type RadioGroupFieldProps< export const RadioGroupField = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ className, + control, legend = '', name, onChange, required, - rules, children, label = '', error: customError, helper, direction, shouldUnregister = false, -}: RadioGroupFieldProps): JSX.Element => { + validate, +}: RadioGroupFieldProps): JSX.Element => { const { getError } = useErrors() const { field, fieldState: { error }, - } = useController({ + } = useController({ name, + control, shouldUnregister, rules: { required, - ...rules, + validate, }, }) diff --git a/packages/form/src/components/SelectInputField/__stories__/index.stories.tsx b/packages/form/src/components/SelectInputField/__stories__/index.stories.tsx index 1fb8971eed..b2f919eea0 100644 --- a/packages/form/src/components/SelectInputField/__stories__/index.stories.tsx +++ b/packages/form/src/components/SelectInputField/__stories__/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { Form, SelectInputField } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -24,7 +24,7 @@ export default { } = methods.formState return ( - {}} errors={mockErrors} methods={methods}> + {}} errors={mockErrors} methods={methods}> diff --git a/packages/form/src/components/SelectInputField/index.tsx b/packages/form/src/components/SelectInputField/index.tsx index 0ad8b9f28f..dfe06db604 100644 --- a/packages/form/src/components/SelectInputField/index.tsx +++ b/packages/form/src/components/SelectInputField/index.tsx @@ -102,8 +102,8 @@ type SelectInputOption = { value: string; label: string } */ export type SelectInputFieldProps< TFieldValues extends FieldValues, - TName extends FieldPath, -> = Omit, 'onChange'> & + TFieldName extends FieldPath, +> = Omit, 'onChange'> & Partial< Pick< SelectInputProps, @@ -146,7 +146,7 @@ const identity = (x: unknown) => x export const SelectInputField = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ animation, animationDuration, @@ -176,13 +176,13 @@ export const SelectInputField = < placeholder, readOnly, required, - rules, noTopLabel, emptyState, customStyle, shouldUnregister = false, 'data-testid': dataTestId, -}: SelectInputFieldProps) => { + validate, +}: SelectInputFieldProps) => { const options = useMemo( () => optionsProp || @@ -253,14 +253,14 @@ export const SelectInputField = < const { field, fieldState: { error }, - } = useController({ + } = useController({ name, shouldUnregister, rules: { required, minLength: minLength || required ? 1 : undefined, maxLength, - ...rules, + validate, }, }) diff --git a/packages/form/src/components/SelectInputFieldV2/__stories__/index.stories.tsx b/packages/form/src/components/SelectInputFieldV2/__stories__/index.stories.tsx index 6f85212406..b2b0ad99eb 100644 --- a/packages/form/src/components/SelectInputFieldV2/__stories__/index.stories.tsx +++ b/packages/form/src/components/SelectInputFieldV2/__stories__/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { Form, SelectInputFieldV2 } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -25,7 +25,7 @@ export default { return ( {}} + onSubmit={() => {}} errors={mockErrors} methods={methods} name="SelectInput" diff --git a/packages/form/src/components/SelectInputFieldV2/index.tsx b/packages/form/src/components/SelectInputFieldV2/index.tsx index e1f73234f2..26c4bbc931 100644 --- a/packages/form/src/components/SelectInputFieldV2/index.tsx +++ b/packages/form/src/components/SelectInputFieldV2/index.tsx @@ -7,8 +7,8 @@ import type { BaseFieldProps } from '../../types' type SelectInputFieldV2Props< TFieldValues extends FieldValues, - TName extends FieldPath, -> = BaseFieldProps & + TFieldName extends FieldPath, +> = BaseFieldProps & Pick< ComponentProps, | 'placeholder' @@ -45,7 +45,7 @@ type SelectInputFieldV2Props< export const SelectInputFieldV2 = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ autofocus, className, @@ -78,18 +78,20 @@ export const SelectInputFieldV2 = < name, 'aria-label': ariaLabel, optionalInfoPlacement, - rules, shouldUnregister = false, -}: SelectInputFieldV2Props) => { + control, + validate, +}: SelectInputFieldV2Props) => { const { field, fieldState: { error }, - } = useController({ + } = useController({ name, + control, shouldUnregister, rules: { required, - ...rules, + validate, }, }) @@ -99,7 +101,7 @@ export const SelectInputFieldV2 = < typeof SelectInputV2 >['onChange'] = useCallback( (value: string | string[]) => { - onChange?.(value as PathValue) + onChange?.(value as PathValue) field.onChange(value) }, [onChange, field], diff --git a/packages/form/src/components/SelectableCardField/__stories__/Checked.stories.tsx b/packages/form/src/components/SelectableCardField/__stories__/Checked.stories.tsx index 461f20e089..bde72a7869 100644 --- a/packages/form/src/components/SelectableCardField/__stories__/Checked.stories.tsx +++ b/packages/form/src/components/SelectableCardField/__stories__/Checked.stories.tsx @@ -1,18 +1,23 @@ import type { StoryFn } from '@storybook/react' import { Stack } from '@ultraviolet/ui' import { SelectableCardField } from '..' +import { useForm } from '../../..' import type { FormErrors } from '../../../types' import { Form } from '../../Form' -export const Checked: StoryFn<{ errors: FormErrors }> = ({ errors }) => ( - {}} errors={errors} initialValues={{ foo: 'bar' }}> - - - Radio left - - - Radio right - - - -) +export const Checked: StoryFn<{ errors: FormErrors }> = ({ errors }) => { + const methods = useForm({ defaultValues: { foo: 'bar' } }) + + return ( +
{}} errors={errors} methods={methods}> + + + Radio left + + + Radio right + + +
+ ) +} diff --git a/packages/form/src/components/SelectableCardField/__stories__/index.stories.tsx b/packages/form/src/components/SelectableCardField/__stories__/index.stories.tsx index e502f0bc1a..ad690c8f8f 100644 --- a/packages/form/src/components/SelectableCardField/__stories__/index.stories.tsx +++ b/packages/form/src/components/SelectableCardField/__stories__/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { Form, SelectableCardField } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -24,7 +24,7 @@ export default { } = methods.formState return ( -
{}} errors={mockErrors} methods={methods}> + {}} errors={mockErrors} methods={methods}> diff --git a/packages/form/src/components/SelectableCardField/__tests__/index.test.tsx b/packages/form/src/components/SelectableCardField/__tests__/index.test.tsx index 7a83cccdc4..53428ea2d3 100644 --- a/packages/form/src/components/SelectableCardField/__tests__/index.test.tsx +++ b/packages/form/src/components/SelectableCardField/__tests__/index.test.tsx @@ -30,7 +30,7 @@ describe('SelectableCardField', () => { Radio field checked , - { initialValues: { test: 'checked' } }, + { defaultValues: { test: 'checked' } }, ) const input = screen.getByRole('radio', { hidden: true }) expect(input).toBeChecked() diff --git a/packages/form/src/components/SelectableCardField/index.tsx b/packages/form/src/components/SelectableCardField/index.tsx index f137e00ea5..5c0d286a10 100644 --- a/packages/form/src/components/SelectableCardField/index.tsx +++ b/packages/form/src/components/SelectableCardField/index.tsx @@ -6,8 +6,8 @@ import type { BaseFieldProps } from '../../types' type SelectableCardFieldProps< TFieldValues extends FieldValues, - TName extends FieldPath, -> = Omit, 'label' | 'onChange'> & + TFieldName extends FieldPath, +> = Omit, 'label' | 'onChange'> & Partial< Pick< ComponentProps, @@ -29,9 +29,10 @@ type SelectableCardFieldProps< export const SelectableCardField = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ name, + control, value, onChange, showTick, @@ -45,19 +46,20 @@ export const SelectableCardField = < tooltip, id, label, - rules, shouldUnregister = false, + validate, 'data-testid': dataTestId, -}: SelectableCardFieldProps) => { +}: SelectableCardFieldProps) => { const { field, fieldState: { error }, - } = useController({ + } = useController({ name, + control, shouldUnregister, rules: { required, - ...rules, + validate, }, }) diff --git a/packages/form/src/components/SelectableCardGroupField/__stories__/index.stories.tsx b/packages/form/src/components/SelectableCardGroupField/__stories__/index.stories.tsx index 7b3656ca8d..0e98237d58 100644 --- a/packages/form/src/components/SelectableCardGroupField/__stories__/index.stories.tsx +++ b/packages/form/src/components/SelectableCardGroupField/__stories__/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { Form, SelectableCardGroupField } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -27,7 +27,7 @@ export default { } = methods.formState return ( - {}} errors={mockErrors} methods={methods}> + {}} errors={mockErrors} methods={methods}> diff --git a/packages/form/src/components/SelectableCardGroupField/__tests__/index.test.tsx b/packages/form/src/components/SelectableCardGroupField/__tests__/index.test.tsx index 39e73663db..ebd669190a 100644 --- a/packages/form/src/components/SelectableCardGroupField/__tests__/index.test.tsx +++ b/packages/form/src/components/SelectableCardGroupField/__tests__/index.test.tsx @@ -33,7 +33,7 @@ describe('SelectableCardField', () => { data-testid="checked" /> , - { initialValues: { test: 'checked' } }, + { defaultValues: { test: 'checked' } }, ) const input = screen.getByLabelText('Radio 1') expect(input).toBeChecked() @@ -54,7 +54,7 @@ describe('SelectableCardField', () => { data-testid="checked" /> , - { initialValues: { test: 'checked' } }, + { defaultValues: { test: 'checked' } }, ) const input = screen.getByLabelText('Checkbox 1') expect(input).toBeChecked() diff --git a/packages/form/src/components/SelectableCardGroupField/index.tsx b/packages/form/src/components/SelectableCardGroupField/index.tsx index e0d15d66e4..5e80f34507 100644 --- a/packages/form/src/components/SelectableCardGroupField/index.tsx +++ b/packages/form/src/components/SelectableCardGroupField/index.tsx @@ -7,9 +7,9 @@ import type { BaseFieldProps } from '../../types' type SelectableCardGroupFieldProps< TFieldValues extends FieldValues, - TName extends FieldPath, -> = BaseFieldProps & - Omit, 'label' | 'onChange'> & + TFieldName extends FieldPath, +> = BaseFieldProps & + Omit, 'label' | 'onChange'> & Partial< Pick< ComponentProps, @@ -27,14 +27,14 @@ type SelectableCardGroupFieldProps< export const SelectableCardGroupField = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ className, legend, + control, name, onChange, required = false, - rules, children, label = '', error: customError, @@ -43,17 +43,19 @@ export const SelectableCardGroupField = < showTick, type = 'radio', shouldUnregister = false, -}: SelectableCardGroupFieldProps): JSX.Element => { + validate, +}: SelectableCardGroupFieldProps): JSX.Element => { const { getError } = useErrors() const { field, fieldState: { error }, - } = useController({ + } = useController({ name, + control, shouldUnregister, rules: { required, - ...rules, + validate, }, }) diff --git a/packages/form/src/components/Submit/__stories__/Invalid.stories.tsx b/packages/form/src/components/Submit/__stories__/Invalid.stories.tsx index 0b09f5afed..928574e2d3 100644 --- a/packages/form/src/components/Submit/__stories__/Invalid.stories.tsx +++ b/packages/form/src/components/Submit/__stories__/Invalid.stories.tsx @@ -1,12 +1,17 @@ import type { StoryFn } from '@storybook/react' import { Submit } from '..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' import { Form } from '../../Form' import { TextInputField } from '../../TextInputField' -export const Invalid: StoryFn = () => ( - {}} errors={mockErrors}> - - This form is invalid - -) +export const Invalid: StoryFn = () => { + const methods = useForm() + + return ( +
{}} errors={mockErrors} methods={methods}> + + This form is invalid + + ) +} diff --git a/packages/form/src/components/Submit/__stories__/Playground.stories.tsx b/packages/form/src/components/Submit/__stories__/Playground.stories.tsx index 0ec0a2ce39..03f49ef619 100644 --- a/packages/form/src/components/Submit/__stories__/Playground.stories.tsx +++ b/packages/form/src/components/Submit/__stories__/Playground.stories.tsx @@ -1,14 +1,19 @@ import type { StoryFn } from '@storybook/react' import type { ComponentProps } from 'react' import { Submit } from '..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' import { Form } from '../../Form' export const Playground: StoryFn> = ({ children, ...props -}) => ( -
{}} errors={mockErrors}> - This form is ready to submit -
-) +}) => { + const methods = useForm() + + return ( +
{}} errors={mockErrors} methods={methods}> + This form is ready to submit +
+ ) +} diff --git a/packages/form/src/components/Submit/__stories__/Submitting.stories.tsx b/packages/form/src/components/Submit/__stories__/Submitting.stories.tsx index 978ee9a513..70bcb79563 100644 --- a/packages/form/src/components/Submit/__stories__/Submitting.stories.tsx +++ b/packages/form/src/components/Submit/__stories__/Submitting.stories.tsx @@ -1,21 +1,27 @@ import type { StoryFn } from '@storybook/react' import { Submit } from '..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' import { Form } from '../../Form' -export const Submitting: StoryFn = () => ( -
- new Promise(resolve => { - setTimeout(() => resolve(undefined), 5000) - }) - } - > - {({ submitting }) => ( +export const Submitting: StoryFn = () => { + const methods = useForm() + + const { isSubmitting } = methods.formState + + return ( + + new Promise(resolve => { + setTimeout(() => resolve(undefined), 5000) + }) + } + methods={methods} + > - {submitting ? 'This form is submitting' : 'Click to submit for 5sec'} + {isSubmitting ? 'This form is submitting' : 'Click to submit for 5sec'} - )} -
-) + + ) +} diff --git a/packages/form/src/components/Submit/__tests__/index.test.tsx b/packages/form/src/components/Submit/__tests__/index.test.tsx index 316a08cc0c..dc3b8f07ec 100644 --- a/packages/form/src/components/Submit/__tests__/index.test.tsx +++ b/packages/form/src/components/Submit/__tests__/index.test.tsx @@ -27,18 +27,22 @@ describe('Submit', () => { Test , - { initialValues: { toto: '4' } }, + { defaultValues: { toto: '4' } }, ) expect(asFragment()).toMatchSnapshot() }) test('form is submitting', async () => { - const { asFragment } = renderWithForm(Test, { - onRawSubmit: () => - new Promise(resolve => { - setTimeout(() => resolve(undefined), 5000) - }), - }) + const { asFragment } = renderWithForm( + Test, + {}, + { + onSubmit: () => + new Promise(resolve => { + setTimeout(() => resolve(undefined), 500) + }), + }, + ) await userEvent.click( // eslint-disable-next-line testing-library/no-node-access screen.getByText('Test').closest('button') as HTMLButtonElement, diff --git a/packages/form/src/components/SubmitErrorAlert/__stories__/Playground.stories.tsx b/packages/form/src/components/SubmitErrorAlert/__stories__/Playground.stories.tsx index 405efcdf26..5e79c2c686 100644 --- a/packages/form/src/components/SubmitErrorAlert/__stories__/Playground.stories.tsx +++ b/packages/form/src/components/SubmitErrorAlert/__stories__/Playground.stories.tsx @@ -1,20 +1,25 @@ import type { StoryFn } from '@storybook/react' import { Stack } from '@ultraviolet/ui' import type { ComponentProps } from 'react' -import { FORM_ERROR, Submit, SubmitErrorAlert } from '../../..' +import { Submit, SubmitErrorAlert, useForm } from '../../..' import { mockErrors } from '../../../mocks' import { Form } from '../../Form' export const Playground: StoryFn< ComponentProps -> = () => ( -
({ [FORM_ERROR]: 'An error occurred' })} - > - - - Click Me - -
-) +> = () => { + const methods = useForm() + + return ( +
'An error occurred'} + methods={methods} + > + + + Click Me + +
+ ) +} diff --git a/packages/form/src/components/SubmitErrorAlert/__tests__/index.test.tsx b/packages/form/src/components/SubmitErrorAlert/__tests__/index.test.tsx index 63ceb628e4..11aa3c78b4 100644 --- a/packages/form/src/components/SubmitErrorAlert/__tests__/index.test.tsx +++ b/packages/form/src/components/SubmitErrorAlert/__tests__/index.test.tsx @@ -2,9 +2,7 @@ import { screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { renderWithForm } from '@utils/test' import { describe, expect, test, vi } from 'vitest' -import { Form, Submit, SubmitErrorAlert } from '../..' -import { FORM_ERROR } from '../../../constants' -import { mockErrors } from '../../../mocks' +import { Submit, SubmitErrorAlert } from '../..' describe('SubmitErrorAlert', () => { test('should render nothing if no error', () => { @@ -13,14 +11,15 @@ describe('SubmitErrorAlert', () => { }) test('should display an alert when submitError is present', async () => { - const onSubmit = vi.fn(() => ({ [FORM_ERROR]: 'hello' })) + const onSubmit = vi.fn(() => 'hello') const { asFragment } = renderWithForm( -
+ <> Submit , - , - { onRawSubmit: onSubmit }, + , + {}, + { onSubmit }, ) await userEvent.click( // eslint-disable-next-line testing-library/no-node-access diff --git a/packages/form/src/components/TagInputField/__stories__/index.stories.tsx b/packages/form/src/components/TagInputField/__stories__/index.stories.tsx index 5b3e9d09ba..6a4874b109 100644 --- a/packages/form/src/components/TagInputField/__stories__/index.stories.tsx +++ b/packages/form/src/components/TagInputField/__stories__/index.stories.tsx @@ -1,8 +1,8 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { TagInputField } from '..' import { Form } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -27,7 +27,7 @@ export default { } = methods.formState return ( -
{}} errors={mockErrors} methods={methods}> + {}} errors={mockErrors} methods={methods}> diff --git a/packages/form/src/components/TagInputField/__tests__/__snapshots__/index.test.tsx.snap b/packages/form/src/components/TagInputField/__tests__/__snapshots__/index.test.tsx.snap index c4955725f8..b04f34451e 100644 --- a/packages/form/src/components/TagInputField/__tests__/__snapshots__/index.test.tsx.snap +++ b/packages/form/src/components/TagInputField/__tests__/__snapshots__/index.test.tsx.snap @@ -162,7 +162,7 @@ exports[`TagInputField > should render correctly 1`] = ` `; -exports[`TagInputField > should works with initialValues 1`] = ` +exports[`TagInputField > should works with defaultValues 1`] = ` .emotion-0 { display: -webkit-box; diff --git a/packages/form/src/components/TagInputField/__tests__/index.test.tsx b/packages/form/src/components/TagInputField/__tests__/index.test.tsx index d7d6739ca9..3e49ed3e8f 100644 --- a/packages/form/src/components/TagInputField/__tests__/index.test.tsx +++ b/packages/form/src/components/TagInputField/__tests__/index.test.tsx @@ -14,14 +14,14 @@ describe('TagInputField', () => { expect(asFragment()).toMatchSnapshot() }) - test('should works with initialValues', async () => { + test('should works with defaultValues', async () => { const onSubmit = vi.fn<[{ test: string[] }], void>() const { result } = renderHook(() => useForm<{ test: string[] }>({ defaultValues: { test: ['First'] } }), ) const { asFragment } = renderWithTheme( - + Submit , diff --git a/packages/form/src/components/TagInputField/index.tsx b/packages/form/src/components/TagInputField/index.tsx index 05ab864eca..79234e9f89 100644 --- a/packages/form/src/components/TagInputField/index.tsx +++ b/packages/form/src/components/TagInputField/index.tsx @@ -7,8 +7,8 @@ import type { BaseFieldProps } from '../../types' export type TagInputFieldProps< TFieldValues extends FieldValues, - TName extends FieldPath, -> = BaseFieldProps & + TFieldName extends FieldPath, +> = BaseFieldProps & Partial< Pick< ComponentProps, @@ -30,16 +30,16 @@ export type TagInputFieldProps< export const TagInputField = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ className, disabled, id, + control, name, onChange, placeholder, required, - rules, variant, shouldUnregister = false, 'data-testid': dataTestId, @@ -50,17 +50,19 @@ export const TagInputField = < success, readOnly, tooltip, -}: TagInputFieldProps) => { + validate, +}: TagInputFieldProps) => { const { getError } = useErrors() const { field, fieldState: { error }, - } = useController({ + } = useController({ name, + control, rules: { required, shouldUnregister, - ...rules, + validate, }, }) diff --git a/packages/form/src/components/TextAreaField/__stories__/Template.stories.tsx b/packages/form/src/components/TextAreaField/__stories__/Template.stories.tsx index 70322a1200..6ae987d04c 100644 --- a/packages/form/src/components/TextAreaField/__stories__/Template.stories.tsx +++ b/packages/form/src/components/TextAreaField/__stories__/Template.stories.tsx @@ -1,12 +1,11 @@ import type { StoryFn } from '@storybook/react' import type { ComponentProps } from 'react' import { TextAreaField } from '..' -import { Submit, TextInputField } from '../..' +import { Submit } from '../..' export const Template: StoryFn> = args => (
- Submit
) diff --git a/packages/form/src/components/TextAreaField/__stories__/index.stories.tsx b/packages/form/src/components/TextAreaField/__stories__/index.stories.tsx index 3a799f0daa..6194efcc1f 100644 --- a/packages/form/src/components/TextAreaField/__stories__/index.stories.tsx +++ b/packages/form/src/components/TextAreaField/__stories__/index.stories.tsx @@ -1,22 +1,25 @@ import type { Meta } from '@storybook/react' import { TextAreaField } from '..' import { Form } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { component: TextAreaField, decorators: [ - ChildStory => ( -
{}} - errors={mockErrors} - initialValues={{ + ChildStory => { + const methods = useForm({ + defaultValues: { textarea: 'A long time ago in a galaxy far, far away', - }} - > - - - ), + }, + }) + + return ( +
{}} errors={mockErrors} methods={methods}> + + + ) + }, ], title: 'Form/Components/Fields/TextAreaField', } as Meta diff --git a/packages/form/src/components/TextAreaField/__tests__/index.test.tsx b/packages/form/src/components/TextAreaField/__tests__/index.test.tsx index 699c1921b8..45583cdec9 100644 --- a/packages/form/src/components/TextAreaField/__tests__/index.test.tsx +++ b/packages/form/src/components/TextAreaField/__tests__/index.test.tsx @@ -1,6 +1,7 @@ -import { screen, waitFor } from '@testing-library/react' +import { renderHook, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { mockFormErrors, renderWithForm, renderWithTheme } from '@utils/test' +import { useForm } from 'react-hook-form' import { describe, expect, test, vi } from 'vitest' import { TextAreaField } from '..' import { Submit } from '../..' @@ -16,9 +17,14 @@ describe('TextAreaField', () => { test('should render correctly generated', async () => { const onSubmit = vi.fn<[values: { test: string }], void>() + const { result } = renderHook(() => useForm<{ test: string }>()) const { asFragment } = renderWithTheme( -
+ Submit , diff --git a/packages/form/src/components/TextAreaField/index.tsx b/packages/form/src/components/TextAreaField/index.tsx index de4fadae4f..7ea693c764 100644 --- a/packages/form/src/components/TextAreaField/index.tsx +++ b/packages/form/src/components/TextAreaField/index.tsx @@ -4,11 +4,12 @@ import type { FieldPath, FieldValues, Path, PathValue } from 'react-hook-form' import { useController } from 'react-hook-form' import { useErrors } from '../../providers' import type { BaseFieldProps } from '../../types' +import { validateRegex } from '../../utils/validateRegex' export type TextAreaFieldProps< TFieldValues extends FieldValues, - TName extends FieldPath, -> = BaseFieldProps & + TFieldName extends FieldPath, +> = BaseFieldProps & Omit< ComponentProps, 'value' | 'error' | 'name' | 'onChange' @@ -21,12 +22,13 @@ export type TextAreaFieldProps< */ export const TextAreaField = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ autoFocus, clearable, className, tabIndex, + control, 'data-testid': dataTestId, disabled, helper, @@ -44,30 +46,23 @@ export const TextAreaField = < rows, success, tooltip, - validate, regex: regexes, -}: TextAreaFieldProps) => { + validate, +}: TextAreaFieldProps) => { const { getError } = useErrors() const { field, fieldState: { error }, - } = useController({ + } = useController({ name, + control, rules: { required, validate: { ...(regexes ? { - pattern: value => - regexes.every( - regex => - value === undefined || - value === '' || - (Array.isArray(regex) - ? regex.some(regexOr => regexOr.test(value)) - : regex.test(value)), - ), + pattern: value => validateRegex(value, regexes), } : {}), ...validate, diff --git a/packages/form/src/components/TextInputField/__stories__/index.stories.tsx b/packages/form/src/components/TextInputField/__stories__/index.stories.tsx index 05dcdd25a4..303ab70e83 100644 --- a/packages/form/src/components/TextInputField/__stories__/index.stories.tsx +++ b/packages/form/src/components/TextInputField/__stories__/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { Form, TextInputField } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -24,7 +24,7 @@ export default { } = methods.formState return ( -
{}} errors={mockErrors} methods={methods}> + {}} errors={mockErrors} methods={methods}> diff --git a/packages/form/src/components/TextInputField/__tests__/index.test.tsx b/packages/form/src/components/TextInputField/__tests__/index.test.tsx index 7b023df398..e641a532f1 100644 --- a/packages/form/src/components/TextInputField/__tests__/index.test.tsx +++ b/packages/form/src/components/TextInputField/__tests__/index.test.tsx @@ -59,7 +59,7 @@ describe('TextInputField', () => { const { asFragment } = renderWithForm( , { - initialValues: { + defaultValues: { test: null, }, }, diff --git a/packages/form/src/components/TextInputField/index.tsx b/packages/form/src/components/TextInputField/index.tsx index 0210cb5881..7874cbf03f 100644 --- a/packages/form/src/components/TextInputField/index.tsx +++ b/packages/form/src/components/TextInputField/index.tsx @@ -4,11 +4,12 @@ import type { FieldPath, FieldValues, Path, PathValue } from 'react-hook-form' import { useController } from 'react-hook-form' import { useErrors } from '../../providers' import type { BaseFieldProps } from '../../types' +import { validateRegex } from '../../utils/validateRegex' type TextInputFieldProps< TFieldValues extends FieldValues, - TName extends FieldPath, -> = BaseFieldProps & + TFieldName extends FieldPath, +> = BaseFieldProps & Partial< Pick< ComponentProps, @@ -61,7 +62,7 @@ type TextInputFieldProps< */ export const TextInputField = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ autoCapitalize, autoComplete, @@ -93,54 +94,46 @@ export const TextInputField = < type, unit, size, - rules, valid, parse, format, formatOnBlur = false, regex: regexes, - min, - max, - minLength, - maxLength, - validate, defaultValue, customError, innerRef, shouldUnregister = false, 'data-testid': dataTestId, -}: TextInputFieldProps) => { + validate, + min, + max, + minLength, + maxLength, + control, +}: TextInputFieldProps) => { const { getError } = useErrors() const { field, fieldState: { error }, - } = useController({ + } = useController({ name, defaultValue, shouldUnregister, + control, rules: { required, + minLength, + maxLength, + min, + max, validate: { ...(regexes ? { - pattern: value => - regexes.every( - regex => - value === undefined || - value === '' || - (Array.isArray(regex) - ? regex.some(regexOr => regexOr.test(value)) - : regex.test(value)), - ), + pattern: value => validateRegex(value, regexes), } : {}), ...validate, }, - minLength, - maxLength, - max, - min, - ...rules, }, }) diff --git a/packages/form/src/components/TextInputFieldV2/__stories__/index.stories.tsx b/packages/form/src/components/TextInputFieldV2/__stories__/index.stories.tsx index 6cf60fa4e4..d673bca7f6 100644 --- a/packages/form/src/components/TextInputFieldV2/__stories__/index.stories.tsx +++ b/packages/form/src/components/TextInputFieldV2/__stories__/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { Form, TextInputFieldV2 } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -24,7 +24,7 @@ export default { } = methods.formState return ( - {}} errors={mockErrors} methods={methods}> + {}} errors={mockErrors} methods={methods}> diff --git a/packages/form/src/components/TextInputFieldV2/__tests__/index.test.tsx b/packages/form/src/components/TextInputFieldV2/__tests__/index.test.tsx index b5512b333c..08f368c774 100644 --- a/packages/form/src/components/TextInputFieldV2/__tests__/index.test.tsx +++ b/packages/form/src/components/TextInputFieldV2/__tests__/index.test.tsx @@ -23,7 +23,7 @@ describe('TextInputFieldV2', () => { const { asFragment } = renderWithTheme( diff --git a/packages/form/src/components/TextInputFieldV2/index.tsx b/packages/form/src/components/TextInputFieldV2/index.tsx index 8bc68b15be..302af2a3b8 100644 --- a/packages/form/src/components/TextInputFieldV2/index.tsx +++ b/packages/form/src/components/TextInputFieldV2/index.tsx @@ -4,11 +4,12 @@ import type { FieldPath, FieldValues, Path, PathValue } from 'react-hook-form' import { useController } from 'react-hook-form' import { useErrors } from '../../providers' import type { BaseFieldProps } from '../../types' +import { validateRegex } from '../../utils/validateRegex' type TextInputFieldProps< TFieldValues extends FieldValues, - TName extends FieldPath, -> = BaseFieldProps & + TFieldName extends FieldPath, +> = BaseFieldProps & Omit< ComponentProps, 'value' | 'error' | 'name' | 'onChange' @@ -21,9 +22,8 @@ type TextInputFieldProps< */ export const TextInputField = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ - validate, regex: regexes, id, className, @@ -56,29 +56,24 @@ export const TextInputField = < 'aria-label': ariaLabel, autoComplete, shouldUnregister, -}: TextInputFieldProps) => { + validate, + control, +}: TextInputFieldProps) => { const { getError } = useErrors() const { field, fieldState: { error }, - } = useController({ + } = useController({ name, shouldUnregister, + control, rules: { required, validate: { ...(regexes ? { - pattern: value => - regexes.every( - regex => - value === undefined || - value === '' || - (Array.isArray(regex) - ? regex.some(regexOr => regexOr.test(value)) - : regex.test(value)), - ), + pattern: value => validateRegex(value, regexes), } : {}), ...validate, diff --git a/packages/form/src/components/TimeField/__stories__/index.stories.tsx b/packages/form/src/components/TimeField/__stories__/index.stories.tsx index 4da2f7d7e4..eb9a1d4948 100644 --- a/packages/form/src/components/TimeField/__stories__/index.stories.tsx +++ b/packages/form/src/components/TimeField/__stories__/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { Form, TimeField } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -24,7 +24,7 @@ export default { } = methods.formState return ( - {}} errors={mockErrors} methods={methods}> + {}} errors={mockErrors} methods={methods}> diff --git a/packages/form/src/components/TimeField/index.tsx b/packages/form/src/components/TimeField/index.tsx index a80de58aed..4e94651f02 100644 --- a/packages/form/src/components/TimeField/index.tsx +++ b/packages/form/src/components/TimeField/index.tsx @@ -18,18 +18,17 @@ const parseTime = (date?: Date | string): { label: string; value: string } => { type TimeFieldProps< TFieldValues extends FieldValues, - TName extends FieldPath, -> = BaseFieldProps & - Omit, 'onChange'> & { - name: string - } + TFieldName extends FieldPath, +> = BaseFieldProps & + Omit, 'onChange'> export const TimeField = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ required, name, + control, schedule, placeholder, disabled, @@ -46,21 +45,22 @@ export const TimeField = < animationOnChange, className, isSearchable, - rules, options, 'data-testid': dataTestId, shouldUnregister = false, noTopLabel, -}: TimeFieldProps) => { + validate, +}: TimeFieldProps) => { const { field, fieldState: { error }, - } = useController({ + } = useController({ name, + control, shouldUnregister, rules: { required, - ...rules, + validate, }, }) diff --git a/packages/form/src/components/ToggleField/__stories__/index.stories.tsx b/packages/form/src/components/ToggleField/__stories__/index.stories.tsx index 504d6ebb38..7151bedf9a 100644 --- a/packages/form/src/components/ToggleField/__stories__/index.stories.tsx +++ b/packages/form/src/components/ToggleField/__stories__/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { Form, ToggleField } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -28,13 +28,7 @@ export default { } = methods.formState return ( - {}} - errors={mockErrors} - initialValues={{ - checked: true, - }} - > + {}} errors={mockErrors} methods={methods}> diff --git a/packages/form/src/components/ToggleField/__tests__/index.test.tsx b/packages/form/src/components/ToggleField/__tests__/index.test.tsx index 2a6a0df01a..c2eb6c61ea 100644 --- a/packages/form/src/components/ToggleField/__tests__/index.test.tsx +++ b/packages/form/src/components/ToggleField/__tests__/index.test.tsx @@ -16,7 +16,7 @@ describe('ToggleField', () => { test('should render correctly checked', () => { const { asFragment } = renderWithForm(, { - initialValues: { + defaultValues: { test: true, }, }) @@ -29,7 +29,7 @@ describe('ToggleField', () => { const { asFragment } = renderWithForm( , { - initialValues: { + defaultValues: { test: true, }, }, diff --git a/packages/form/src/components/ToggleField/index.tsx b/packages/form/src/components/ToggleField/index.tsx index 22fc8f3696..eb5ddf6859 100644 --- a/packages/form/src/components/ToggleField/index.tsx +++ b/packages/form/src/components/ToggleField/index.tsx @@ -7,8 +7,8 @@ import type { BaseFieldProps } from '../../types' type ToggleFieldProps< TFieldValues extends FieldValues, - TName extends FieldPath, -> = Omit, 'label'> & + TFieldName extends FieldPath, +> = Omit, 'label'> & Pick< ComponentProps, | 'disabled' @@ -26,32 +26,34 @@ type ToggleFieldProps< export const ToggleField = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ className, disabled, label, name, + control, onChange, required, size, tooltip, - rules, labelPosition, parse, format, 'data-testid': dataTestId, shouldUnregister = false, -}: ToggleFieldProps) => { + validate, +}: ToggleFieldProps) => { const { field, fieldState: { error }, - } = useController({ + } = useController({ name, + control, shouldUnregister, rules: { required, - ...rules, + validate, }, }) const { getError } = useErrors() diff --git a/packages/form/src/components/ToggleGroupField/__stories__/Required.stories.tsx b/packages/form/src/components/ToggleGroupField/__stories__/Required.stories.tsx index 89d216d5ca..126a0b5536 100644 --- a/packages/form/src/components/ToggleGroupField/__stories__/Required.stories.tsx +++ b/packages/form/src/components/ToggleGroupField/__stories__/Required.stories.tsx @@ -1,8 +1,8 @@ import type { StoryFn } from '@storybook/react' import { Stack } from '@ultraviolet/ui' -import { useWatch } from 'react-hook-form' import { ToggleGroupField } from '..' import { Submit } from '../..' +import { useWatch } from '../../..' export const RequiredStory: StoryFn = args => { const values = useWatch() diff --git a/packages/form/src/components/ToggleGroupField/__stories__/Template.stories.tsx b/packages/form/src/components/ToggleGroupField/__stories__/Template.stories.tsx index 8dd4b5494b..19e79e8876 100644 --- a/packages/form/src/components/ToggleGroupField/__stories__/Template.stories.tsx +++ b/packages/form/src/components/ToggleGroupField/__stories__/Template.stories.tsx @@ -1,7 +1,7 @@ import type { StoryFn } from '@storybook/react' import { Stack } from '@ultraviolet/ui' -import { useWatch } from 'react-hook-form' import { ToggleGroupField } from '..' +import { useWatch } from '../../..' const ToggleGroupFieldStory: StoryFn = args => { const values = useWatch() diff --git a/packages/form/src/components/ToggleGroupField/__stories__/index.stories.tsx b/packages/form/src/components/ToggleGroupField/__stories__/index.stories.tsx index 2f2fdf375c..2147237f30 100644 --- a/packages/form/src/components/ToggleGroupField/__stories__/index.stories.tsx +++ b/packages/form/src/components/ToggleGroupField/__stories__/index.stories.tsx @@ -1,8 +1,8 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { ToggleGroupField } from '..' import { Form } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -29,7 +29,7 @@ export default { } = methods.formState return ( - {}} errors={mockErrors} methods={methods}> + {}} errors={mockErrors} methods={methods}> diff --git a/packages/form/src/components/ToggleGroupField/__tests__/index.test.tsx b/packages/form/src/components/ToggleGroupField/__tests__/index.test.tsx index 268165fe69..e19ccbb45c 100644 --- a/packages/form/src/components/ToggleGroupField/__tests__/index.test.tsx +++ b/packages/form/src/components/ToggleGroupField/__tests__/index.test.tsx @@ -19,7 +19,7 @@ describe('GroupField', () => { /> , { - initialValues: { + defaultValues: { Group: [], }, }, @@ -56,7 +56,7 @@ describe('GroupField', () => { /> , { - initialValues: { + defaultValues: { test: [], }, }, diff --git a/packages/form/src/components/ToggleGroupField/index.tsx b/packages/form/src/components/ToggleGroupField/index.tsx index 560a5f9580..83de148158 100644 --- a/packages/form/src/components/ToggleGroupField/index.tsx +++ b/packages/form/src/components/ToggleGroupField/index.tsx @@ -7,21 +7,22 @@ import type { BaseFieldProps } from '../../types' type ToggleGroupFieldProps< TFieldValues extends FieldValues, - TName extends FieldPath, -> = BaseFieldProps & + TFieldName extends FieldPath, +> = BaseFieldProps & Partial< Pick< ComponentProps, 'className' | 'helper' | 'direction' | 'children' | 'error' | 'legend' > > & - Required, 'legend' | 'name'>> + Required, 'legend'>> export const ToggleGroupField = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ legend, + control, className, helper, direction, @@ -32,16 +33,21 @@ export const ToggleGroupField = < name, required = false, shouldUnregister = false, -}: ToggleGroupFieldProps) => { + validate, +}: ToggleGroupFieldProps) => { const { getError } = useErrors() const { field, fieldState: { error }, - } = useController({ + } = useController({ name, + control, shouldUnregister, rules: { - validate: required ? value => value.length > 0 : undefined, + validate: { + ...(required ? { required: value => value.length > 0 } : undefined), + ...validate, + }, }, }) diff --git a/packages/form/src/components/UnitInputField/__stories__/index.stories.tsx b/packages/form/src/components/UnitInputField/__stories__/index.stories.tsx index 2ffcc4b89b..042d640bab 100644 --- a/packages/form/src/components/UnitInputField/__stories__/index.stories.tsx +++ b/packages/form/src/components/UnitInputField/__stories__/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react' import { Snippet, Stack, Text } from '@ultraviolet/ui' -import { useForm } from 'react-hook-form' import { Form, UnitInputField } from '../..' +import { useForm } from '../../..' import { mockErrors } from '../../../mocks' export default { @@ -24,7 +24,7 @@ export default { } = methods.formState return ( - {}} errors={mockErrors} methods={methods}> + {}} errors={mockErrors} methods={methods}> {ChildStory()} diff --git a/packages/form/src/components/UnitInputField/__tests__/index.test.tsx b/packages/form/src/components/UnitInputField/__tests__/index.test.tsx index 9ba7959f5d..511f30c6e1 100644 --- a/packages/form/src/components/UnitInputField/__tests__/index.test.tsx +++ b/packages/form/src/components/UnitInputField/__tests__/index.test.tsx @@ -44,7 +44,7 @@ describe('UnitInputField', () => { const { asFragment } = renderWithTheme( onSubmit(value)} + onSubmit={value => onSubmit(value)} errors={mockErrors} methods={result.current} > diff --git a/packages/form/src/components/UnitInputField/index.tsx b/packages/form/src/components/UnitInputField/index.tsx index 6522565b1c..6f44472495 100644 --- a/packages/form/src/components/UnitInputField/index.tsx +++ b/packages/form/src/components/UnitInputField/index.tsx @@ -7,8 +7,8 @@ import type { BaseFieldProps } from '../../types' type UnitInputFieldProps< TFieldValues extends FieldValues, - TName extends FieldPath, -> = BaseFieldProps & + TFieldName extends FieldPath, +> = BaseFieldProps & Pick< ComponentProps, | 'id' @@ -36,7 +36,7 @@ type UnitInputFieldProps< export const UnitInputField = < TFieldValues extends FieldValues, - TName extends FieldPath = FieldPath, + TFieldName extends FieldPath = FieldPath, >({ id, name, @@ -55,10 +55,10 @@ export const UnitInputField = < width, selectInputWidth, helper, - rules, shouldUnregister = false, validate, -}: UnitInputFieldProps) => { + control, +}: UnitInputFieldProps) => { const { getError } = useErrors() const { field: unitField } = useController({ name: `${name}-unit`, @@ -66,18 +66,20 @@ export const UnitInputField = < rules: { required }, }) - const { field: valueField, fieldState: valueFieldState } = - useController({ - name, - shouldUnregister, - rules: { - required, - min, - max, - ...rules, - ...validate, - }, - }) + const { field: valueField, fieldState: valueFieldState } = useController< + TFieldValues, + TFieldName + >({ + name, + shouldUnregister, + control, + rules: { + required, + min, + max, + validate, + }, + }) return ( { ) } -export const Usage: StoryFn = () => ( - {}} - errors={mockErrors} - > - - -) +export const Usage: StoryFn = () => { + const methods = useForm({ defaultValues: { email: 'test@test.com' } }) + + return ( +
{}} errors={mockErrors}> + + + ) +} diff --git a/packages/form/src/hooks/__tests__/useOnFieldChange.test.tsx b/packages/form/src/hooks/__tests__/useOnFieldChange.test.tsx index 8001f30b16..5679a64708 100644 --- a/packages/form/src/hooks/__tests__/useOnFieldChange.test.tsx +++ b/packages/form/src/hooks/__tests__/useOnFieldChange.test.tsx @@ -15,7 +15,7 @@ type FormValues = { type Wrapers = { children: ReactNode - initialValues: FormValues + defaultValues: FormValues } const initial = { @@ -28,9 +28,9 @@ const updated = { check: false, } -const Wrapper = ({ children, initialValues }: Wrapers) => { +const Wrapper = ({ children, defaultValues }: Wrapers) => { const methods = useForm({ - values: initialValues, + values: defaultValues, }) return ( @@ -38,7 +38,7 @@ const Wrapper = ({ children, initialValues }: Wrapers) => { methods={methods} errors={mockErrors} - onRawSubmit={() => {}} + onSubmit={() => {}} > {children} @@ -55,7 +55,7 @@ describe('useOnFieldChange', () => { expect(values).toStrictEqual(updated) }) - let initialValues = initial + let defaultValues = initial const { result, rerender } = renderHook( () => @@ -65,7 +65,7 @@ describe('useOnFieldChange', () => { ), { wrapper: ({ children }) => ( - {children} + {children} ), }, ) @@ -74,7 +74,7 @@ describe('useOnFieldChange', () => { expect(callback).toHaveBeenCalledTimes(0) - initialValues = updated + defaultValues = updated rerender() @@ -84,7 +84,7 @@ describe('useOnFieldChange', () => { test('should render when condition change', () => { const callback = vi.fn() - let initialValues = initial + let defaultValues = initial const { result, rerender } = renderHook( ({ enabled }) => { @@ -97,7 +97,7 @@ describe('useOnFieldChange', () => { }, { wrapper: ({ children }) => ( - {children} + {children} ), initialProps: { @@ -110,7 +110,7 @@ describe('useOnFieldChange', () => { expect(callback).toHaveBeenCalledTimes(0) - initialValues = updated + defaultValues = updated rerender({ enabled: true }) diff --git a/packages/form/src/hooks/index.ts b/packages/form/src/hooks/index.ts index ad2afca3da..ed146da7e3 100644 --- a/packages/form/src/hooks/index.ts +++ b/packages/form/src/hooks/index.ts @@ -1,5 +1 @@ export { useOnFieldChange } from './useOnFieldChange' -export { useFormStateDeprecated } from './useFormState' -export { useFieldDeprecated } from './useField' -export { useFormDeprecated } from './useForm' -export { useFieldArrayDeprecated } from './useFieldArray' diff --git a/packages/form/src/hooks/useField.ts b/packages/form/src/hooks/useField.ts deleted file mode 100644 index 819d3bfc09..0000000000 --- a/packages/form/src/hooks/useField.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { - FieldPath, - FieldPathValue, - FieldValues, - Path, - Validate, -} from 'react-hook-form' -import { useController, useFormContext } from 'react-hook-form' - -type Options = { - subscription?: Record - validate?: Validate< - FieldPathValue>, - TFieldValues - > -} - -/** - * @deprecated Use [useForm](https://www.react-hook-form.com/api/useform/), [useFormContext](https://www.react-hook-form.com/api/useformcontext/) or [useWatch](https://www.react-hook-form.com/api/usewatch/) to get values. Use [useFormState](https://www.react-hook-form.com/api/useformstate/) to get fields states. - * - * @example - * ```tsx - * const Input = () { - * const username = useWatch({ - * name: 'username' - * }) - * - * const { errors } = useFormState() - * - * console.log(errors.username) - * } - * ``` - */ -export const useFieldDeprecated = < - T, - TFieldValues extends FieldValues = FieldValues, - TFieldName extends FieldPath = FieldPath, ->( - name: TFieldName, - options?: Options, -) => { - const { getValues } = useFormContext() - const { field, fieldState } = useController({ - name, - rules: { validate: options?.validate }, - }) - - return { - input: { - value: getValues(name) as T, - onChange: field.onChange, - }, - meta: { - error: fieldState.error?.message, - touched: fieldState.isTouched, - invalid: fieldState.invalid, - dirty: fieldState.isDirty, - }, - } -} diff --git a/packages/form/src/hooks/useFieldArray.ts b/packages/form/src/hooks/useFieldArray.ts deleted file mode 100644 index f0e78805e5..0000000000 --- a/packages/form/src/hooks/useFieldArray.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { ReactNode } from 'react' -import type { - ArrayPath, - FieldArray, - FieldValues, - Validate, -} from 'react-hook-form' -import { - useFieldArray as useFieldArrayHookForm, - useWatch, -} from 'react-hook-form' - -type Options = { - validate?: Validate[], TFieldValues> - subscription?: Record -} - -/** - * @deprecated Use [useFieldArray](https://www.react-hook-form.com/api/usefieldarray/) - */ -export const useFieldArrayDeprecated = < - T, - TFieldValues extends FieldValues = FieldValues, ->( - name: ArrayPath, - options?: Options, -) => { - const { fields, append, remove, update, move } = - useFieldArrayHookForm({ - name, - rules: { validate: options?.validate }, - }) - - const value = useWatch({ name }) - - return { - fields: { - push: append, - value: value as T[], - remove, - update, - map: (callback: (name: string, index: number) => ReactNode) => - fields.map((_, index) => callback(`${name}.${index}`, index)), - move, - length: fields.length, - }, - } -} diff --git a/packages/form/src/hooks/useForm.ts b/packages/form/src/hooks/useForm.ts deleted file mode 100644 index b3ae649df8..0000000000 --- a/packages/form/src/hooks/useForm.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useContext } from 'react' -import type { FieldValues } from 'react-hook-form' -import { useFormContext } from 'react-hook-form' -import { FormSubmitContext } from '../components/Form' - -/** - * @deprecated Use [useFormContext](https://www.react-hook-form.com/api/useformcontext/) - * - * @example - * ```tsx - * const Input = () { - * const { setValue } = useFormContext() - * - * setValue('username', 'John Wick') - * } - * ``` - */ -export const useFormDeprecated = () => { - const { setValue, resetField, getFieldState, reset } = - useFormContext() - - const formSubmitContext = useContext(FormSubmitContext) - - return { - change: setValue, - resetFieldState: resetField, - getFieldState, - batch: (callback: () => void) => callback(), - restart: reset, - reset, - submit: formSubmitContext.handleSubmit, - } -} diff --git a/packages/form/src/hooks/useFormState.ts b/packages/form/src/hooks/useFormState.ts deleted file mode 100644 index f185bdbb63..0000000000 --- a/packages/form/src/hooks/useFormState.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { FieldValues } from 'react-hook-form' -import { useFormContext, useWatch } from 'react-hook-form' - -type UseFormStateParams = { - subscription?: Record -} - -/** - * @deprecated Use [useForm](https://www.react-hook-form.com/api/useform/), [useFormContext](https://www.react-hook-form.com/api/useformcontext/) or [useWatch](https://www.react-hook-form.com/api/usewatch/) to get values. Use [useFormState](https://www.react-hook-form.com/api/useformstate/) to get form states. - * - * @example - * ```tsx - * const Input = () { - * const username = useWatch({ - * name: 'username' - * }) - * - * const { isValid } = useFormState() - * } - * ``` - - */ -export const useFormStateDeprecated = ( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _params?: UseFormStateParams, -) => { - const { formState } = useFormContext() - - return { - dirtySinceLastSubmit: formState.isDirty, - submitErrors: formState.errors.root, - values: useWatch() as TFieldValues, - hasValidationErrors: !formState.isValid, - pristine: !formState.isDirty, - errors: formState.errors, - initialValues: formState.defaultValues, - touched: formState.touchedFields, - submitting: formState.isSubmitting, - invalid: !formState.isValid, - valid: formState.isValid, - } -} diff --git a/packages/form/src/hooks/useOnFieldChange.ts b/packages/form/src/hooks/useOnFieldChange.ts index 60f148e66e..7932862c18 100644 --- a/packages/form/src/hooks/useOnFieldChange.ts +++ b/packages/form/src/hooks/useOnFieldChange.ts @@ -15,6 +15,9 @@ export type CallbackFn< values: DeepPartial, ) => void | Promise +/** + * @deprecated + */ export const useOnFieldChange = < TFieldValues extends FieldValues, TFieldName extends FieldPath, diff --git a/packages/form/src/index.ts b/packages/form/src/index.ts index 60e9bebabf..d9557d7c0f 100644 --- a/packages/form/src/index.ts +++ b/packages/form/src/index.ts @@ -1,16 +1,11 @@ -export { FORM_ERROR } from './constants' -// eslint-disable-next-line no-restricted-syntax +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable import/export */ + export * from './components' export type { BaseFieldProps, FormErrors } from './types' export { useErrors, ErrorProvider } from './providers/ErrorContext' -export { - useFormStateDeprecated, - useFieldDeprecated, - useFormDeprecated, - useFieldArrayDeprecated, - useOnFieldChange, -} from './hooks' - +export { useOnFieldChange } from './hooks' export { useForm, useFieldArray, @@ -19,25 +14,5 @@ export { useFormState, useFormContext, } from 'react-hook-form' - -export type { - UseFieldArrayMove, - DeepPartial, - UseFormSetValue, - UseFormReturn, - ControllerRenderProps, - PathValue, - FieldValues, - FieldPath, - FieldErrors, - Path, - UseFieldArrayRemove, - ArrayPath, - FieldArrayPath, - FieldPathValues, - FieldArray, - FieldPathValue, - FieldArrayPathValue, - FieldPathByValue, - Control, -} from 'react-hook-form' +export type * from 'react-hook-form' +export { Form } from './components' diff --git a/packages/form/src/providers/ErrorContext/__tests__/index.test.tsx b/packages/form/src/providers/ErrorContext/__tests__/index.test.tsx index 163aa4cb21..eeb0fa9a88 100644 --- a/packages/form/src/providers/ErrorContext/__tests__/index.test.tsx +++ b/packages/form/src/providers/ErrorContext/__tests__/index.test.tsx @@ -1,20 +1,30 @@ import { renderHook } from '@testing-library/react' import { mockFormErrors, renderWithTheme } from '@utils/test' import type { ReactNode } from 'react' +import { useForm } from 'react-hook-form' import { describe, expect, test } from 'vitest' import { useErrors } from '..' import { Form } from '../../../components/Form' -const HookWrapper = ({ children }: { children: ReactNode }) => ( -
null}> - {children} -
-) +const HookWrapper = ({ children }: { children: ReactNode }) => { + const methods = useForm() + + return ( +
null} methods={methods}> + {children} +
+ ) +} describe('ErrorProvider', () => { test('renders correctly ', () => { + const { result } = renderHook(() => useForm()) const { asFragment } = renderWithTheme( -
null} errors={mockFormErrors}> + null} + errors={mockFormErrors} + methods={result.current} + > Test
, ) diff --git a/packages/form/src/types.ts b/packages/form/src/types.ts index 4c02d88c49..e240956c1d 100644 --- a/packages/form/src/types.ts +++ b/packages/form/src/types.ts @@ -1,4 +1,5 @@ import type { + Control, FieldError, FieldPath, FieldPathValue, @@ -38,22 +39,22 @@ export type FormErrors = { } export type BaseFieldProps< - TFieldValues extends FieldValues, - TName extends FieldPath, + TFieldValues extends FieldValues = FieldValues, + TFieldName extends FieldPath = FieldPath, > = { - name: TName + name: TFieldName required?: boolean validate?: Record< string, - Validate, TFieldValues> + Validate, TFieldValues> > - /** - * @deprecated Use individual props instead - */ - rules?: UseControllerProps['rules'] defaultValue?: PathValue> label?: string value?: PathValue> - onChange?: (value?: PathValue) => void - shouldUnregister?: UseControllerProps['shouldUnregister'] + onChange?: (value?: PathValue) => void + shouldUnregister?: UseControllerProps< + TFieldValues, + TFieldName + >['shouldUnregister'] + control?: Control } diff --git a/packages/form/src/utils/validateRegex.ts b/packages/form/src/utils/validateRegex.ts new file mode 100644 index 0000000000..8c4481cac7 --- /dev/null +++ b/packages/form/src/utils/validateRegex.ts @@ -0,0 +1,17 @@ +import type { FieldPath, FieldValues, PathValue } from 'react-hook-form' + +export const validateRegex = < + TFieldValues extends FieldValues, + TName extends FieldPath, +>( + value: PathValue, + regexes: (RegExp | RegExp[])[], +) => + regexes.every( + regex => + value === undefined || + value === '' || + (Array.isArray(regex) + ? regex.some(regexOr => regexOr.test(value)) + : regex.test(value)), + ) diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index c974f5b719..a6dbc6c0ec 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -2606,7 +2606,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- **tagsFields:** initialValues was not working ([#2147](https://github.com/scaleway/scaleway-ui/issues/2147)) ([219f584](https://github.com/scaleway/scaleway-ui/commit/219f58436377d7f604882cee6f9dbcbf3e655702)) +- **tagsFields:** defaultValues was not working ([#2147](https://github.com/scaleway/scaleway-ui/issues/2147)) ([219f584](https://github.com/scaleway/scaleway-ui/commit/219f58436377d7f604882cee6f9dbcbf3e655702)) ## [0.215.0](https://github.com/scaleway/scaleway-ui/compare/@scaleway/ui@0.214.4...@scaleway/ui@0.215.0) (2022-12-27) diff --git a/packages/ui/src/__stories__/Tools/ThemeGenerator/FormContent/index.tsx b/packages/ui/src/__stories__/Tools/ThemeGenerator/FormContent/index.tsx index fdaca94b48..91cbf885b1 100644 --- a/packages/ui/src/__stories__/Tools/ThemeGenerator/FormContent/index.tsx +++ b/packages/ui/src/__stories__/Tools/ThemeGenerator/FormContent/index.tsx @@ -3,6 +3,7 @@ import { useState } from 'react' import { Submit, TextInputField, + TextInputFieldV2, useFieldArray, useFormContext, useFormState, @@ -129,18 +130,11 @@ export const FormContent = () => { - { placeholder="#FFFFFF" noTopLabel type="color" - rules={{ - pattern: hexadecimalColorRegex, - }} + regex={[hexadecimalColorRegex]} /> {!isRequiredSentiment ? ( diff --git a/packages/ui/src/__stories__/Tools/ThemeGenerator/index.tsx b/packages/ui/src/__stories__/Tools/ThemeGenerator/index.tsx index feadf26117..c39cae9b97 100644 --- a/packages/ui/src/__stories__/Tools/ThemeGenerator/index.tsx +++ b/packages/ui/src/__stories__/Tools/ThemeGenerator/index.tsx @@ -132,7 +132,7 @@ export const ThemeGenerator = () => {
'', diff --git a/utils/test/src/vitest/helpers/index.tsx b/utils/test/src/vitest/helpers/index.tsx index 0e2442be09..9748fc9faa 100644 --- a/utils/test/src/vitest/helpers/index.tsx +++ b/utils/test/src/vitest/helpers/index.tsx @@ -1,11 +1,14 @@ import createCache from '@emotion/cache' import { CacheProvider, ThemeProvider } from '@emotion/react' -import { render } from '@testing-library/react' +import { render, renderHook } from '@testing-library/react' import type { RenderOptions } from '@testing-library/react' import { consoleLightTheme } from '@ultraviolet/themes' import type { ComponentProps, ReactElement, ReactNode } from 'react' -import { Form } from '../../../../../packages/form/src/index' -import type { FormErrors } from '../../../../../packages/form/src/index' +import type { + FormErrors, + UseFormProps, +} from '../../../../../packages/form/src/index' +import { Form, useForm } from '../../../../../packages/form/src/index' import { makeShouldMatchEmotionSnapshot } from './shouldMatchEmotionSnapshot' import { makeShouldMatchEmotionSnapshotWithPortal } from './shouldMatchEmotionSnapshotWithPortal' @@ -108,14 +111,25 @@ export const renderWithTheme = ( export const renderWithForm = ( compoment: ReactElement, - formOptions?: Partial>, + useFormProps?: UseFormProps, + formProps?: Partial>, theme?: typeof consoleLightTheme, -) => - renderWithTheme( - {}} errors={mockFormErrors} {...formOptions}> +) => { + const { result } = renderHook(() => + useForm({ mode: 'onChange', ...useFormProps }), + ) + + return renderWithTheme( + {}} + errors={mockFormErrors} + methods={result.current} + {...formProps} + > {compoment} , theme, ) +} export const defaultError = new Error('Default error message')