diff --git a/packages/form/.jest/helpers.tsx b/packages/form/.jest/helpers.tsx index 6a2ebebcd9..9a4e96ebe8 100644 --- a/packages/form/.jest/helpers.tsx +++ b/packages/form/.jest/helpers.tsx @@ -34,7 +34,7 @@ export const shouldMatchEmotionSnapshotFormWrapper = ( errors={mockErrors} initialValues={initialValues} > - {() => children} + {children} , options, ) diff --git a/packages/form/package.json b/packages/form/package.json index 703c8e62db..6bcb450432 100644 --- a/packages/form/package.json +++ b/packages/form/package.json @@ -54,10 +54,7 @@ "@emotion/react": "11.11.1", "@emotion/styled": "11.11.0", "@ultraviolet/ui": "workspace:*", - "final-form": "4.20.10", - "final-form-arrays": "3.0.2", - "react-final-form": "6.5.9", - "react-final-form-arrays": "3.1.4", - "react-select": "5.7.7" + "react-hook-form": "^7.47.0", + "react-select": "5.7.5" } } diff --git a/packages/form/src/components/CheckboxField/__stories__/BooleanChecked.stories.tsx b/packages/form/src/components/CheckboxField/__stories__/BooleanChecked.stories.tsx index 004e129950..581921dd85 100644 --- a/packages/form/src/components/CheckboxField/__stories__/BooleanChecked.stories.tsx +++ b/packages/form/src/components/CheckboxField/__stories__/BooleanChecked.stories.tsx @@ -1,9 +1,9 @@ import type { StoryFn } from '@storybook/react' import { CheckboxField } from '..' -import type { FormProps } from '../../Form' +import type { FormErrors } from '../../../types' import { Form } from '../../Form' -export const BooleanChecked: StoryFn = ({ errors }) => ( +export const BooleanChecked: StoryFn<{ errors: FormErrors }> = ({ errors }) => (
{}} errors={errors} initialValues={{ foo: true }}> Default Checked Boolean Item
diff --git a/packages/form/src/components/CheckboxField/__stories__/Checked.stories.tsx b/packages/form/src/components/CheckboxField/__stories__/Checked.stories.tsx index 8399cf6999..1d324398c7 100644 --- a/packages/form/src/components/CheckboxField/__stories__/Checked.stories.tsx +++ b/packages/form/src/components/CheckboxField/__stories__/Checked.stories.tsx @@ -1,9 +1,9 @@ import type { StoryFn } from '@storybook/react' import { CheckboxField } from '..' -import type { FormProps } from '../..' +import type { FormErrors } from '../../../types' import { Form } from '../../Form' -export const Checked: StoryFn = ({ errors }) => ( +export const Checked: StoryFn<{ errors: FormErrors }> = ({ errors }) => (
{}} errors={errors} initialValues={{ foo: ['bar'] }}> Checked Item diff --git a/packages/form/src/components/CheckboxField/__tests__/__snapshots__/index.spec.tsx.snap b/packages/form/src/components/CheckboxField/__tests__/__snapshots__/index.spec.tsx.snap index 4375b93091..7a88a972eb 100644 --- a/packages/form/src/components/CheckboxField/__tests__/__snapshots__/index.spec.tsx.snap +++ b/packages/form/src/components/CheckboxField/__tests__/__snapshots__/index.spec.tsx.snap @@ -1,5 +1,372 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`CheckboxField should check two boxes 1`] = ` + + .cache-13gdmaa { + position: relative; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-align-items: start; + -webkit-box-align: start; + -ms-flex-align: start; + align-items: start; + gap: 8px; +} + +.cache-13gdmaa .eqr7bqq3 { + cursor: pointer; +} + +.cache-13gdmaa[aria-disabled='true'] { + cursor: not-allowed; + color: #b5b7bd; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq3 { + cursor: not-allowed; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq4 { + fill: #e9eaeb; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq4 .eqr7bqq6 { + stroke: #d9dadd; + fill: #f3f3f4; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq2[aria-invalid="true"]:checked+.eqr7bqq4 { + fill: #ffd3e3; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq2[aria-invalid="true"]:checked+.eqr7bqq4 .eqr7bqq6 { + stroke: #ffd3e3; + fill: #ffd3e3; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq2[aria-invalid="true"]+.eqr7bqq4 { + fill: #ffebf2; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq2[aria-invalid="true"]+.eqr7bqq4 .eqr7bqq6 { + stroke: #ffbbd3; + fill: #ffebf2; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq2:checked+.eqr7bqq4 { + fill: #e5dbfd; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq2:checked+.eqr7bqq4 .eqr7bqq6 { + stroke: #d8c5fc; + fill: #d8c5fc; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq2[aria-checked="mixed"]+.eqr7bqq4 { + fill: #e5dbfd; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq2[aria-checked="mixed"]+.eqr7bqq4 .eqr7bqq6 { + stroke: #e5dbfd; + fill: #e5dbfd; +} + +.cache-13gdmaa .eqr7bqq2:checked+.eqr7bqq4 path { + transform-origin: center; + -webkit-transition: 200ms -webkit-transform ease-in-out; + transition: 200ms transform ease-in-out; + -webkit-transform: scale(1); + -moz-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + -webkit-transform: translate(2px, 2px); + -moz-transform: translate(2px, 2px); + -ms-transform: translate(2px, 2px); + transform: translate(2px, 2px); +} + +.cache-13gdmaa .eqr7bqq2:checked+.eqr7bqq4 .eqr7bqq6 { + fill: #8c40ef; + stroke: #8c40ef; +} + +.cache-13gdmaa .eqr7bqq2[aria-invalid="true"]:checked+.eqr7bqq4 .eqr7bqq6 { + fill: #e51963; + stroke: #e51963; +} + +.cache-13gdmaa .eqr7bqq2[aria-checked="mixed"]+.eqr7bqq4 .eqr7bqq5 { + fill: #ffffff; +} + +.cache-13gdmaa .eqr7bqq2[aria-checked="mixed"]+.eqr7bqq4 .eqr7bqq6 { + fill: #8c40ef; + stroke: #8c40ef; +} + +.cache-13gdmaa:hover[aria-disabled='false'] .eqr7bqq2[aria-invalid='false'][aria-checked='false']+.eqr7bqq4 .eqr7bqq6 { + stroke: #792dd4; + fill: #e5dbfd; +} + +.cache-13gdmaa:hover[aria-disabled='false'] .eqr7bqq2[aria-invalid='false'][aria-checked='true']+.eqr7bqq4 .eqr7bqq6 { + stroke: #792dd4; + fill: #792dd4; +} + +.cache-13gdmaa:hover[aria-disabled='false'] .eqr7bqq2[aria-invalid='false'][aria-checked='mixed']+.eqr7bqq4 .eqr7bqq6 { + stroke: #792dd4; + fill: #792dd4; +} + +.cache-13gdmaa:hover[aria-disabled='false'] .eqr7bqq2[aria-invalid='true'][aria-checked='false']+.eqr7bqq4 .eqr7bqq6 { + stroke: #92103f; + fill: #ffd3e3; +} + +.cache-13gdmaa:hover[aria-disabled='false'] .eqr7bqq2[aria-invalid='true'][aria-checked='true']+.eqr7bqq4 .eqr7bqq6 { + stroke: #d6175c; + fill: #d6175c; +} + +.cache-13gdmaa .eqr7bqq2[aria-invalid="true"]+.eqr7bqq4 { + fill: #e51963; +} + +.cache-13gdmaa .eqr7bqq2[aria-invalid="true"]+.eqr7bqq4 .eqr7bqq6 { + stroke: #e51963; + fill: #ffebf2; +} + +.cache-m3g6o6 { + position: absolute; + white-space: nowrap; + height: 24px; + width: 24px; + opacity: 0; + border-width: 0; +} + +.cache-m3g6o6:not(:disabled) { + cursor: pointer; +} + +.cache-m3g6o6:disabled { + cursor: not-allowed; +} + +.cache-m3g6o6:not(:disabled):checked+.eqr7bqq4, +.cache-m3g6o6:not(:disabled)[aria-checked='mixed']+.eqr7bqq4 { + fill: #8c40ef; +} + +.cache-m3g6o6:not(:disabled):checked+.eqr7bqq4 .eqr7bqq6, +.cache-m3g6o6:not(:disabled)[aria-checked='mixed']+.eqr7bqq4 .eqr7bqq6 { + stroke: #8c40ef; +} + +.cache-m3g6o6:not(:disabled)[aria-invalid='true']+.eqr7bqq4, +.cache-m3g6o6:not(:disabled)[aria-invalid='mixed']+.eqr7bqq4 { + fill: #ffebf2; +} + +.cache-m3g6o6:not(:disabled)[aria-invalid='true']+.eqr7bqq4 .eqr7bqq6, +.cache-m3g6o6:not(:disabled)[aria-invalid='mixed']+.eqr7bqq4 .eqr7bqq6 { + stroke: #b3144d; +} + +.cache-m3g6o6:focus+.eqr7bqq4 { + background-color: #f1eefc; + fill: #ffebf2; + outline: 1px solid 0px 0px 0px 3px #8c40ef40; +} + +.cache-m3g6o6:focus+.eqr7bqq4 .eqr7bqq6 { + stroke: #792dd4; + fill: #e5dbfd; +} + +.cache-m3g6o6[aria-invalid='true']:focus+.eqr7bqq4 { + background-color: #ffebf2; + fill: #ffebf2; + outline: 1px solid 0px 0px 0px 3px #f91b6c40; +} + +.cache-m3g6o6[aria-invalid='true']:focus+.eqr7bqq4 .eqr7bqq6 { + stroke: #92103f; + fill: #ffd3e3; +} + +.cache-55x4xn { + border-radius: 4px; + height: 24px; + width: 24px; + min-width: 24px; + min-height: 24px; +} + +.cache-55x4xn path { + fill: #ffffff; + -webkit-transform: translate(2px, 2px); + -moz-transform: translate(2px, 2px); + -ms-transform: translate(2px, 2px); + transform: translate(2px, 2px); + -webkit-transform: scale(0); + -moz-transform: scale(0); + -ms-transform: scale(0); + transform: scale(0); +} + +.cache-16eol3o { + fill: #ffffff; + stroke: #d9dadd; +} + +.cache-n7398p { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + gap: 2px; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-align-items: normal; + -webkit-box-align: normal; + -ms-flex-align: normal; + align-items: normal; + -webkit-box-pack: normal; + -ms-flex-pack: normal; + -webkit-justify-content: normal; + justify-content: normal; + -webkit-box-flex-wrap: nowrap; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; +} + +.cache-7efi4w { + display: grid; + grid-template-columns: 11fr 1fr; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: normal; + -ms-flex-pack: normal; + -webkit-justify-content: normal; + justify-content: normal; +} + + +
+ + + + + + + +
+
+
+
+
+ + + + + + + +
+
+
+
+ + +`; + exports[`CheckboxField should render correctly 1`] = ` .cache-13gdmaa { @@ -260,21 +627,336 @@ exports[`CheckboxField should render correctly 1`] = ` justify-content: normal; } -
+ +
+ + + + + + + +
+
+
+
+ + +`; + +exports[`CheckboxField should render correctly checked without value 1`] = ` + + .cache-13gdmaa { + position: relative; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-align-items: start; + -webkit-box-align: start; + -ms-flex-align: start; + align-items: start; + gap: 8px; +} + +.cache-13gdmaa .eqr7bqq3 { + cursor: pointer; +} + +.cache-13gdmaa[aria-disabled='true'] { + cursor: not-allowed; + color: #b5b7bd; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq3 { + cursor: not-allowed; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq4 { + fill: #e9eaeb; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq4 .eqr7bqq6 { + stroke: #d9dadd; + fill: #f3f3f4; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq2[aria-invalid="true"]:checked+.eqr7bqq4 { + fill: #ffd3e3; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq2[aria-invalid="true"]:checked+.eqr7bqq4 .eqr7bqq6 { + stroke: #ffd3e3; + fill: #ffd3e3; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq2[aria-invalid="true"]+.eqr7bqq4 { + fill: #ffebf2; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq2[aria-invalid="true"]+.eqr7bqq4 .eqr7bqq6 { + stroke: #ffbbd3; + fill: #ffebf2; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq2:checked+.eqr7bqq4 { + fill: #e5dbfd; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq2:checked+.eqr7bqq4 .eqr7bqq6 { + stroke: #d8c5fc; + fill: #d8c5fc; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq2[aria-checked="mixed"]+.eqr7bqq4 { + fill: #e5dbfd; +} + +.cache-13gdmaa[aria-disabled='true'] .eqr7bqq2[aria-checked="mixed"]+.eqr7bqq4 .eqr7bqq6 { + stroke: #e5dbfd; + fill: #e5dbfd; +} + +.cache-13gdmaa .eqr7bqq2:checked+.eqr7bqq4 path { + transform-origin: center; + -webkit-transition: 200ms -webkit-transform ease-in-out; + transition: 200ms transform ease-in-out; + -webkit-transform: scale(1); + -moz-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + -webkit-transform: translate(2px, 2px); + -moz-transform: translate(2px, 2px); + -ms-transform: translate(2px, 2px); + transform: translate(2px, 2px); +} + +.cache-13gdmaa .eqr7bqq2:checked+.eqr7bqq4 .eqr7bqq6 { + fill: #8c40ef; + stroke: #8c40ef; +} + +.cache-13gdmaa .eqr7bqq2[aria-invalid="true"]:checked+.eqr7bqq4 .eqr7bqq6 { + fill: #e51963; + stroke: #e51963; +} + +.cache-13gdmaa .eqr7bqq2[aria-checked="mixed"]+.eqr7bqq4 .eqr7bqq5 { + fill: #ffffff; +} + +.cache-13gdmaa .eqr7bqq2[aria-checked="mixed"]+.eqr7bqq4 .eqr7bqq6 { + fill: #8c40ef; + stroke: #8c40ef; +} + +.cache-13gdmaa:hover[aria-disabled='false'] .eqr7bqq2[aria-invalid='false'][aria-checked='false']+.eqr7bqq4 .eqr7bqq6 { + stroke: #792dd4; + fill: #e5dbfd; +} + +.cache-13gdmaa:hover[aria-disabled='false'] .eqr7bqq2[aria-invalid='false'][aria-checked='true']+.eqr7bqq4 .eqr7bqq6 { + stroke: #792dd4; + fill: #792dd4; +} + +.cache-13gdmaa:hover[aria-disabled='false'] .eqr7bqq2[aria-invalid='false'][aria-checked='mixed']+.eqr7bqq4 .eqr7bqq6 { + stroke: #792dd4; + fill: #792dd4; +} + +.cache-13gdmaa:hover[aria-disabled='false'] .eqr7bqq2[aria-invalid='true'][aria-checked='false']+.eqr7bqq4 .eqr7bqq6 { + stroke: #92103f; + fill: #ffd3e3; +} + +.cache-13gdmaa:hover[aria-disabled='false'] .eqr7bqq2[aria-invalid='true'][aria-checked='true']+.eqr7bqq4 .eqr7bqq6 { + stroke: #d6175c; + fill: #d6175c; +} + +.cache-13gdmaa .eqr7bqq2[aria-invalid="true"]+.eqr7bqq4 { + fill: #e51963; +} + +.cache-13gdmaa .eqr7bqq2[aria-invalid="true"]+.eqr7bqq4 .eqr7bqq6 { + stroke: #e51963; + fill: #ffebf2; +} + +.cache-m3g6o6 { + position: absolute; + white-space: nowrap; + height: 24px; + width: 24px; + opacity: 0; + border-width: 0; +} + +.cache-m3g6o6:not(:disabled) { + cursor: pointer; +} + +.cache-m3g6o6:disabled { + cursor: not-allowed; +} + +.cache-m3g6o6:not(:disabled):checked+.eqr7bqq4, +.cache-m3g6o6:not(:disabled)[aria-checked='mixed']+.eqr7bqq4 { + fill: #8c40ef; +} + +.cache-m3g6o6:not(:disabled):checked+.eqr7bqq4 .eqr7bqq6, +.cache-m3g6o6:not(:disabled)[aria-checked='mixed']+.eqr7bqq4 .eqr7bqq6 { + stroke: #8c40ef; +} + +.cache-m3g6o6:not(:disabled)[aria-invalid='true']+.eqr7bqq4, +.cache-m3g6o6:not(:disabled)[aria-invalid='mixed']+.eqr7bqq4 { + fill: #ffebf2; +} + +.cache-m3g6o6:not(:disabled)[aria-invalid='true']+.eqr7bqq4 .eqr7bqq6, +.cache-m3g6o6:not(:disabled)[aria-invalid='mixed']+.eqr7bqq4 .eqr7bqq6 { + stroke: #b3144d; +} + +.cache-m3g6o6:focus+.eqr7bqq4 { + background-color: #f1eefc; + fill: #ffebf2; + outline: 1px solid 0px 0px 0px 3px #8c40ef40; +} + +.cache-m3g6o6:focus+.eqr7bqq4 .eqr7bqq6 { + stroke: #792dd4; + fill: #e5dbfd; +} + +.cache-m3g6o6[aria-invalid='true']:focus+.eqr7bqq4 { + background-color: #ffebf2; + fill: #ffebf2; + outline: 1px solid 0px 0px 0px 3px #f91b6c40; +} + +.cache-m3g6o6[aria-invalid='true']:focus+.eqr7bqq4 .eqr7bqq6 { + stroke: #92103f; + fill: #ffd3e3; +} + +.cache-55x4xn { + border-radius: 4px; + height: 24px; + width: 24px; + min-width: 24px; + min-height: 24px; +} + +.cache-55x4xn path { + fill: #ffffff; + -webkit-transform: translate(2px, 2px); + -moz-transform: translate(2px, 2px); + -ms-transform: translate(2px, 2px); + transform: translate(2px, 2px); + -webkit-transform: scale(0); + -moz-transform: scale(0); + -ms-transform: scale(0); + transform: scale(0); +} + +.cache-16eol3o { + fill: #ffffff; + stroke: #d9dadd; +} + +.cache-n7398p { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + gap: 2px; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-align-items: normal; + -webkit-box-align: normal; + -ms-flex-align: normal; + align-items: normal; + -webkit-box-pack: normal; + -ms-flex-pack: normal; + -webkit-justify-content: normal; + justify-content: normal; + -webkit-box-flex-wrap: nowrap; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; +} + +.cache-7efi4w { + display: grid; + grid-template-columns: 11fr 1fr; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: normal; + -ms-flex-pack: normal; + -webkit-justify-content: normal; + justify-content: normal; +} + +
@@ -318,7 +1000,7 @@ exports[`CheckboxField should render correctly 1`] = ` `; -exports[`CheckboxField should render correctly checked without value 1`] = ` +exports[`CheckboxField should render correctly disabled 1`] = ` .cache-13gdmaa { position: relative; @@ -578,22 +1260,20 @@ exports[`CheckboxField should render correctly checked without value 1`] = ` justify-content: normal; } - +
@@ -637,7 +1317,7 @@ exports[`CheckboxField should render correctly checked without value 1`] = ` `; -exports[`CheckboxField should render correctly disabled 1`] = ` +exports[`CheckboxField should render correctly not checked without value 1`] = ` .cache-13gdmaa { position: relative; @@ -897,11 +1577,9 @@ exports[`CheckboxField should render correctly disabled 1`] = ` justify-content: normal; } - +
@@ -1216,9 +1893,7 @@ exports[`CheckboxField should render correctly with a value 1`] = ` justify-content: normal; } - +
+
+
{ shouldMatchEmotionSnapshotFormWrapper( , { - transform: async () => { + transform: () => { const input = screen.getByRole('checkbox', { hidden: true }) - await waitFor(() => expect(input).toBeChecked()) + expect(input).toBeChecked() }, }, { @@ -39,6 +39,20 @@ describe('CheckboxField', () => { }, )) + test('should render correctly not checked without value', () => + shouldMatchEmotionSnapshotFormWrapper( + , + { + transform: () => { + const input = screen.getByRole('checkbox', { hidden: true }) + expect(input).not.toBeChecked() + }, + }, + { + initialValues: {}, + }, + )) + test('should render correctly with a value', () => shouldMatchEmotionSnapshotFormWrapper( <> @@ -94,6 +108,25 @@ describe('CheckboxField', () => { ) }) + test('should check two boxes', () => + shouldMatchEmotionSnapshotFormWrapper( + <> + + + , + { + transform: () => { + const inputs = screen.getAllByRole('checkbox', { hidden: true }) + act(() => inputs[0].click()) + expect(inputs[0]).toBeChecked() + expect(inputs[1]).not.toBeChecked() + act(() => inputs[1].click()) + expect(inputs[0]).toBeChecked() + expect(inputs[1]).toBeChecked() + }, + }, + )) + test('should render correctly with errors', () => shouldMatchEmotionSnapshot( {}} errors={mockErrors}> @@ -108,7 +141,7 @@ describe('CheckboxField', () => { // to trigger error await userEvent.click(screen.getByRole('checkbox', { hidden: true })) await userEvent.click(screen.getByText('Focus')) - const error = screen.getByText(mockErrors.REQUIRED as string) + const error = screen.getByText(mockErrors.required({ label: '' })) expect(error).toBeVisible() }, }, diff --git a/packages/form/src/components/CheckboxField/index.tsx b/packages/form/src/components/CheckboxField/index.tsx index f9718346ca..01a27a1917 100644 --- a/packages/form/src/components/CheckboxField/index.tsx +++ b/packages/form/src/components/CheckboxField/index.tsx @@ -1,105 +1,91 @@ import { Checkbox } from '@ultraviolet/ui' -import type { FieldState } from 'final-form' -import type { ComponentProps, JSX, ReactNode, Ref } from 'react' -import { forwardRef } from 'react' -import { useFormField } from '../../hooks' +import type { ComponentProps, ReactNode } from 'react' +import type { FieldValues } from 'react-hook-form' +import { Controller } from 'react-hook-form' import { useErrors } from '../../providers' import type { BaseFieldProps } from '../../types' -type CheckboxValue = string - -type CheckboxFieldProps = BaseFieldProps & - Partial< - Pick< - ComponentProps, - | 'disabled' - | 'onBlur' - | 'onChange' - | 'onFocus' - | 'progress' - | 'size' - | 'value' - | 'data-testid' - | 'helper' - | 'tooltip' - > - > & { - name: string - label?: string - className?: string - children?: ReactNode - required?: boolean - } - -export const CheckboxField = forwardRef( - ( - { - validate, - name, - label = '', - size, - progress, - disabled, - required, - className, - children, - onChange, - onBlur, - onFocus, - value, - helper, - tooltip, - 'data-testid': dataTestId, - }: CheckboxFieldProps, - ref: Ref, - ): JSX.Element => { - const { getError } = useErrors() - - const { input, meta } = useFormField(name, { - disabled, - required, - type: 'checkbox', - validate, - value, - }) +type CheckboxFieldProps = + BaseFieldProps & + Partial< + Pick< + ComponentProps, + | 'disabled' + | 'onBlur' + | 'onChange' + | 'onFocus' + | 'progress' + | 'size' + | 'data-testid' + | 'helper' + | 'tooltip' + > + > & { + label?: string + className?: string + children?: ReactNode + required?: boolean + } - const error = getError({ - label, - meta: meta as FieldState, - name, - value: input.value ?? input.checked, - }) +export const CheckboxField = ({ + name, + label, + size, + progress, + disabled, + required, + className, + children, + onChange, + onBlur, + onFocus, + rules, + helper, + tooltip, + 'data-testid': dataTestId, + value, +}: CheckboxFieldProps) => { + const { getError } = useErrors() - return ( - { - input.onChange(event) - onChange?.(event) - }} - onBlur={event => { - input.onBlur(event) - onBlur?.(event) - }} - onFocus={event => { - input.onFocus(event) - onFocus?.(event) - }} - size={size} - progress={progress} - disabled={disabled} - checked={input.checked} - error={error} - helper={helper} - ref={ref} - className={className} - value={input.value} - required={required} - data-testid={dataTestId} - tooltip={tooltip} - > - {children} - - ) - }, -) + return ( + ( + { + field.onChange( + value ? [...(field.value ?? []), value] : event.target.checked, + ) + onChange?.(event) + }} + onBlur={event => { + field.onBlur() + onBlur?.(event) + }} + onFocus={onFocus} + size={size} + progress={progress} + disabled={field.disabled} + checked={ + Array.isArray(field.value) + ? (field.value as (typeof value)[]).includes(value) + : !!field.value + } + error={getError({ label: label ?? '' }, error)} + ref={field.ref} + className={className} + required={required} + data-testid={dataTestId} + helper={helper} + tooltip={tooltip} + value={value} + > + {children} + + )} + /> + ) +} diff --git a/packages/form/src/components/CheckboxGroupField/__stories__/Direction.stories.tsx b/packages/form/src/components/CheckboxGroupField/__stories__/Direction.stories.tsx index d7131cc553..f10edc7df9 100644 --- a/packages/form/src/components/CheckboxGroupField/__stories__/Direction.stories.tsx +++ b/packages/form/src/components/CheckboxGroupField/__stories__/Direction.stories.tsx @@ -1,10 +1,10 @@ import type { StoryFn } from '@storybook/react' import { Stack } from '@ultraviolet/ui' -import { useFormState } from 'react-final-form' +import { useFormContext } from 'react-hook-form' import { CheckboxGroupField } from '..' export const DirectionStory: StoryFn = args => { - const { values } = useFormState() + const { watch } = useFormContext() return ( @@ -20,7 +20,7 @@ export const DirectionStory: StoryFn = args => { - Form content: {JSON.stringify(values)} + Form content: {JSON.stringify(watch())} ) diff --git a/packages/form/src/components/CheckboxGroupField/__stories__/Required.stories.tsx b/packages/form/src/components/CheckboxGroupField/__stories__/Required.stories.tsx index ce04ca7c16..dbfaa688e6 100644 --- a/packages/form/src/components/CheckboxGroupField/__stories__/Required.stories.tsx +++ b/packages/form/src/components/CheckboxGroupField/__stories__/Required.stories.tsx @@ -1,11 +1,11 @@ import type { StoryFn } from '@storybook/react' import { Stack } from '@ultraviolet/ui' -import { useFormState } from 'react-final-form' +import { useFormContext } from 'react-hook-form' import { CheckboxGroupField } from '..' import { Submit } from '../..' export const RequiredStory: StoryFn = args => { - const { values } = useFormState() + const { watch } = useFormContext() return ( @@ -22,7 +22,7 @@ export const RequiredStory: StoryFn = args => { Submit - Form content: {JSON.stringify(values)} + Form content: {JSON.stringify(watch())} ) diff --git a/packages/form/src/components/CheckboxGroupField/__stories__/Template.stories.tsx b/packages/form/src/components/CheckboxGroupField/__stories__/Template.stories.tsx index c46bc392f1..df97cfc4a5 100644 --- a/packages/form/src/components/CheckboxGroupField/__stories__/Template.stories.tsx +++ b/packages/form/src/components/CheckboxGroupField/__stories__/Template.stories.tsx @@ -1,10 +1,10 @@ import type { StoryFn } from '@storybook/react' import { Stack } from '@ultraviolet/ui' -import { useFormState } from 'react-final-form' +import { useFormContext } from 'react-hook-form' import { CheckboxGroupField } from '..' const CheckboxGroupFieldStory: StoryFn = args => { - const { values } = useFormState() + const { watch } = useFormContext() return ( @@ -20,7 +20,7 @@ const CheckboxGroupFieldStory: StoryFn = args => { - Form content: {JSON.stringify(values)} + Form content: {JSON.stringify(watch())} ) diff --git a/packages/form/src/components/CheckboxGroupField/__stories__/index.stories.tsx b/packages/form/src/components/CheckboxGroupField/__stories__/index.stories.tsx index c75ffecefa..2423558dee 100644 --- a/packages/form/src/components/CheckboxGroupField/__stories__/index.stories.tsx +++ b/packages/form/src/components/CheckboxGroupField/__stories__/index.stories.tsx @@ -8,7 +8,7 @@ export default { decorators: [ ChildStory => ( { console.log('data', data) }} diff --git a/packages/form/src/components/CheckboxGroupField/__tests__/__snapshots__/index.spec.tsx.snap b/packages/form/src/components/CheckboxGroupField/__tests__/__snapshots__/index.spec.tsx.snap index 95da7195eb..25208c9d2d 100644 --- a/packages/form/src/components/CheckboxGroupField/__tests__/__snapshots__/index.spec.tsx.snap +++ b/packages/form/src/components/CheckboxGroupField/__tests__/__snapshots__/index.spec.tsx.snap @@ -323,9 +323,7 @@ exports[`CheckboxField should render correctly checked 1`] = ` justify-content: normal; } - +
@@ -800,9 +798,7 @@ exports[`CheckboxField should trigger events correctly with required prop 1`] = justify-content: normal; } - +
diff --git a/packages/form/src/components/CheckboxGroupField/__tests__/index.spec.tsx b/packages/form/src/components/CheckboxGroupField/__tests__/index.spec.tsx index dfded7b844..470819f8f9 100644 --- a/packages/form/src/components/CheckboxGroupField/__tests__/index.spec.tsx +++ b/packages/form/src/components/CheckboxGroupField/__tests__/index.spec.tsx @@ -25,6 +25,11 @@ describe('CheckboxField', () => { expect(secondInput).toBeChecked() }, }, + { + initialValues: { + Checkbox: [], + }, + }, )) test('should trigger events correctly with required prop', () => { @@ -56,6 +61,11 @@ describe('CheckboxField', () => { expect(input).not.toBeChecked() }, }, + { + initialValues: { + test: [], + }, + }, ) }) }) diff --git a/packages/form/src/components/CheckboxGroupField/index.tsx b/packages/form/src/components/CheckboxGroupField/index.tsx index 98ea121d43..7ce5b80102 100644 --- a/packages/form/src/components/CheckboxGroupField/index.tsx +++ b/packages/form/src/components/CheckboxGroupField/index.tsx @@ -1,81 +1,74 @@ import { CheckboxGroup } from '@ultraviolet/ui' import type { ComponentProps } from 'react' -import { useFieldArray } from 'react-final-form-arrays' +import type { FieldValues } from 'react-hook-form' +import { Controller } from 'react-hook-form' import { useErrors } from '../../providers' import type { BaseFieldProps } from '../../types' -type CheckboxGroupValue = string[] +type CheckboxGroupFieldProps = + BaseFieldProps & + Partial< + Pick< + ComponentProps, + | 'className' + | 'helper' + | 'onChange' + | 'required' + | 'direction' + | 'children' + | 'value' + | 'error' + | 'legend' + > + > & + Required, 'legend' | 'name'>> -type CheckboxGroupFieldProps< - T = CheckboxGroupValue, - K = string, -> = BaseFieldProps & - Partial< - Pick< - ComponentProps, - | 'className' - | 'helper' - | 'onChange' - | 'required' - | 'direction' - | 'children' - | 'value' - | 'error' - | 'legend' - > - > & - Required, 'legend' | 'name'>> - -export const CheckboxGroupField = ({ +export const CheckboxGroupField = ({ legend, - value, className, helper, direction, children, onChange, + label = '', error: customError, name, required = false, -}: CheckboxGroupFieldProps) => { +}: CheckboxGroupFieldProps) => { const { getError } = useErrors() - const { fields, meta } = useFieldArray(name, { - type: 'checkbox', - value, - validate: localValue => - required && localValue?.length === 0 ? 'Required' : undefined, - }) - - const error = getError({ - label: legend, - meta, - value: fields.value, - name, - }) - return ( - { - if (fields.value?.includes(event.currentTarget.value)) { - fields.remove(fields.value.indexOf(event.currentTarget?.value)) - } else { - fields.push(event.currentTarget.value) - } + ( + { + const fieldValue = field.value as string[] + if (fieldValue?.includes(event.currentTarget.value)) { + field.onChange( + fieldValue?.filter( + currentValue => currentValue !== event.currentTarget.value, + ), + ) + } else { + field.onChange([...field.value, event.currentTarget.value]) + } - onChange?.(event) - }} - error={error ?? customError} - className={className} - direction={direction} - helper={helper} - required={required} - > - {children} - + onChange?.(event) + }} + error={getError({ label }, error) ?? customError} + className={className} + direction={direction} + helper={helper} + required={required} + > + {children} + + )} + /> ) } diff --git a/packages/form/src/components/DateField/__stories__/MinMaxWithTimeField.stories.tsx b/packages/form/src/components/DateField/__stories__/MinMaxWithTimeField.stories.tsx index 2293e2cc6c..909c138d0f 100644 --- a/packages/form/src/components/DateField/__stories__/MinMaxWithTimeField.stories.tsx +++ b/packages/form/src/components/DateField/__stories__/MinMaxWithTimeField.stories.tsx @@ -5,8 +5,6 @@ import { DateField } from '..' import { Submit } from '../../Submit' import { TimeField } from '../../TimeField' -const now = new Date() - export const MinMaxDateWithTimeField: StoryFn< ComponentProps > = ({ name, minDate, maxDate, required }) => ( @@ -14,7 +12,6 @@ export const MinMaxDateWithTimeField: StoryFn< diff --git a/packages/form/src/components/DateField/__tests__/__snapshots__/index.tsx.snap b/packages/form/src/components/DateField/__tests__/__snapshots__/index.tsx.snap index 5e16540339..6379e3f1d6 100644 --- a/packages/form/src/components/DateField/__tests__/__snapshots__/index.tsx.snap +++ b/packages/form/src/components/DateField/__tests__/__snapshots__/index.tsx.snap @@ -279,9 +279,7 @@ exports[`DateField should render correctly 1`] = ` min-height: 24px; } - +
@@ -625,9 +623,7 @@ exports[`DateField should render correctly disabled 1`] = ` min-height: 24px; } - +
@@ -968,9 +964,7 @@ exports[`DateField should trigger events 1`] = ` min-height: 24px; } - +
diff --git a/packages/form/src/components/DateField/__tests__/index.tsx b/packages/form/src/components/DateField/__tests__/index.tsx index a8fc465c24..7dcf3fccf2 100644 --- a/packages/form/src/components/DateField/__tests__/index.tsx +++ b/packages/form/src/components/DateField/__tests__/index.tsx @@ -32,12 +32,7 @@ describe('DateField', () => { const onChange = jest.fn() return shouldMatchEmotionSnapshotFormWrapper( - , + , { transform: () => { const select = screen.getByRole('textbox') @@ -53,6 +48,11 @@ describe('DateField', () => { // expect(onBlur).toBeCalledTimes(1) }, }, + { + initialValues: { + 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 86781ce3c9..00b90eab4b 100644 --- a/packages/form/src/components/DateField/index.tsx +++ b/packages/form/src/components/DateField/index.tsx @@ -1,39 +1,42 @@ import { DateInput } from '@ultraviolet/ui' -import type { FieldState } from 'final-form' import type { ComponentProps, FocusEvent } from 'react' -import { useFormField } from '../../hooks' +import type { FieldValues } from 'react-hook-form' +import { Controller } from 'react-hook-form' import { useErrors } from '../../providers' import type { BaseFieldProps } from '../../types' +import { maxDateValidator } from '../../validators/maxDate' +import { minDateValidator } from '../../validators/minDate' type DateExtends = Date | [Date | null, Date | null] -type DateFieldProps = BaseFieldProps & - Omit< - ComponentProps, - | 'maxDate' - | 'minDate' - | 'disabled' - | 'required' - | 'locale' - | 'name' - | 'onChange' - | 'onFocus' - | 'onBlur' - | 'autoFocus' - | 'startDate' - | 'endDate' - > & { - name: string - maxDate?: Date - minDate?: Date - disabled?: boolean - required?: boolean - locale?: string - onChange?: (value: Date | null) => void - onBlur?: (event: FocusEvent) => void - onFocus?: (value: FocusEvent) => void - autoFocus?: boolean - } +type DateFieldProps = + BaseFieldProps & + Omit< + ComponentProps, + | 'maxDate' + | 'minDate' + | 'disabled' + | 'required' + | 'locale' + | 'name' + | 'onChange' + | 'onFocus' + | 'onBlur' + | 'autoFocus' + | 'stardDate' + | 'endDate' + > & { + name: string + maxDate?: Date + minDate?: Date + disabled?: boolean + required?: boolean + locale?: string + onChange?: (value: Date | null) => void + onBlur?: (event: FocusEvent) => void + onFocus?: (value: FocusEvent) => void + autoFocus?: boolean + } const parseDate = (value: string | Date): Date => typeof value === 'string' ? new Date(value) : value @@ -41,102 +44,87 @@ const parseDate = (value: string | Date): Date => const isEmpty = (value?: Date | string): boolean => typeof value === 'string' ? value === '' : value === undefined -export const DateField = ({ +export const DateField = ({ required, name, label = '', - validate, format, locale, maxDate, minDate, - initialValue, disabled, - value: inputVal, onChange, onBlur, onFocus, - formatOnBlur, + rules, autoFocus = false, excludeDates, selectsRange, 'data-testid': dataTestId, -}: DateFieldProps) => { +}: DateFieldProps) => { const { getError } = useErrors() - const { input, meta } = useFormField(name, { - disabled, - formatOnBlur, - initialValue, - maxDate, - minDate, - required, - validate, - value: inputVal, - }) - - const error = getError({ - label, - maxDate, - meta: meta as FieldState, - minDate, - name, - value: input.value, - }) - return ( - (value ? parseDate(value).toLocaleDateString() : '')) - } - locale={locale} - required={required} - value={input.value as Date} - onChange={(val: DateExtends | null) => { - if (val && val instanceof Date) { - onChange?.(val) - const newDate = parseDate(val) - if (isEmpty(input.value as Date)) { - input.onChange(newDate) + ( + (value ? parseDate(value).toLocaleDateString() : '')) + } + locale={locale} + required={required} + onChange={(val: DateExtends | null) => { + if (val && val instanceof Date) { + onChange?.(val) + const newDate = parseDate(val) + if (isEmpty(field.value as Date)) { + field.onChange(newDate) - return + return + } + const currentDate = parseDate(field.value as Date) + newDate.setHours(currentDate.getHours(), currentDate.getMinutes()) + field.onChange(newDate) + } + }} + onBlur={(e: FocusEvent) => { + field.onBlur() + onBlur?.(e) + }} + onFocus={onFocus} + maxDate={maxDate} + minDate={minDate} + error={getError({ minDate, maxDate, label }, error)} + disabled={disabled} + autoFocus={autoFocus} + excludeDates={excludeDates} + data-testid={dataTestId} + startDate={ + selectsRange + ? (field.value as [Date | null, Date | null])[0] + : undefined } - const currentDate = parseDate(input.value as Date) - newDate.setHours(currentDate.getHours(), currentDate.getMinutes()) - input.onChange(newDate) - } else if (Array.isArray(val)) { - input.onChange(val) - } - }} - onBlur={(e: FocusEvent) => { - input.onBlur(e) - onBlur?.(e) - }} - onFocus={(e: FocusEvent) => { - input.onFocus(e) - onFocus?.(e) - }} - maxDate={maxDate} - minDate={minDate} - startDate={ - selectsRange - ? (input.value as [Date | null, Date | null])[0] - : undefined - } - endDate={ - selectsRange - ? (input.value as [Date | null, Date | null])[1] - : undefined - } - error={error} - disabled={disabled} - autoFocus={autoFocus} - name={input.name} - data-testid={dataTestId} - excludeDates={excludeDates} - selectsRange={selectsRange} + endDate={ + selectsRange + ? (field.value as [Date | null, Date | null])[1] + : undefined + } + /> + )} /> ) } diff --git a/packages/form/src/components/Form/__stories__/Playground.stories.tsx b/packages/form/src/components/Form/__stories__/Playground.stories.tsx index b289aa107a..1e2aa23e17 100644 --- a/packages/form/src/components/Form/__stories__/Playground.stories.tsx +++ b/packages/form/src/components/Form/__stories__/Playground.stories.tsx @@ -1,11 +1,11 @@ import type { StoryFn } from '@storybook/react' -import { Checkbox, Stack } from '@ultraviolet/ui' -import type { ChangeEvent } from 'react' -import { useState } from 'react' +import { Stack } from '@ultraviolet/ui' +import { useForm } from 'react-hook-form' import { CheckboxField, DateField, Form, + NumberInputField, RadioField, SelectInputField, SelectableCardField, @@ -18,20 +18,47 @@ import { } from '../..' import { emailRegex, mockErrors } from '../../../mocks/mockErrors' -export const Playground: StoryFn = args => { - const [state, setState] = useState(false) +type FormValues = { + receiveEmailUpdates: boolean + choice: string + tags: string[] + selectableCard: string + disableName: boolean + email: string +} + +export const Playground: StoryFn = () => { + const methods = useForm({ + mode: 'onChange', + defaultValues: { + receiveEmailUpdates: true, + choice: '2', + tags: ['cloud', 'of', 'choice'], + selectableCard: '1', + disableName: false, + email: 'email', + }, + }) + + const disableName = methods.watch('disableName') return ( - + + errors={mockErrors} + methods={methods} + onRawSubmit={() => + new Promise(rejects => { + setTimeout( + () => rejects({ 'FINAL_FORM/form-error': 'SERVER ERROR' }), + 5000, + ) + }) + } + > - ) => - setState(event.target.checked) - } - > + I'm disabling the field name to remove validation - + @@ -59,17 +86,23 @@ export const Playground: StoryFn = args => { name="name" label="Name" placeholder="John" - required autoComplete="given-name" - disabled={state} + required={!disableName} + disabled={disableName} /> + @@ -95,14 +128,15 @@ export const Playground: StoryFn = args => { } Playground.args = { - errors: mockErrors, - initialValues: { - receiveEmailUpdates: true, - choice: '2', - tags: ['cloud', 'of', 'choice'], - selectableCard: '1', - }, onRawSubmit: values => { console.log('Submit', values) }, + // initialValues: { + // receiveEmailUpdates: true, + // choice: '2', + // tags: ['cloud', 'of', 'choice'], + // selectableCard: '1', + // disableName: false, + // email: 'email' + // } } diff --git a/packages/form/src/components/Form/__tests__/__snapshots__/index.spec.tsx.snap b/packages/form/src/components/Form/__tests__/__snapshots__/index.spec.tsx.snap index c635a536db..766660eb75 100644 --- a/packages/form/src/components/Form/__tests__/__snapshots__/index.spec.tsx.snap +++ b/packages/form/src/components/Form/__tests__/__snapshots__/index.spec.tsx.snap @@ -2,9 +2,7 @@ exports[`Form renders correctly 1`] = ` - + Test @@ -12,9 +10,7 @@ exports[`Form renders correctly 1`] = ` exports[`Form renders correctly with node children 1`] = ` -
+ Test
@@ -22,9 +18,7 @@ exports[`Form renders correctly with node children 1`] = ` exports[`Form renders correctly with onRawSubmit 1`] = ` -
+
- - - - tags-2 - - - @@ -527,9 +365,7 @@ exports[`ToggleField should render correctly with default tags on initialValues color: #727683; } - +
@@ -764,9 +600,7 @@ exports[`ToggleField should render correctly with default tags on initialValues color: #727683; } - +
diff --git a/packages/form/src/components/TagInputField/index.tsx b/packages/form/src/components/TagInputField/index.tsx index 15b60574fb..6e4577cd32 100644 --- a/packages/form/src/components/TagInputField/index.tsx +++ b/packages/form/src/components/TagInputField/index.tsx @@ -1,67 +1,55 @@ import { TagInput } from '@ultraviolet/ui' -import type { ComponentProps, JSX } from 'react' -import { useFormField } from '../../hooks' +import type { ComponentProps } from 'react' +import type { FieldValues } from 'react-hook-form' +import { Controller } from 'react-hook-form' import type { BaseFieldProps } from '../../types' -type TagInputProp = ComponentProps['tags'] - -export type TagInputFieldProps = BaseFieldProps< - T, - K -> & - Partial< - Pick< - ComponentProps, - | 'tags' - | 'variant' - | 'onChange' - | 'placeholder' - | 'disabled' - | 'className' - | 'id' - | 'data-testid' +export type TagInputFieldProps = + BaseFieldProps & + Partial< + Pick< + ComponentProps, + | 'tags' + | 'variant' + | 'onChange' + | 'placeholder' + | 'disabled' + | 'className' + | 'id' + > > - > & { - name: string - required?: boolean - } -export const TagInputField = ({ +export const TagInputField = ({ className, - 'data-testid': dataTestId, disabled, id, name, onChange, placeholder, required, - tags, - validate, + rules, variant, -}: TagInputFieldProps): JSX.Element => { - const { input } = useFormField(name, { - disabled, - required, - initialValue: tags, - type: 'text', - validate, - value: tags, - }) - - return ( - { - onChange?.(event) - input.onChange(event) - }} - placeholder={placeholder} - variant={variant} - tags={input.value} - data-testid={dataTestId} - /> - ) -} +}: TagInputFieldProps) => ( + ( + { + field.onChange(event) + onChange?.(event) + }} + placeholder={placeholder} + variant={variant} + tags={field.value} + /> + )} + /> +) diff --git a/packages/form/src/components/TextInputField/__tests__/__snapshots__/index.spec.tsx.snap b/packages/form/src/components/TextInputField/__tests__/__snapshots__/index.spec.tsx.snap index 7fc6d893f1..4c73dca453 100644 --- a/packages/form/src/components/TextInputField/__tests__/__snapshots__/index.spec.tsx.snap +++ b/packages/form/src/components/TextInputField/__tests__/__snapshots__/index.spec.tsx.snap @@ -95,9 +95,7 @@ exports[`TextInputField should render correctly 1`] = ` padding-top: 2px; } - +
+
+
+
+
+
+
+
{ await waitFor(() => { expect( screen.getByText( - typeof mockErrors.MIN_LENGTH === 'function' - ? mockErrors.MIN_LENGTH({ - allValues: {}, - label: 'test', - minLength: 13, - name: 'test', - value: 'test', - }) - : mockErrors.MIN_LENGTH, + mockErrors.minLength({ + label: 'test', + minLength: 13, + value: 'test', + }), ), ).toBeVisible() }) diff --git a/packages/form/src/components/TextInputField/index.tsx b/packages/form/src/components/TextInputField/index.tsx index fbdc7820ed..e7005d7a75 100644 --- a/packages/form/src/components/TextInputField/index.tsx +++ b/packages/form/src/components/TextInputField/index.tsx @@ -1,209 +1,202 @@ import { TextInput } from '@ultraviolet/ui' -import type { FieldState } from 'final-form' -import type { ComponentProps, JSX, Ref } from 'react' -import { forwardRef } from 'react' -import { useFormField } from '../../hooks' +import type { ComponentProps } from 'react' +import type { FieldValues, Path, PathValue } from 'react-hook-form' +import { Controller } from 'react-hook-form' import { useErrors } from '../../providers' import type { BaseFieldProps } from '../../types' -type TextInputValue = NonNullable['value']> +type TextInputFieldProps = + BaseFieldProps & + Partial< + Pick< + ComponentProps, + | 'autoCapitalize' + | 'autoComplete' + | 'autoCorrect' + | 'autoFocus' + | 'autoSave' + | 'cols' + | 'disabled' + | 'fillAvailable' + | 'generated' + | 'id' + | 'label' + | 'multiline' + | 'notice' + | 'onBlur' + | 'onChange' + | 'onFocus' + | 'onKeyDown' + | 'onKeyUp' + | 'placeholder' + | 'random' + | 'readOnly' + | 'required' + | 'resizable' + | 'rows' + | 'type' + | 'noTopLabel' + | 'unit' + | 'valid' + | 'size' + | 'maxLength' + | 'minLength' + | 'min' + | 'max' + > + > & { + className?: string + /** + * @deprecated Use rules instead + */ + regex?: (RegExp | RegExp[])[] -type TextInputFieldProps = BaseFieldProps< - T, - K -> & - Partial< - Pick< - ComponentProps, - | 'autoCapitalize' - | 'autoComplete' - | 'autoCorrect' - | 'autoFocus' - | 'autoSave' - | 'cols' - | 'data-testid' - | 'disabled' - | 'fillAvailable' - | 'generated' - | 'id' - | 'label' - | 'maxLength' - | 'minLength' - | 'multiline' - | 'notice' - | 'onBlur' - | 'onChange' - | 'onFocus' - | 'onKeyDown' - | 'onKeyUp' - | 'placeholder' - | 'random' - | 'readOnly' - | 'required' - | 'resizable' - | 'rows' - | 'type' - | 'value' - | 'noTopLabel' - | 'unit' - | 'valid' - | 'size' - > - > & { - name: string - className?: string - max?: number - min?: number - regex?: (RegExp | RegExp[])[] - } + format?: (value: unknown) => PathValue> -export const TextInputField = forwardRef( - ( - { - afterSubmit, - allowNull, - autoCapitalize, - autoComplete, - autoCorrect, - autoFocus, - autoSave, - beforeSubmit, - className, - cols, - 'data-testid': dataTestId, - defaultValue, - disabled, - fillAvailable, - format, - formatOnBlur, - generated, - id, - initialValue, - isEqual, - label = '', - max, - maxLength, - min, - minLength, - multiline, - multiple, - name, - noTopLabel, - notice, - onBlur, - onChange, - onFocus, - onKeyDown, - onKeyUp, - parse, - placeholder, - random, - readOnly, - regex, - required, - resizable, - rows, - subscription, - type, - unit, - size, - validate, - validateFields, - valid, - value, - }: TextInputFieldProps, - ref: Ref, - ): JSX.Element => { - const { getError } = useErrors() + parse?: (value: string) => PathValue> + } - const { input, meta } = useFormField(name, { - afterSubmit, - allowNull, - beforeSubmit, - defaultValue, - disabled, - format, - formatOnBlur, - initialValue, - isEqual, - max, - maxLength, - min, - minLength, - multiple, - parse, - regex, - required, - subscription, - type, - validate, - validateFields, - value, - }) +export const TextInputField = ({ + autoCapitalize, + autoComplete, + autoCorrect, + autoFocus, + autoSave, + className, + cols, + disabled, + fillAvailable, + generated, + id, + label = '', + multiline, + name, + noTopLabel, + notice, + onChange, + onFocus, + onKeyDown, + onKeyUp, + onBlur, + placeholder, + random, + readOnly, + required, + resizable, + rows, + type, + unit, + size, + rules, + valid, + parse, + format, + regex: regexes, + min, + max, + minLength, + maxLength, + validate, + defaultValue, +}: TextInputFieldProps) => { + const { getError } = useErrors() - const error = getError({ - label, - max, - maxLength, - meta: meta as FieldState, - min, - minLength, - name, - regex, - value: input.value, - }) + return ( + + name={name} + rules={{ + required, + pattern: + regexes && + new RegExp( + regexes + .map(regex => { + const newRegex = Array.isArray(regex) + ? regex.map(reg => reg.source).join('|') + : regex.source - return ( - { - input.onBlur(event) - onBlur?.(event) - }} - onChange={event => { - input.onChange(event) - onChange?.(event) - }} - onFocus={event => { - input.onFocus(event) - onFocus?.(event) - }} - onKeyUp={onKeyUp} - onKeyDown={onKeyDown} - placeholder={placeholder} - random={random} - readOnly={readOnly} - ref={ref} - required={required} - resizable={resizable} - rows={rows} - type={input.type} - value={input.value} - noTopLabel={noTopLabel} - unit={unit} - valid={valid} - size={size} - /> - ) - }, -) + return `(?=${newRegex})` + }) + .join(''), + ), + validate, + minLength, + maxLength, + max, + min, + ...rules, + }} + defaultValue={defaultValue} + render={({ field, fieldState: { error } }) => { + const transformedValue = () => { + if (format) { + return format(field.value) as string + } + + return field.value as string + } + + return ( + { + field.onBlur() + onBlur?.(event) + }} + onChange={event => { + if (parse) { + field.onChange(parse(event)) + } else { + field.onChange(event) + } + onChange?.(event) + }} + onFocus={event => { + onFocus?.(event) + }} + onKeyUp={onKeyUp} + onKeyDown={onKeyDown} + placeholder={placeholder} + random={random} + readOnly={readOnly} + resizable={resizable} + rows={rows} + type={type} + noTopLabel={noTopLabel} + unit={unit} + valid={valid} + size={size} + value={transformedValue()} + /> + ) + }} + /> + ) +} diff --git a/packages/form/src/components/TimeField/__tests__/__snapshots__/index.spec.tsx.snap b/packages/form/src/components/TimeField/__tests__/__snapshots__/index.spec.tsx.snap index 115b42b515..7a719e2c24 100644 --- a/packages/form/src/components/TimeField/__tests__/__snapshots__/index.spec.tsx.snap +++ b/packages/form/src/components/TimeField/__tests__/__snapshots__/index.spec.tsx.snap @@ -283,12 +283,10 @@ exports[`TimeField should render correctly 1`] = ` padding-top: 2px; } - +
+
@@ -664,12 +667,10 @@ exports[`TimeField should render correctly checked without value 1`] = ` padding-top: 2px; } - +
+
@@ -1032,12 +1038,10 @@ exports[`TimeField should render correctly disabled 1`] = ` padding-top: 2px; } - +
+
+
diff --git a/packages/form/src/components/TimeField/__tests__/index.spec.tsx b/packages/form/src/components/TimeField/__tests__/index.spec.tsx index 590f26fdb5..1c96df22cd 100644 --- a/packages/form/src/components/TimeField/__tests__/index.spec.tsx +++ b/packages/form/src/components/TimeField/__tests__/index.spec.tsx @@ -54,7 +54,7 @@ describe('TimeField', () => { fireEvent.keyDown(select, { key: 'ArrowDown', keyCode: 40 }) const option = // eslint-disable-next-line testing-library/no-node-access - screen.getByTestId('option--01:00').firstChild as HTMLElement + screen.getByTestId('option-test-01:00').firstChild as HTMLElement act(() => option.click()) expect(onChange).toBeCalledTimes(1) act(() => select.blur()) diff --git a/packages/form/src/components/TimeField/index.tsx b/packages/form/src/components/TimeField/index.tsx index 074462417b..33e0b5e7ac 100644 --- a/packages/form/src/components/TimeField/index.tsx +++ b/packages/form/src/components/TimeField/index.tsx @@ -1,7 +1,7 @@ import { TimeInput } from '@ultraviolet/ui' import type { ComponentProps } from 'react' -import { useMemo } from 'react' -import { useFormField } from '../../hooks' +import type { FieldValues } from 'react-hook-form' +import { Controller } from 'react-hook-form' import type { BaseFieldProps } from '../../types' const parseTime = (date?: Date | string): { label: string; value: string } => { @@ -16,89 +16,80 @@ const parseTime = (date?: Date | string): { label: string; value: string } => { } } -type TimeFieldProps = BaseFieldProps & - ComponentProps & { - name: string - } +type TimeFieldProps = + BaseFieldProps & + ComponentProps & { + name: string + } -export const TimeField = ({ +export const TimeField = ({ required, name, schedule, placeholder, disabled, - initialValue, - validate, readOnly, - value, - onChange, onBlur, onFocus, + onChange, isLoading, isClearable, inputId, id, - formatOnBlur, animation, animationDuration, animationOnChange, className, isSearchable, + rules, options, 'data-testid': dataTestId, -}: TimeFieldProps) => { - const { input, meta } = useFormField(name, { - disabled, - formatOnBlur, - initialValue, - required, - validate, - value, - }) - - const error = useMemo( - () => (input.value && meta.error ? (meta.error as string) : undefined), - [input.value, meta.error], - ) - - return ( - { - if (!val) return - onChange?.(val, action) - const [hours, minutes] = ( - val as { value: string; label: string } - ).value.split(':') - const date = input.value ? new Date(input.value) : new Date() - date.setHours(Number(hours), Number(minutes), 0) - input.onChange(date) - }} - onBlur={event => { - input.onBlur(event) - onBlur?.(event) - }} - onFocus={event => { - input.onFocus(event) - onFocus?.(event) - }} - error={error} - disabled={disabled} - readOnly={readOnly} - animation={animation} - animationDuration={animationDuration} - animationOnChange={animationOnChange} - className={className} - isLoading={isLoading} - isClearable={isClearable} - isSearchable={isSearchable} - inputId={inputId} - id={id} - options={options} - data-testid={dataTestId} - /> - ) -} +}: TimeFieldProps) => ( + ( + { + if (!val) return + onChange?.(val, action) + const [hours, minutes] = ( + val as { value: string; label: string } + ).value.split(':') + const date = field.value ? new Date(field.value) : new Date() + date.setHours(Number(hours), Number(minutes), 0) + field.onChange(date) + }} + onBlur={event => { + field.onBlur() + onBlur?.(event) + }} + onFocus={event => { + onFocus?.(event) + }} + error={error?.message} + disabled={disabled} + readOnly={readOnly} + animation={animation} + animationDuration={animationDuration} + animationOnChange={animationOnChange} + className={className} + isLoading={isLoading} + isClearable={isClearable} + isSearchable={isSearchable} + inputId={inputId} + id={id} + options={options} + data-testid={dataTestId} + /> + )} + /> +) diff --git a/packages/form/src/components/ToggleField/__stories__/ActAsRadio.stories.tsx b/packages/form/src/components/ToggleField/__stories__/ActAsRadio.stories.tsx index 904b6da876..14e543f830 100644 --- a/packages/form/src/components/ToggleField/__stories__/ActAsRadio.stories.tsx +++ b/packages/form/src/components/ToggleField/__stories__/ActAsRadio.stories.tsx @@ -3,6 +3,5 @@ import { Template } from './Template.stories' export const ActAsRadio = Template.bind({}) ActAsRadio.args = { - initialValue: true, name: 'default', } diff --git a/packages/form/src/components/ToggleField/__stories__/Checked.stories.tsx b/packages/form/src/components/ToggleField/__stories__/Checked.stories.tsx index 8bc2c396eb..72be5b5d9f 100644 --- a/packages/form/src/components/ToggleField/__stories__/Checked.stories.tsx +++ b/packages/form/src/components/ToggleField/__stories__/Checked.stories.tsx @@ -3,7 +3,6 @@ import { Template } from './Template.stories' export const Checked = Template.bind({}) Checked.args = { - initialValue: ['test'], name: 'default', - value: 'test', + value: true, } diff --git a/packages/form/src/components/ToggleField/__tests__/__snapshots__/index.spec.tsx.snap b/packages/form/src/components/ToggleField/__tests__/__snapshots__/index.spec.tsx.snap index a5bfd3bdb3..31b05135ab 100644 --- a/packages/form/src/components/ToggleField/__tests__/__snapshots__/index.spec.tsx.snap +++ b/packages/form/src/components/ToggleField/__tests__/__snapshots__/index.spec.tsx.snap @@ -145,9 +145,7 @@ exports[`ToggleField should render correctly 1`] = ` cursor: not-allowed; } - +