Skip to content

Commit 900db9d

Browse files
JulienSaguezjsulpisradhi-nasser-scaleway
authored
feat(RichTextInput): create new component (#6318)
* feat(RichTextEditor): create new component * fix(RichTextEditor): fix threads and move component in compositions folder * fix(RichTextEditor): fix tests * fix(RichTextEditor): fix anothers threads * fix(RichTextEditor): add errorLabel prop * fix(RichTextEditor): update snapshots * fix(RichTextEditor): fix threads part one * fix(RichTextInput): fix threads part two and rename to richtextinput * fix(RichTextInput): fix another threads * fix(RichTextInput): update snapshot * fix(RichTextInput): fix last threads * fix(RichTextInput): fix snapshot * feat(RichTextInput): fix threads * fix(RichTextInput): fix format * fix: statusIcon positionning * fix: add missing sizes and fix styles * fix: padding with status Icon * fix: enter in bullet list not behaving correctly * fix: test Signed-off-by: Alexandre Philibeaux <aphilibeaux@scaleway.com> * fix: lint * fix: type error --------- Signed-off-by: Alexandre Philibeaux <aphilibeaux@scaleway.com> Co-authored-by: Julien Sulpis <jsulpis@scaleway.com> Co-authored-by: Radhi NASSER <rnasser@scaleway.com>
1 parent e59646e commit 900db9d

30 files changed

Lines changed: 1858 additions & 0 deletions

.changeset/fluffy-clowns-repair.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@ultraviolet/form": minor
3+
"@ultraviolet/ui": minor
4+
---
5+
6+
`RichTextInput`: create component `RichTextInput` and `RichTextInputField`
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Template } from './Template.stories'
2+
3+
export const Playground = Template.bind({})
4+
5+
Playground.args = Template.args
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Template } from './Template.stories'
2+
3+
export const Required = Template.bind({})
4+
5+
Required.args = { ...Template.args, required: true }
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { StoryFn } from '@storybook/react-vite'
2+
import { Stack } from '@ultraviolet/ui'
3+
import type { ComponentProps } from 'react'
4+
import { RichTextInputField } from '..'
5+
import { Submit } from '../../../components'
6+
7+
export const Template: StoryFn<ComponentProps<typeof RichTextInputField>> = args => (
8+
<Stack gap={1}>
9+
<RichTextInputField {...args} />
10+
<Submit>Submit</Submit>
11+
</Stack>
12+
)
13+
14+
Template.args = {
15+
label: 'Label',
16+
name: 'richTextInput',
17+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { Meta } from '@storybook/react-vite'
2+
import { Snippet, Stack, Text } from '@ultraviolet/ui'
3+
import { RichTextInputField } from '..'
4+
import { useForm } from '../../..'
5+
import { Form } from '../../../components'
6+
import { mockErrors } from '../../../mocks'
7+
8+
export default {
9+
component: RichTextInputField,
10+
decorators: [
11+
ChildStory => {
12+
const methods = useForm()
13+
const {
14+
errors,
15+
isDirty,
16+
isSubmitting,
17+
touchedFields,
18+
submitCount,
19+
dirtyFields,
20+
isValid,
21+
isLoading,
22+
isSubmitted,
23+
isValidating,
24+
isSubmitSuccessful,
25+
} = methods.formState
26+
27+
return (
28+
<Form errors={mockErrors} methods={methods} onSubmit={() => {}}>
29+
<Stack gap={2}>
30+
<ChildStory />
31+
<Stack gap={1}>
32+
<Text as="p" variant="bodyStrong">
33+
Form input values:
34+
</Text>
35+
<Snippet initiallyExpanded prefix="lines">
36+
{JSON.stringify(methods.watch(), null, 1)}
37+
</Snippet>
38+
</Stack>
39+
<Stack gap={1}>
40+
<Text as="p" variant="bodyStrong">
41+
Form values:
42+
</Text>
43+
<Snippet prefix="lines">
44+
{JSON.stringify(
45+
{
46+
errors,
47+
isDirty,
48+
isSubmitting,
49+
touchedFields,
50+
submitCount,
51+
dirtyFields,
52+
isValid,
53+
isLoading,
54+
isSubmitted,
55+
isValidating,
56+
isSubmitSuccessful,
57+
},
58+
null,
59+
1,
60+
)}
61+
</Snippet>
62+
</Stack>
63+
</Stack>
64+
</Form>
65+
)
66+
},
67+
],
68+
title: 'Form/Components/Compositions/RichTextInputField',
69+
} as Meta
70+
71+
export { Playground } from './Playground.stories'
72+
export { Required } from './Required.stories'

packages/form/src/compositions/RichTextInputField/__tests__/__snapshots__/index.test.tsx.snap

Lines changed: 381 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { renderHook, screen, waitFor } from '@testing-library/react'
2+
import { userEvent } from '@testing-library/user-event'
3+
import { mockFormErrors, renderWithForm, renderWithTheme } from '@utils/test'
4+
import { useForm } from 'react-hook-form'
5+
import { beforeAll, describe, expect, it, vi } from 'vitest'
6+
import { RichTextInputField } from '..'
7+
import { Submit } from '../../../components'
8+
import { Form } from '../../../components/Form'
9+
10+
describe('richTextInputField', () => {
11+
beforeAll(() => {
12+
Object.defineProperty(document, 'elementFromPoint', {
13+
value: vi.fn().mockReturnValue(null),
14+
writable: true,
15+
configurable: true,
16+
})
17+
})
18+
it('should render correctly', () => {
19+
const { asFragment } = renderWithForm(<RichTextInputField label="Test" name="test" />)
20+
21+
expect(asFragment()).toMatchSnapshot()
22+
})
23+
24+
it('should render correctly generated', async () => {
25+
const onSubmit = vi.fn()
26+
const { result } = renderHook(() => useForm<{ test: string }>({ defaultValues: { test: '' } }))
27+
28+
const { asFragment } = renderWithTheme(
29+
<Form errors={mockFormErrors} methods={result.current} onSubmit={onSubmit}>
30+
<RichTextInputField label="Test" name="test" required />
31+
<Submit>Submit</Submit>
32+
</Form>,
33+
)
34+
35+
await userEvent.click(screen.getByRole('button', { name: 'Submit' }))
36+
await waitFor(() => {
37+
expect(onSubmit).not.toHaveBeenCalled()
38+
})
39+
40+
const doc = document.querySelector<HTMLDivElement>('[contenteditable="true"]')
41+
expect(doc).not.toBeNull()
42+
const editor = doc!
43+
await userEvent.click(editor)
44+
await userEvent.type(editor, 'This is an example')
45+
await userEvent.click(screen.getByRole('button', { name: 'Submit' }))
46+
47+
await waitFor(() => {
48+
expect(onSubmit).toHaveBeenCalledOnce()
49+
expect(onSubmit).toHaveBeenCalledWith({
50+
test: '<p>This is an example</p>',
51+
})
52+
})
53+
expect(asFragment()).toMatchSnapshot()
54+
})
55+
56+
it('should submit rich text with style and list', async () => {
57+
const onSubmit = vi.fn()
58+
const { result } = renderHook(() => useForm<{ test: string }>({ defaultValues: { test: '' } }))
59+
60+
renderWithTheme(
61+
<Form errors={mockFormErrors} methods={result.current} onSubmit={onSubmit}>
62+
<RichTextInputField label="Test" name="test" required />
63+
<Submit>Submit</Submit>
64+
</Form>,
65+
)
66+
67+
const italicButton = screen.getByRole('button', { name: 'Italic' })
68+
const bulletListButton = screen.getByRole('button', { name: 'Bullet List' })
69+
expect(italicButton).not.toBeNull()
70+
expect(bulletListButton).not.toBeNull()
71+
72+
const doc = document.querySelector<HTMLDivElement>('[contenteditable="true"]')
73+
expect(doc).not.toBeNull()
74+
const editor = doc!
75+
await userEvent.click(editor)
76+
await userEvent.click(italicButton)
77+
await userEvent.type(editor, 'Styled ')
78+
await userEvent.click(bulletListButton)
79+
await userEvent.type(editor, 'item')
80+
await userEvent.click(screen.getByRole('button', { name: 'Submit' }))
81+
82+
await waitFor(() => {
83+
expect(onSubmit).toHaveBeenCalledOnce()
84+
expect(onSubmit).toHaveBeenCalledWith({
85+
test: '<ul><li><p><em>Styled item</em></p></li></ul>',
86+
})
87+
})
88+
})
89+
})
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use client'
2+
3+
import { RichTextInput } from '@ultraviolet/ui/compositions/RichTextInput'
4+
import type { ComponentProps, FocusEvent } from 'react'
5+
import { useController } from 'react-hook-form'
6+
import type { FieldPath, FieldValues, Path, PathValue } from 'react-hook-form'
7+
import { useErrors } from '../../providers'
8+
import type { BaseFieldProps } from '../../types'
9+
10+
export type RichTextInputFieldProps<
11+
TFieldValues extends FieldValues,
12+
TFieldName extends FieldPath<TFieldValues>,
13+
> = BaseFieldProps<TFieldValues, TFieldName> &
14+
Omit<ComponentProps<typeof RichTextInput>, 'value' | 'onChange' | 'error'>
15+
16+
export const RichTextInputField = <
17+
TFieldValues extends FieldValues,
18+
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
19+
>({
20+
control,
21+
errorLabel,
22+
label,
23+
onChange,
24+
name,
25+
onBlur,
26+
required = false,
27+
validate,
28+
'aria-label': ariaLabel,
29+
...props
30+
}: RichTextInputFieldProps<TFieldValues, TFieldName>) => {
31+
const { getError } = useErrors()
32+
33+
const {
34+
field,
35+
fieldState: { error },
36+
} = useController<TFieldValues, TFieldName>({
37+
control,
38+
name,
39+
rules: {
40+
required,
41+
validate,
42+
},
43+
})
44+
45+
return (
46+
<RichTextInput
47+
{...props}
48+
error={getError(
49+
{
50+
label: errorLabel ?? label ?? ariaLabel ?? name,
51+
value: field.value,
52+
},
53+
error,
54+
)}
55+
onBlur={(event: FocusEvent<HTMLElement>) => {
56+
onBlur?.(event)
57+
field.onBlur()
58+
}}
59+
onChange={value => {
60+
field.onChange(value)
61+
onChange?.(value as PathValue<TFieldValues, Path<TFieldValues>>)
62+
}}
63+
value={field.value}
64+
{...(label ? { label } : { 'aria-label': ariaLabel! })}
65+
/>
66+
)
67+
}

packages/form/src/compositions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export { CustomerSatisfactionField } from './CustomerSatisfactionField'
33
export { OfferListField } from './OfferListField'
44
export { OptionSelectorField } from './OptionSelectorField'
55
export { PlansField } from './PlansField'
6+
export { RichTextInputField } from './RichTextInputField'

packages/ui/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
"watch:build": "vite build --watch"
7474
},
7575
"dependencies": {
76+
"@handlewithcare/react-prosemirror": "3.0.0",
7677
"@nivo/bar": "0.99.0",
7778
"@nivo/core": "0.99.0",
7879
"@nivo/line": "0.99.0",
@@ -97,6 +98,13 @@
9798
"codemirror": "6.0.2",
9899
"csstype": "3.2.3",
99100
"deepmerge": "4.3.1",
101+
"prosemirror-commands": "1.5.0",
102+
"prosemirror-keymap": "1.2.1",
103+
"prosemirror-model": "1.25.1",
104+
"prosemirror-schema-basic": "1.2.2",
105+
"prosemirror-schema-list": "1.2.2",
106+
"prosemirror-state": "1.4.3",
107+
"prosemirror-view": "1.41.4",
100108
"react-intersection-observer": "10.0.3",
101109
"react-toastify": "11.1.0"
102110
},

0 commit comments

Comments
 (0)