Skip to content

feat(RichTextInput): create new component#6318

Open
JulienSaguez wants to merge 8 commits intomainfrom
feat/rich-text-editor
Open

feat(RichTextInput): create new component#6318
JulienSaguez wants to merge 8 commits intomainfrom
feat/rich-text-editor

Conversation

@JulienSaguez
Copy link
Copy Markdown
Contributor

@JulienSaguez JulienSaguez commented Apr 9, 2026

Summary

Type

  • Feature

Summarize concisely:

What is expected?

RichTextInput: create component RichTextInput and RichTextInputField

The following changes were made:

(Describe what you did)

Relevant logs and/or screenshots

Page Before After
url screenshot screenshot
url screenshot screenshot

@JulienSaguez JulienSaguez self-assigned this Apr 9, 2026
@JulienSaguez JulienSaguez requested review from a team and lisalupi as code owners April 9, 2026 08:54
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 9, 2026

🦋 Changeset detected

Latest commit: 63e7331

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@ultraviolet/form Minor
@ultraviolet/ui Minor
@ultraviolet/nextjs Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 9, 2026

Codecov Report

❌ Patch coverage is 83.83838% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.44%. Comparing base (2b89546) to head (63e7331).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...ages/ui/src/compositions/RichTextInput/Toolbar.tsx 68.51% 15 Missing and 2 partials ⚠️
...kages/ui/src/compositions/RichTextInput/helpers.ts 64.70% 6 Missing ⚠️
...es/ui/src/compositions/RichTextInput/editorCore.ts 88.88% 4 Missing and 1 partial ⚠️
...kages/ui/src/compositions/RichTextInput/Notice.tsx 81.81% 2 Missing ⚠️
...ckages/ui/src/compositions/RichTextInput/index.tsx 95.83% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #6318      +/-   ##
==========================================
+ Coverage   83.30%   92.44%   +9.13%     
==========================================
  Files          44      540     +496     
  Lines         683    10234    +9551     
  Branches      195     3913    +3718     
==========================================
+ Hits          569     9461    +8892     
- Misses        104      706     +602     
- Partials       10       67      +57     
Files with missing lines Coverage Δ
...form/src/compositions/RichTextInputField/index.tsx 100.00% <100.00%> (ø)
...es/ui/src/compositions/RichTextInput/styles.css.ts 100.00% <100.00%> (ø)
...kages/ui/src/compositions/RichTextInput/Notice.tsx 81.81% <81.81%> (ø)
...ckages/ui/src/compositions/RichTextInput/index.tsx 95.83% <95.83%> (ø)
...es/ui/src/compositions/RichTextInput/editorCore.ts 88.88% <88.88%> (ø)
...kages/ui/src/compositions/RichTextInput/helpers.ts 64.70% <64.70%> (ø)
...ages/ui/src/compositions/RichTextInput/Toolbar.tsx 68.51% <68.51%> (ø)

... and 489 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e4ae00d...63e7331. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

{...props}
error={getError(
{
label: label ?? ariaLabel ?? name,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
label: label ?? ariaLabel ?? name,
label: errorLabel ?? label ?? ariaLabel ?? name,

errorLabel is a new prop of BaseFieldProps that can be used to customize the error message (see #6241)

field.onChange(value)
onChange?.(value as PathValue<TFieldValues, Path<TFieldValues>>)
}}
value={field.value ?? ''}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why add ?? ''

)
},
],
title: 'Form/Components/Fields/RichTextEditorField',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
title: 'Form/Components/Fields/RichTextEditorField',
title: 'Form/Components/Compositions/RichTextEditorField',


export default {
component: RichTextEditor,
title: 'UI/Data Entry/RichTextEditor',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
title: 'UI/Data Entry/RichTextEditor',
title: 'Compositions/RichTextEditor',

Comment on lines +153 to +156
style={{
minHeight,
...(maxHeight ? { maxHeight } : {}),
}}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not add this in richTextEditorStyle.docRegion paired with assignInlineVars?

sentiment?: 'danger' | 'success' | 'neutral'
}) => (
<Row gap="1" templateColumns="minmax(0, 1fr) min-content">
<div>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't understand the role of this div

})

