On this page
Import
import {
createRichTextExtensions,
RichText,
} from '@envsemble/envui/rich-text';
Usage
import { RichText, RichTextEditor } from '@envsemble/envui/rich-text';
<RichTextEditor label="Meeting notes" value={notes} onValueChange={setNotes} />
<RichText>{notes}</RichText>
RichTextEditor and RichText come from the optional @envsemble/envui/rich-text entry, which needs Tiptap and marked, installed by the application. The editor is a field with its label, description and error, and its value is Markdown: leave it uncontrolled, with defaultValue, or control it with value and onValueChange, which receives the Markdown after every edit, normalized by the editor's schema. placeholder shows in an empty document; readOnly shows it without editing, and disabled turns it off and leaves its value out of a form.
Show saved content
RichText renders the Markdown in its children, also on a server. Pass it the editor's extensions. onReferenceClick and onLinkClick open references and links; renderReference renders a reference's content, such as an avatar beside the name, in both.
Toolbar and variants
toolbar takes the commands to show, in order, or false for none; Undo and Redo, and Attach files with onUpload, follow them. variant is outline, the default, or ghost. The document is at least 10 rem tall; minRows makes it as tall as that many lines instead, such as minRows={2} for a short note in a card, and it grows with its text either way.
References and slash commands
// At module scope, so the sources stay the same between renders.
const references: RichTextReferenceSource[] = [
{ trigger: '@', label: 'People', search: searchPeople },
{ trigger: '#', label: 'Topics', search: searchTopics },
];
<RichTextEditor label="Comment" references={references} slashCommands />;
Each source has its trigger, a label and a search lookup, which receives the query and a signal that aborts when it changes. The Markdown keeps each reference's ID. slashCommands is true for heading, the lists, quote, code block and table, or an array that adds the application's commands after them. Keep references, extensions and slashCommands the same between renders: a new array starts a new session and loses undo.
Attachments and links
onUpload adds Attach files to the toolbar and uploads each chosen file, returning where it lives, null to skip it, or a promise of either; accept and multiple set what it offers. pickLink replaces the link dialog with the application's picker: it receives a MarkdownLinkRequest and returns a MarkdownLink or null, types @envsemble/envui/rich-text exports.
Commands from outside
const editorRef = useRef<RichTextEditorHandle>(null);
<RichTextEditor editorRef={editorRef} label="Reply" />
<Button
onClick={() =>
editorRef.current?.editor?.chain().focus('end').insertContent(' — Alex Morgan').run()
}
>
Add signature
</Button>
editorRef.current.editor is the Tiptap editor, and ref reaches the root element, so the application can change the document with its commands.
In a form
<form onSubmit={publish} noValidate>
<RichTextEditor name="notes" label="Release notes" error={error} />
<Button type="submit">Publish</Button>
</form>
With name, a native form submits the Markdown through a hidden input, and form names a form the editor sits outside of. error says what to fix.
Composition
extensions adds Tiptap extensions to the schema the editor and RichText share, such as a custom mark, which must parse and write Markdown; pass the same ones to both. createRichTextExtensions returns that shared set, with options.extensions after it, for another Tiptap integration that needs the same schema. The toolbar is the Toolbar MarkdownEditor shares; the suggestions are env-rich-text-suggestions, and the status env-editor-status.
Customize
Its popup is frosted, as every popup is: --env-popup-opacity, --env-popup-blur and --env-popup-shadow, set on a theme, change it, as Popover describes.
Accessibility
You provide
- A
labelthat names the document, and one for each reference source. - Labels for image attachments.
- Lookups, uploads, navigation and saving.
- Translated words through EnvUIProvider's
messages.
envUI provides
- The document as a multiline
textbox, named by the label and described by the description and the error. - A named toolbar with one tab stop, a tooltip on each button and
aria-pressedfor formatting at the caret. - Suggestions as a listbox named by the source's
label, with the highlighted option announced. - A status for uploads, failures and refused links.
- Raw HTML kept as text, and unsafe URLs refused.
Props
RichText
Saved rich text, read-only: Markdown rendered with the editor's own schema and parser, so it shows what RichTextEditor wrote, references included, with envUI's links, code and checkboxes. It renders on the server too.
| Prop | Type | Description |
|---|---|---|
childrenrequired | string | The Markdown to show, such as what RichTextEditor saved. |
extensionsDefault [] | AnyExtension[ | Tiptap extensions added to the shared schema, such as a custom mark. They must parse and write Markdown, and the array must stay the same between renders, such as at module scope; a new array starts a new editing session. Pass the same extensions to RichText. |
renderReferenceDefault the trigger and the label, such as #Launch | ( | Renders a reference's content, such as an avatar beside the name; it receives whether the document is read-only and whether the reference is selected. |
onReferenceClick | ( | Called when people activate a reference, with the reference and the event, such as to open the record. A reference with an href is a link, which the editor never follows while it is editable; without an href, this makes the reference a button. |
onLinkClick | ( | Called when people activate a link in the document, with its href and the event: a native MouseEvent in the editor, a React mouse event in RichText. The editor never follows links while editable; call preventDefault to keep RichText or a read-only editor from following it. |
Also accepts the native props of <div>, such as ref, className and aria-* attributes.
RichTextEditor
A labelled editor for formatted text that people edit as it looks, built on Tiptap, with a formatting toolbar, references typed with a trigger such as
| Prop | Type | Description |
|---|---|---|
editorRef | Ref< | Gives the Tiptap editor, as editorRef.current.editor, for commands. |
value | string | The Markdown of a controlled editor; update it from onValueChange. A new value from outside, such as another draft, replaces the document and keeps the caret where it can. |
defaultValueDefault '' | string | The Markdown an uncontrolled editor starts with, and returns to when its form resets. |
onValueChange | ( | Receives the document as Markdown after every edit, normalized by the editor's schema, so it can differ from the Markdown it was given, such as * for _ around italics. |
name | string | Submits the Markdown with a native form under this name, through a hidden input. |
form | string | The ID of the form the hidden input belongs to, where the editor sits outside it. |
placeholder | string | Text shown in an empty document, such as what to write. |
disabledDefault false | boolean | Makes the editor unavailable: it cannot be edited or focused, the toolbar is disabled, its value is left out of form submission and it takes the shared disabled opacity. |
readOnlyDefault false | boolean | Shows the document without editing it: the toolbar is disabled and the document cannot be focused, but its links and references can, and the form still submits its value. |
toolbarDefault every command, from heading to table | readonly RichTextCommand[false | The toolbar's commands, in order, or false for no toolbar; Undo and Redo, and Attach files with onUpload, follow them. |
referencesDefault [] | readonly RichTextReferenceSource[ | Kinds of references people insert by typing a trigger, such as @ for people and # for topics, each with its own lookup. Triggers must be unique, and the array must stay the same between renders, such as at module scope. |
slashCommandsDefault false | booleanreadonly RichTextSlashCommand[ | Typing / opens a menu of block commands: true offers heading, the lists, quote, code block and table; an array adds the application's commands after them. A / reference source can't be used beside it. |
pickLink | ( | Opens the application's own link picker for Insert link, with the selected text, instead of the built-in dialog. Return the link, null to cancel, or a promise of either. A selection becomes a link to its href; at the caret, the label is inserted as the link's text. |
onUpload | ( | Uploads a file people chose with Attach files, which shows in the toolbar only with this. Return where it lives as an attachment, null to skip it, or a promise of either; the signal aborts when the editor becomes read-only or disabled, or when a controlled value changes. |
accept | string | The file types Attach files offers, as in a file input's accept, such as "image/*,.pdf". |
multipleDefault true | boolean | Lets people choose several files at once with Attach files. |
variantDefault 'outline' | 'outline''ghost' | The document's look, as a text area's: outline, in a bordered box on what it sits on; or ghost, only its text at rest, with a soft fill under the pointer and while it has focus. The document is always tall, with its label above it. |
minRows | number | Lines of text the document is tall enough for while it holds fewer, such as 2 for a short note in a card; it grows with its text. Without it the document is at least 10 rem tall. |
extensionsDefault [] | AnyExtension[ | Tiptap extensions added to the shared schema, such as a custom mark. They must parse and write Markdown, and the array must stay the same between renders, such as at module scope; a new array starts a new editing session. Pass the same extensions to RichText. |
renderReferenceDefault the trigger and the label, such as #Launch | ( | Renders a reference's content, such as an avatar beside the name; it receives whether the document is read-only and whether the reference is selected. |
onReferenceClick | ( | Called when people activate a reference, with the reference and the event, such as to open the record. A reference with an href is a link, which the editor never follows while it is editable; without an href, this makes the reference a button. |
onLinkClick | ( | Called when people activate a link in the document, with its href and the event: a native MouseEvent in the editor, a React mouse event in RichText. The editor never follows links while editable; call preventDefault to keep RichText or a read-only editor from following it. |
label | ReactNode | The field's visible name, such as Project name; it also names the control for assistive technology. |
description | ReactNode | Lasting guidance under the field, such as a format or a limit, linked to the control as its description. |
error | ReactNode | A message under the field that says how to correct the value. While it is set, the control is invalid (aria-invalid) and the message joins its description, after the description. |
Also accepts the native props of <div>, such as ref, className and aria-* attributes.
createRichTextExtensions
The Tiptap extensions RichTextEditor and RichText share: formatting, lists and task lists, tables, safe links and images, Markdown with raw HTML kept as text, a placeholder and references. options.extensions are added after them, for other Tiptap integrations that need the same schema.
createRichTextExtensions(options?: RichTextRenderOptions & { placeholder?: string; }): AnyExtension[]
Part names
Each part's class and state attributes are public and versioned, for an application's CSS to style.
RichText
env-rich-text- The rendered rich text.
env-rich-text-document- The editable document.
env-rich-text-reference- A reference to a person or an item.
env-rich-text-suggestions- The list of suggestions while typing a reference.
env-rich-text-suggestions-positioner- The suggestions' positioner.
env-rich-text-task- A task item.
env-rich-text-task-check- A task's checkbox.
env-rich-text-task-content- A task's text.
data-checkedfalse·trueon env-rich-text-taskdata-reference-id- Any value, or present on env-rich-text-reference
RichTextEditor
env-rich-text-editor- The rich text editor.
env-rich-text-editor-content- The editable region.
env-rich-text-editor-control- The well around the toolbar and the document.
data-disabled- Present on env-rich-text-editor, env-rich-text-editor-control
data-invalid- Present on env-rich-text-editor-control
data-readonly- Present
Strings
EnvUIProvider's messages overrides each, and a component's own label prop wins over it.
messages.editor.heading"Heading"A command.messages.editor.bold"Bold"A command.messages.editor.italic"Italic"A command.messages.editor.strike"Strikethrough"A command.messages.editor.bulletList"Bulleted list"A command.messages.editor.numberedList"Numbered list"A command.messages.editor.taskList"Task list"A command.messages.editor.quote"Quote"A command.messages.editor.inlineCode"Inline code"A command.messages.editor.codeBlock"Code block"A command.messages.editor.link"Insert link"A command.messages.editor.table"Insert table"A command.messages.editor.undo"Undo"A command.messages.editor.redo"Redo"A command.messages.linkDialog.title"Insert link"The dialog's title.messages.linkDialog.insert"Insert link"The confirm button's label.messages.linkDialog.text"Text"The text field's label.messages.linkDialog.url"URL or path"The URL field's label.messages.linkDialog.urlPlaceholder"https://example.com or /documents/example"The URL field's placeholder, an example of what it takes.messages.linkDialog.invalidUrl"Enter a valid URL."The error for an empty destination.messages.linkDialog.invalidPath"Enter a valid URL or relative path."The error for a destination that is neither a URL nor a path.messages.richText.task"Task"Names a task item's checkbox without text.messages.richText.commands"Commands"Names the slash command list.messages.richText.toolbar"Rich text formatting"Names the formatting toolbar.messages.richText.attach"Attach files"The attach button's name.messages.richText.suggestionsLoading"Loading suggestions…"Shown while suggestions load.messages.richText.suggestionsEmpty"No results"Shown when no suggestion matches.messages.richText.suggestionsFailed"Suggestions could not load. Try typing again."Shown when the suggestions could not load.messages.richText.suggestions"Suggestions"Names the suggestion list of a source without a label.messages.richText.changedWhilePicking"The document changed while the picker was open. Insert the link again."Said when the document changed while the application's link picker was open.messages.richText.pickerFailed"The link picker could not finish. Try again."Said when the application's link picker failed.messages.richText.uploading"Uploading attachments…"Said while attachments upload.messages.richText.unsafeUpload"The upload returned an unsafe URL."Said when an upload returned a URL the editor does not link to.messages.richText.changedWhileUploading"The document changed during upload. Attach the files again."Said when the document changed while the files uploaded.messages.richText.uploadFailed"The upload failed. Try again."Said when an upload failed without a message of its own.
Changelog
- 1.0.0
A reference source's
charistriggerand itsitemslookup issearch, and a reference'scharistrigger; The Markdown saved before keeps working.RichTextEditor takes the field vocabulary:
descriptioninstead ofhint, and the variantsoutline,underlineandghostinstead ofoutlined,plainandquiet, which drew as outlined. The document sits in the field's box, with its border, its states and a disabled field whose label stays readable, and task items show envUI's checkboxes beside their text. The toolbar is the Toolbar MarkdownEditor shares, with tooltips, the link dialog is the shared one, and every word comes from EnvUIProvider'smessages. Suggestions are rows of a listbox in the editor, above a Dialog it sits in, and close as focus leaves; a controlled editor raises noflushSyncwarning. The./rich-textentry is alpha.Tiptap, marked, Shiki and the Pierre themes are no longer peer dependencies; applications that import the rich-text entry install them, as Setup lists.
- 1.0.0-prerelease.6
RichTextEditor takes
minRows, the lines of text its document is tall enough for, for a short editor rather than one at least 10 rem tall.Bold text in a document is semibold rather than medium, as in Markdown, so it stands out from the body text around it.
Where the toolbar is wider than the editor, its groups wrap onto more rows rather than scrolling sideways, so Undo and Redo stay in view. The editor's label takes the size, weight and muted color of a field's floated label.
In RichText and a read-only editor, a task's checkbox is read-only rather than disabled: in full color, with
aria-readonly, out of the tab order and not answering the pointer. A disabled editor's tasks stay disabled. The checkbox sits on the middle of its text's first line rather than a little above it.RichText keeps a line break within a paragraph, a single newline in the Markdown, and runs of spaces, as the editor shows them, rather than joining the lines.
A disabled editor no longer fades as a whole: its document takes
--env-color-fg-disabledand its outline the muted border, as a disabled TextField does.- 1.0.0-prerelease.5
onRequestLinkispickLink: it supplies the link rather than reporting an event.refreaches the root element, andeditorRefgives the Tiptap editor.Suggestions are frosted: the popup's color lets a little of what is behind through, blurred, with a soft shadow and no outline edge but a faint hairline in dark, and is solid with its edge at high contrast and with reduced transparency, with the rows of Select's list.
- 1.0.0-prerelease.3
A task's row in RichText is as tall as in the editor. The suggestions keep the active option in view as the arrow keys move past the list's edge, and close as focus leaves the editor rather than on any press on the page.
- 1.0.0-prerelease.2
The
underlinevariant is gone, for the default outline.