diff --git a/.changeset/red-buttons-eat.md b/.changeset/red-buttons-eat.md new file mode 100644 index 0000000000..7377f1209f --- /dev/null +++ b/.changeset/red-buttons-eat.md @@ -0,0 +1,5 @@ +--- +"@ultraviolet/form": minor +--- + +Add `` diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6b47b59741..d4e9814c49 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,15 +1,15 @@ -* @matthprost @lisalupi -/packages/form @johnrazeur -**/package.json @scaleway/front-kernel -Dockerfile @scaleway/front-kernel -babel.config.json @scaleway/front-kernel -tsconfig.json @scaleway/front-kernel -vite.config.ts @scaleway/front-kernel -turbo.json @scaleway/front-kernel -biome.json @scaleway/front-kernel -eslint.config.mjs @scaleway/front-kernel -pnpm-workspace.yaml @scaleway/front-kernel -svgo.config.cjs @scaleway/front-kernel -.github @scaleway/front-kernel -.changeset @scaleway/front-kernel -.aws @scaleway/front-kernel +* @matthprost @lisalupi +/packages/form @johnrazeur +**/package.json @scaleway/front-kernel +Dockerfile @scaleway/front-kernel +babel.config.json @scaleway/front-kernel +tsconfig.json @scaleway/front-kernel +vite.config.ts @scaleway/front-kernel +turbo.json @scaleway/front-kernel +biome.json @scaleway/front-kernel +eslint.config.mjs @scaleway/front-kernel +pnpm-workspace.yaml @scaleway/front-kernel +svgo.config.cjs @scaleway/front-kernel +.github @scaleway/front-kernel +.changeset/config.json @scaleway/front-kernel +.aws @scaleway/front-kernel diff --git a/packages/form/src/components/VerificationCodeField/__stories__/Playground.stories.tsx b/packages/form/src/components/VerificationCodeField/__stories__/Playground.stories.tsx new file mode 100644 index 0000000000..5fd6bef5ce --- /dev/null +++ b/packages/form/src/components/VerificationCodeField/__stories__/Playground.stories.tsx @@ -0,0 +1,5 @@ +import { Template } from './Template.stories' + +export const Playground = Template.bind({}) + +Playground.args = Template.args diff --git a/packages/form/src/components/VerificationCodeField/__stories__/Required.stories.tsx b/packages/form/src/components/VerificationCodeField/__stories__/Required.stories.tsx new file mode 100644 index 0000000000..5a8e3ffe10 --- /dev/null +++ b/packages/form/src/components/VerificationCodeField/__stories__/Required.stories.tsx @@ -0,0 +1,17 @@ +import type { StoryFn } from '@storybook/react' +import { Stack } from '@ultraviolet/ui' +import type { ComponentProps } from 'react' +import { VerificationCodeField } from '..' +import { Submit } from '../../Submit' +import { Template } from './Template.stories' + +export const Required: StoryFn< + ComponentProps +> = args => ( + + + Submit + +) + +Required.args = { ...Template.args, required: true } diff --git a/packages/form/src/components/VerificationCodeField/__stories__/Template.stories.tsx b/packages/form/src/components/VerificationCodeField/__stories__/Template.stories.tsx new file mode 100644 index 0000000000..5b86f897c0 --- /dev/null +++ b/packages/form/src/components/VerificationCodeField/__stories__/Template.stories.tsx @@ -0,0 +1,16 @@ +import type { StoryFn } from '@storybook/react' +import { Stack } from '@ultraviolet/ui' +import type { ComponentProps } from 'react' +import { VerificationCodeField } from '..' +import { Submit } from '../..' + +export const Template: StoryFn< + ComponentProps +> = args => ( + + + Submit + +) + +Template.args = { name: 'verification' } diff --git a/packages/form/src/components/VerificationCodeField/__stories__/index.stories.tsx b/packages/form/src/components/VerificationCodeField/__stories__/index.stories.tsx new file mode 100644 index 0000000000..95e6c24f1b --- /dev/null +++ b/packages/form/src/components/VerificationCodeField/__stories__/index.stories.tsx @@ -0,0 +1,71 @@ +import type { Meta } from '@storybook/react' +import { Snippet, Stack, Text } from '@ultraviolet/ui' +import { Form, VerificationCodeField } from '../..' +import { useForm } from '../../..' +import { mockErrors } from '../../../mocks' + +export default { + component: VerificationCodeField, + decorators: [ + ChildStory => { + const methods = useForm() + const { + errors, + isDirty, + isSubmitting, + touchedFields, + submitCount, + dirtyFields, + isValid, + isLoading, + isSubmitted, + isValidating, + isSubmitSuccessful, + } = methods.formState + + return ( +
{}} errors={mockErrors} methods={methods}> + + + + + Form input values: + + + {JSON.stringify(methods.watch(), null, 1)} + + + + + Form values: + + + {JSON.stringify( + { + errors, + isDirty, + isSubmitting, + touchedFields, + submitCount, + dirtyFields, + isValid, + isLoading, + isSubmitted, + isValidating, + isSubmitSuccessful, + }, + null, + 1, + )} + + + +
+ ) + }, + ], + title: 'Form/Components/Fields/VerificationCodeField', +} as Meta + +export { Playground } from './Playground.stories' +export { Required } from './Required.stories' diff --git a/packages/form/src/components/VerificationCodeField/__tests__/__snapshots__/index.test.tsx.snap b/packages/form/src/components/VerificationCodeField/__tests__/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000..c33a8f8b2f --- /dev/null +++ b/packages/form/src/components/VerificationCodeField/__tests__/__snapshots__/index.test.tsx.snap @@ -0,0 +1,159 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`VerificationCodeField > should render correctly 1`] = ` + + .emotion-0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + gap: 8px; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; + -webkit-box-flex-wrap: nowrap; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; +} + +.emotion-2 { + font-size: 16px; + font-family: Inter,Asap,sans-serif; + font-weight: 400; + letter-spacing: 0; + line-height: 24px; + text-transform: none; + -webkit-text-decoration: none; + text-decoration: none; +} + +.emotion-4 { + background: #ffffff; + border: solid 1px #d9dadd; + color: #3f4250; + font-size: 16px; + font-weight: 400; + text-align: center; + border-radius: 4px; + margin-right: 8px; + width: 40px; + height: 48px; + outline-style: none; + -webkit-transition: border-color 0.2s ease,box-shadow 0.2s ease; + transition: border-color 0.2s ease,box-shadow 0.2s ease; +} + +.emotion-4:hover, +.emotion-4:focus { + border-color: #792dd4; +} + +.emotion-4:focus { + box-shadow: 0px 0px 0px 3px #8c40ef40; +} + +.emotion-4:last-child { + margin-right: 0; +} + +.emotion-4::-webkit-input-placeholder { + color: #727683; +} + +.emotion-4::-moz-placeholder { + color: #727683; +} + +.emotion-4:-ms-input-placeholder { + color: #727683; +} + +.emotion-4::placeholder { + color: #727683; +} + +
+
+
+ +
+ + + + +
+
+
+
+
+`; diff --git a/packages/form/src/components/VerificationCodeField/__tests__/index.test.tsx b/packages/form/src/components/VerificationCodeField/__tests__/index.test.tsx new file mode 100644 index 0000000000..da7bee0ff3 --- /dev/null +++ b/packages/form/src/components/VerificationCodeField/__tests__/index.test.tsx @@ -0,0 +1,17 @@ +import { renderWithForm } from '@utils/test' +import { describe, expect, test } from 'vitest' +import { VerificationCodeField } from '..' + +describe('VerificationCodeField', () => { + test('should render correctly', () => { + const { asFragment } = renderWithForm( + , + ) + expect(asFragment()).toMatchSnapshot() + }) +}) diff --git a/packages/form/src/components/VerificationCodeField/index.tsx b/packages/form/src/components/VerificationCodeField/index.tsx new file mode 100644 index 0000000000..173b76cb65 --- /dev/null +++ b/packages/form/src/components/VerificationCodeField/index.tsx @@ -0,0 +1,118 @@ +import { Stack, Text, VerificationCode } from '@ultraviolet/ui' +import type { ComponentProps } from 'react' +import type { FieldPath, FieldValues } from 'react-hook-form' +import { useController } from 'react-hook-form' +import { useErrors } from '../../providers' +import type { BaseFieldProps } from '../../types' + +type VerificationCodeFieldProps< + TFieldValues extends FieldValues, + TName extends FieldPath, +> = BaseFieldProps & + Partial< + Pick< + ComponentProps, + | 'disabled' + | 'error' + | 'fields' + | 'initialValue' + | 'onChange' + | 'onComplete' + | 'placeholder' + | 'required' + | 'type' + > + > & { + className?: string + id?: string + name: string + label?: string + } + +export const VerificationCodeField = < + TFieldValues extends FieldValues, + TName extends FieldPath = FieldPath, +>({ + className, + fields, + id = 'verification-code-input', + label, + name, + onChange, + onComplete, + placeholder, + required, + type = 'number', + disabled, + validate, +}: VerificationCodeFieldProps) => { + const { getError } = useErrors() + + const { + field, + fieldState: { error }, + } = useController({ + name, + rules: { + required, + validate: { + required: localValue => { + if (required && localValue.length !== (fields ?? 4)) { + return false + } + + return true + }, + ...validate, + }, + }, + }) + + return ( + + {label ? ( + + ) : null} + + { + onChange?.(event) + field.onChange(event) + }} + onComplete={event => { + onComplete?.(event) + }} + type={type} + disabled={disabled} + required={required} + /> + {error ? ( + + {getError({ label: label || 'verification-code-field' }, error)} + + ) : null} + + ) +} diff --git a/packages/form/src/components/index.ts b/packages/form/src/components/index.ts index c217ec375b..e7f4fffb85 100644 --- a/packages/form/src/components/index.ts +++ b/packages/form/src/components/index.ts @@ -21,3 +21,4 @@ export { SelectableCardGroupField } from './SelectableCardGroupField' export { SelectInputFieldV2 } from './SelectInputFieldV2' export { UnitInputField } from './UnitInputField' export { SliderField } from './SliderField' +export { VerificationCodeField } from './VerificationCodeField'