export const docRegion = style({
lineHeight: 1.5,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
lineHeight: 1.5,
lineHeight: theme.typography.body.lineHeight,

Copy link
Copy Markdown
Collaborator

@lisalupi lisalupi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can copy-paste text that renders as h1, h2, etc., but it is not possible to write headings. Is it ok?

Image

)
},
],
title: 'Compositions/RichTextEditorField',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
title: 'Compositions/RichTextEditorField',
title: 'Form/Components/Compositions/RichTextEditorField',

Comment on lines +75 to +78
const maxHeight =
typeof maxRows === 'number'
? `calc(${lineHeightEm}em * ${maxRows} + 2 * ${padding})`
: undefined
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const maxHeight =
typeof maxRows === 'number'
? `calc(${lineHeightEm}em * ${maxRows} + 2 * ${padding})`
: undefined
const maxHeight =
typeof maxRows === 'number'
? `calc(${lineHeightEm}em * ${maxRows} + 2 * ${padding})`
: 'none'

Just a suggestion

...assignInlineVars({
[docRegionMaxHeightVar]: maxHeight ?? 'none',
}),
minHeight,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minHeight should be an inlineVar too

const isInOrderedList = isSelectionInNodeType(editorState, orderedList)

return (
<Stack alignItems="center" direction="row" gap={1}>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idéalement il faudrait un role="toolbar" comme dans l'exemple ProseMirror. La navigation clavier est aussi différente (flèches pour naviguer entre les boutons, et quand on clique dessus ça remet le focus dans l'éditeur).

disabled={disabled}
size="small"
variant={isMarkActive(editorState, strongMark) ? 'filled' : 'ghost'}
onMouseDown={event => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • utiliser onClick pour supporter le clavier, et qui est déclenché quand on relâche le clic. C'est plus courant que de faire l'action au MouseDown, avant d'avoir relâché
  • pourquoi event.preventDefault() ?
  • il faut un label explicite aux boutons, là c'est le nom de l'icône ("BoldIcon"). Je vois qu'on utilise par endroits des fichiers de traduction en.ts, tu pourrais faire pareil en attendant qu'on trouve une meilleure solution

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Utiliser onMouseDown permet runCommand avant le changement de focus
  • L'event.preventDefault permet d'empecher le comportement par defaut d'un clic sur un bouton qui est de prendre le focus
  • Je rajoute les labels

return (
<Stack gap="0.5">
{label ? (
<Label htmlFor={id ?? localId} required={required}>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues :

  • the localId is not used on the editor so the label points to nothing
  • a label element cannot be linked to a div. You can use an id on the label and aria-labelledby on the editor instead

}),
className,
)}
data-disabled={disabled ? 'true' : undefined}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need this data-disabled attribute ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effectivement il n'est pas utile

disabled?: boolean
sentiment?: 'danger' | 'success' | 'neutral'
}) => (
<Row gap="1" templateColumns="minmax(0, 1fr) min-content">
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the container of the Notice message should have:

  • an id paired with an aria-describedby attribute on the text editor to link the message to the editor
  • a role="status" which implicitely add an aria-live="polite" so that screen readers read the status message when it's updated

Comment on lines +160 to +161
onBlur={onBlur}
onFocus={onFocus}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the blur and focus events don't bubble, did you check that you actually receive them here ? I don't see them in the proseMirror doc

</Form>,
)

await userEvent.click(screen.getByText('Submit'))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

always prefer getByRole, idem in the other tests below

Suggested change
await userEvent.click(screen.getByText('Submit'))
await userEvent.click(screen.getByRole('button', { name: 'Submit' }))

await waitFor(() => {
expect(onSubmit).toHaveBeenCalledOnce()
expect(onSubmit.mock.calls[0]?.[0]?.test).toEqual(
expect.stringContaining('This is an example'),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you check the whole string ?


await waitFor(() => {
expect(onSubmit).toHaveBeenCalledOnce()
expect(onSubmit.mock.calls[0]?.[0]?.test).toEqual(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check the whole string ?

export const Required: StoryFn<
ComponentProps<typeof RichTextEditorField>
> = args => (
<Stack gap={1}>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could add this Stack and gap in the Template story, and bind the Template markup here like the playground so that they look the same

@JulienSaguez JulienSaguez changed the title feat(RichTextEditor): create new component feat(RichTextInput): create new component Apr 20, 2026
@JulienSaguez JulienSaguez requested a review from jsulpis April 20, 2026 14:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants