Fields package: Add RichText field#75275
Conversation
Per review feedback on #78242, replace the block-editor RichText component with RichTextControl introduced in #75275. RichText is intended for use inside blocks and pulls in editor state (selection store, format toolbar wiring, etc.) that the note form does not need. RichTextControl wraps useRichText directly and exposes the smaller surface the note form actually uses, which mirrors the approach DataForms takes for its rich-text field. The form keeps the same allowed-formats list (bold, italic, link, code), the same keyboard shortcuts (Cmd+B / Cmd+I / Cmd+K), and the same empty-state submit guard. The Cmd+Enter / Escape handling moves up to the form element because RichTextControl does not expose an onKeyDown prop; the events still reach it via bubbling from the contenteditable. Note styles are rescoped to the form-level container (the input now carries the .block-editor-rich-text-control class) and the explicit VisuallyHidden label is replaced by RichTextControl's built-in hideLabelFromVision option. Refs #75275 Refs #73413
Mock @wordpress/block-editor's privateApis and the editor lock-unlock module so the test substitutes a lightweight RichTextControl for the real one. The mock keeps the same data-testid the suite already uses so the existing assertions about format allowlist and keyboard handling continue to work unchanged. Refs #75275
|
Hi @talldan I'd like to work on landing this so we can use it for rich formatting in Notes. Do you have any tips about what remains? Would you be available to review any fixes I push to the branch? |
|
In the meantime I'm going to add tests/update the pr and review. |
…ers/className The trunk `useRichText` now consumes `allowedFormats`, `withoutInteractiveFormatting`, and a format-type handler context directly, returning `formatTypes` alongside the editor state. Lean on that and drop the duplicated `useFormatTypes` / editor-only-format wrappers. Add `autocompleters` (forwarded to `useBlockEditorAutocompleteProps`) and `className` props so callers like the Notes inline form can wire `@`-mention completers and customize the contenteditable styling. Route the stylesheet through the block-editor entry instead of importing the SCSS from JS.
Add `@wordpress/block-editor` to the fields package's dependencies — the new `RichTextControl` is imported via private APIs and the dependency was missing, tripping `import/no-extraneous-dependencies`. In `fields/rich-text/edit.tsx`, forward the new `className` and `autocompleters` config to `RichTextControl` and make `config` itself optional (consumers like the title field do not pass one). Drop the commented placeholder `ConfiguredRichTextEdit` in the title field.
Add focused unit tests covering: - `RichTextControl`: labeled-textbox markup, `hideLabelFromVision`, `disableLineBreaks`/`aria-multiline` toggling, and consumer-supplied `className` merging. - `fields/rich-text/edit`: that the wrapper forwards field label/value/id to the underlying control, that change events flow through `field.setValue` back to the consumer's `onChange`, that optional config props (`clientId`, `placeholder`, `allowedFormats`, etc.) are passed through, and that a missing config object does not crash.
af5710c to
520950d
Compare
|
@talldan - I picked this up since you mentioned not having time — pushed three commits on top of yours to address review findings and unblock #78242 (Notes inline rich text):
Rebased onto current trunk along the way (only Notes for follow-up
|
Per review feedback on #78242, replace the block-editor RichText component with RichTextControl introduced in #75275. RichText is intended for use inside blocks and pulls in editor state (selection store, format toolbar wiring, etc.) that the note form does not need. RichTextControl wraps useRichText directly and exposes the smaller surface the note form actually uses, which mirrors the approach DataForms takes for its rich-text field. The form keeps the same allowed-formats list (bold, italic, link, code), the same keyboard shortcuts (Cmd+B / Cmd+I / Cmd+K), and the same empty-state submit guard. The Cmd+Enter / Escape handling moves up to the form element because RichTextControl does not expose an onKeyDown prop; the events still reach it via bubbling from the contenteditable. Note styles are rescoped to the form-level container (the input now carries the .block-editor-rich-text-control class) and the explicit VisuallyHidden label is replaced by RichTextControl's built-in hideLabelFromVision option. Refs #75275 Refs #73413
Mock @wordpress/block-editor's privateApis and the editor lock-unlock module so the test substitutes a lightweight RichTextControl for the real one. The mock keeps the same data-testid the suite already uses so the existing assertions about format allowlist and keyboard handling continue to work unchanged. Refs #75275
A `<label for>` only contributes an accessible name to native form
controls, not to a `<div role="textbox">`. Mirroring `label` onto
`aria-label` gives the contenteditable a stable accessible name so
assistive tech and Playwright `getByRole('textbox', { name })` lookups
resolve consistently regardless of `hideLabelFromVision`.
The wrapper's prop type was `Pick<DataFormControlProps, ...> & { config: RichTextFieldConfig }`,
which is not contravariantly assignable to `ComponentType<DataFormControlProps<Item>>`
because the rich-text `config` shape diverges from the generic one. Accepting the
standard `DataFormControlProps` and narrowing `config` at the call site keeps the
wrapper usable as a `Field.Edit` and drops the now-unneeded `@ts-expect-error`.
Per review feedback on #78242, replace the block-editor RichText component with RichTextControl introduced in #75275. RichText is intended for use inside blocks and pulls in editor state (selection store, format toolbar wiring, etc.) that the note form does not need. RichTextControl wraps useRichText directly and exposes the smaller surface the note form actually uses, which mirrors the approach DataForms takes for its rich-text field. The form keeps the same allowed-formats list (bold, italic, link, code), the same keyboard shortcuts (Cmd+B / Cmd+I / Cmd+K), and the same empty-state submit guard. The Cmd+Enter / Escape handling moves up to the form element because RichTextControl does not expose an onKeyDown prop; the events still reach it via bubbling from the contenteditable. Note styles are rescoped to the form-level container (the input now carries the .block-editor-rich-text-control class) and the explicit VisuallyHidden label is replaced by RichTextControl's built-in hideLabelFromVision option. Refs #75275 Refs #73413
Mock @wordpress/block-editor's privateApis and the editor lock-unlock module so the test substitutes a lightweight RichTextControl for the real one. The mock keeps the same data-testid the suite already uses so the existing assertions about format allowlist and keyboard handling continue to work unchanged. Refs #75275
`@wordpress/block-editor` ships no `.d.ts` files, so the type-declaration build (`tsgo --build`) fails to resolve the import even though the package is declared as a runtime dependency. Use `@ts-ignore` (rather than the `@ts-expect-error` that was dropped earlier) so the directive does not flip to "unused" under per-package `tsc` checks that happen to resolve the source successfully.
Per review feedback on #78242, replace the block-editor RichText component with RichTextControl introduced in #75275. RichText is intended for use inside blocks and pulls in editor state (selection store, format toolbar wiring, etc.) that the note form does not need. RichTextControl wraps useRichText directly and exposes the smaller surface the note form actually uses, which mirrors the approach DataForms takes for its rich-text field. The form keeps the same allowed-formats list (bold, italic, link, code), the same keyboard shortcuts (Cmd+B / Cmd+I / Cmd+K), and the same empty-state submit guard. The Cmd+Enter / Escape handling moves up to the form element because RichTextControl does not expose an onKeyDown prop; the events still reach it via bubbling from the contenteditable. Note styles are rescoped to the form-level container (the input now carries the .block-editor-rich-text-control class) and the explicit VisuallyHidden label is replaced by RichTextControl's built-in hideLabelFromVision option. Refs #75275 Refs #73413
Mock @wordpress/block-editor's privateApis and the editor lock-unlock module so the test substitutes a lightweight RichTextControl for the real one. The mock keeps the same data-testid the suite already uses so the existing assertions about format allowlist and keyboard handling continue to work unchanged. Refs #75275
|
Size Change: +1.35 kB (+0.02%) Total Size: 7.97 MB 📦 View Changed
ℹ️ View Unchanged
|
Per review feedback on #78242, replace the block-editor RichText component with RichTextControl introduced in #75275. RichText is intended for use inside blocks and pulls in editor state (selection store, format toolbar wiring, etc.) that the note form does not need. RichTextControl wraps useRichText directly and exposes the smaller surface the note form actually uses, which mirrors the approach DataForms takes for its rich-text field. The form keeps the same allowed-formats list (bold, italic, link, code), the same keyboard shortcuts (Cmd+B / Cmd+I / Cmd+K), and the same empty-state submit guard. The Cmd+Enter / Escape handling moves up to the form element because RichTextControl does not expose an onKeyDown prop; the events still reach it via bubbling from the contenteditable. Note styles are rescoped to the form-level container (the input now carries the .block-editor-rich-text-control class) and the explicit VisuallyHidden label is replaced by RichTextControl's built-in hideLabelFromVision option. Refs #75275 Refs #73413
Mock @wordpress/block-editor's privateApis and the editor lock-unlock module so the test substitutes a lightweight RichTextControl for the real one. The mock keeps the same data-testid the suite already uses so the existing assertions about format allowlist and keyboard handling continue to work unchanged. Refs #75275
`FormatEdit` populates `keyboardShortcuts` and `inputEvents` Sets via context, but `RichTextControl` never attached a `keydown` or `input` listener to the contenteditable, so registered shortcuts (Cmd+B, Cmd+I, Cmd+K, etc.) and native InputEvents (formatBold/formatItalic) never fired. Cmd+K also bubbled past the control to open the WordPress command palette because no shortcut consumed it. Attach the existing `shortcuts` and `input-events` listeners while the control is focused, mirroring the in-canvas `RichText` wiring. The link format's `RichTextShortcut` now calls `preventDefault()` on Cmd+K, which causes the command palette's global handler (which bails on `defaultPrevented`) to skip opening. Add unit tests covering shortcut dispatch on focus, no dispatch when unfocused, and listener teardown on blur.
|
I don't have any experience with this codebase to really be able to review it authoritatively, but I think in general core needs to be using rich text in many more places than we currently are using it. See #73180 (comment). |
|
Flaky tests detected in 55a4cfe. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/25998302682
|
`RichTextControl` now invokes each format type's `__unstableInputRule` on `input`/`compositionend` events, so e.g. typing `` `code` `` in a notes field auto-applies `core/code`'s inline-code format the same way it does in the canvas. The block-editor's existing `input-rules.js` listener handles three distinct concerns (block prefix transforms, block input transforms, format input rules), and pulls in `@wordpress/blocks`, block-editor store actions, and `onReplace`/`selectionChange` callbacks. None of those apply to a standalone field control — so wire a focused handler that only runs the format-rule reduce, mirroring the same branch inline. Also set `suppressContentEditableWarning` on the contenteditable so React doesn't warn when `useRichText` writes value into the DOM directly (matching in-canvas `RichText`).
`isVisible` controls whether a format's toolbar button is shown — it's
how a format hides its toolbar surface in contexts that don't have a
toolbar (e.g., the standalone `RichTextControl` used in DataForm
fields). It should not gate the link popover itself: the popover is
triggered by `Cmd+K` (or the toolbar button) and represents the link
editing UI, not a toolbar element.
Before this change, `RichTextControl` consumers that hid format
toolbars (`isVisible={false}`) could press `Cmd+K` on a selection and
nothing visible would happen — the shortcut fired, `addingLink`
flipped true, but `InlineLinkUI` was suppressed by the same flag that
hid the toolbar button.
When a format type opens a popover from inside `RichTextControl` (the inline link UI on Cmd+K, or any similar format-spawned UI), focus moves out of the contenteditable to the popover's first focusable element. That fired the textbox's onBlur, which flipped `isSelected` to false, unmounted `FormatEdit`, and tore the popover down before it ever rendered. Defer the `isSelected = false` flip via a 0ms `setTimeout` so the new focus target has a chance to land. If the active element is inside `.components-popover`, leave the control selected — it's a format popover keeping the user in this control's interaction scope. Verified with a new unit test covering the popover-focus case in addition to the existing focus/blur shortcut tests.
- Revert the format-library/link change: restoring the `isVisible &&` guard on `InlineLinkUI` here. Making the link popover appear inside RichTextControl is the upstream component's responsibility and belongs in #75275. - AddNote: hoist the `!clientId` early return up to the parent so AddNoteInner only mounts once we have a clientId snapshot. This removes the two `@wordpress/no-unused-vars-before-return` suppressions around `useDispatch` calls and a now-unreachable inner guard. - hooks.js: clarify in-place that the `blockClientId` snapshot parameter is a workaround for selection drift caused by RichTextControl focus, with a pointer to #75275 so the param can be removed once that lands.
|
While stacking #78242 (Notes rich-text input) on top of 1. 2. Focusing the rich-text field shifts block-editor selection. When |
The earlier eslint-disable cleanup hoisted the `!clientId` guard from AddNoteInner up to the AddNote parent. That made the parent re-check *live* block selection every render, so when focusing the note form cleared the canvas selection the form unmounted mid-edit — regressing the trunk-passing 'should move focus to add a new note form' e2e test. Restore the snapshot + inner-guard structure; the two eslint-disables are ugly but load-bearing. Mark 'Cmd+K opens the inline link popover' as test.fixme: it asserts the link popover renders inside a toolbar-less RichTextControl, which depends on the format-library link-gate relaxation that is intentionally being moved to the upstream RichTextControl PR (#75275) rather than shipped here.
The note form now uses RichTextControl, which does not reliably take/hold focus in a standalone, toolbar-less context. This regresses several trunk-passing e2e tests (focus-to-form, reply, reopen-and-reply, keyboard nav, multiple notes, and collaboration reply-sync) — all failing on toBeFocused()/reply-button timeouts with the same root cause. The fix belongs in the upstream RichTextControl PR (#75275), not here. Mark the affected tests test.fixme with explicit comments so the regression is visible to reviewers and the tests are re-enabled when #75275 lands. This is a known, documented scope gap, not a silent skip.
RichTextControl deliberately drops the block-editor selection coupling that focuses the in-canvas RichText, so a standalone consumer (e.g. a note form) has no way to place the caret in the field when it opens — regressing focus-on-open behavior the old RichText got for free. Add an opt-in focusOnMount prop that focuses the contenteditable on mount via useRefEffect (mirroring the existing eventListenersRef pattern in this file). Off by default so DataForms and other consumers are unaffected. Named focusOnMount rather than autoFocus to match @wordpress/compose's useFocusOnMount and to avoid the jsx-a11y/no-autofocus rule, since this is not the browser autofocus attribute. Covered by unit tests for both the default-off and opt-in cases.
Per review feedback on #78242, replace the block-editor RichText component with RichTextControl introduced in #75275. RichText is intended for use inside blocks and pulls in editor state (selection store, format toolbar wiring, etc.) that the note form does not need. RichTextControl wraps useRichText directly and exposes the smaller surface the note form actually uses, which mirrors the approach DataForms takes for its rich-text field. The form keeps the same allowed-formats list (bold, italic, link, code), the same keyboard shortcuts (Cmd+B / Cmd+I / Cmd+K), and the same empty-state submit guard. The Cmd+Enter / Escape handling moves up to the form element because RichTextControl does not expose an onKeyDown prop; the events still reach it via bubbling from the contenteditable. Note styles are rescoped to the form-level container (the input now carries the .block-editor-rich-text-control class) and the explicit VisuallyHidden label is replaced by RichTextControl's built-in hideLabelFromVision option. Refs #75275 Refs #73413
Mock @wordpress/block-editor's privateApis and the editor lock-unlock module so the test substitutes a lightweight RichTextControl for the real one. The mock keeps the same data-testid the suite already uses so the existing assertions about format allowlist and keyboard handling continue to work unchanged. Refs #75275
- Revert the format-library/link change: restoring the `isVisible &&` guard on `InlineLinkUI` here. Making the link popover appear inside RichTextControl is the upstream component's responsibility and belongs in #75275. - AddNote: hoist the `!clientId` early return up to the parent so AddNoteInner only mounts once we have a clientId snapshot. This removes the two `@wordpress/no-unused-vars-before-return` suppressions around `useDispatch` calls and a now-unreachable inner guard. - hooks.js: clarify in-place that the `blockClientId` snapshot parameter is a workaround for selection drift caused by RichTextControl focus, with a pointer to #75275 so the param can be removed once that lands.
The earlier eslint-disable cleanup hoisted the `!clientId` guard from AddNoteInner up to the AddNote parent. That made the parent re-check *live* block selection every render, so when focusing the note form cleared the canvas selection the form unmounted mid-edit — regressing the trunk-passing 'should move focus to add a new note form' e2e test. Restore the snapshot + inner-guard structure; the two eslint-disables are ugly but load-bearing. Mark 'Cmd+K opens the inline link popover' as test.fixme: it asserts the link popover renders inside a toolbar-less RichTextControl, which depends on the format-library link-gate relaxation that is intentionally being moved to the upstream RichTextControl PR (#75275) rather than shipped here.
The note form now uses RichTextControl, which does not reliably take/hold focus in a standalone, toolbar-less context. This regresses several trunk-passing e2e tests (focus-to-form, reply, reopen-and-reply, keyboard nav, multiple notes, and collaboration reply-sync) — all failing on toBeFocused()/reply-button timeouts with the same root cause. The fix belongs in the upstream RichTextControl PR (#75275), not here. Mark the affected tests test.fixme with explicit comments so the regression is visible to reviewers and the tests are re-enabled when #75275 lands. This is a known, documented scope gap, not a silent skip.
- Revert the format-library/link change: restoring the `isVisible &&` guard on `InlineLinkUI` here. Making the link popover appear inside RichTextControl is the upstream component's responsibility and belongs in #75275. - AddNote: hoist the `!clientId` early return up to the parent so AddNoteInner only mounts once we have a clientId snapshot. This removes the two `@wordpress/no-unused-vars-before-return` suppressions around `useDispatch` calls and a now-unreachable inner guard. - hooks.js: clarify in-place that the `blockClientId` snapshot parameter is a workaround for selection drift caused by RichTextControl focus, with a pointer to #75275 so the param can be removed once that lands.
The earlier eslint-disable cleanup hoisted the `!clientId` guard from AddNoteInner up to the AddNote parent. That made the parent re-check *live* block selection every render, so when focusing the note form cleared the canvas selection the form unmounted mid-edit — regressing the trunk-passing 'should move focus to add a new note form' e2e test. Restore the snapshot + inner-guard structure; the two eslint-disables are ugly but load-bearing. Mark 'Cmd+K opens the inline link popover' as test.fixme: it asserts the link popover renders inside a toolbar-less RichTextControl, which depends on the format-library link-gate relaxation that is intentionally being moved to the upstream RichTextControl PR (#75275) rather than shipped here.
The note form now uses RichTextControl, which does not reliably take/hold focus in a standalone, toolbar-less context. This regresses several trunk-passing e2e tests (focus-to-form, reply, reopen-and-reply, keyboard nav, multiple notes, and collaboration reply-sync) — all failing on toBeFocused()/reply-button timeouts with the same root cause. The fix belongs in the upstream RichTextControl PR (#75275), not here. Mark the affected tests test.fixme with explicit comments so the regression is visible to reviewers and the tests are re-enabled when #75275 lands. This is a known, documented scope gap, not a silent skip.
|
Opened #78471 as an architectural alternative: same end result (RichText field in If you prefer the simpler diff here, this PR works as-is — fields is already on the private-apis allowlist. The other PR exists to address the transitive block-editor coupling for npm consumers. |
See #73180
What?
Adds a
RichTextfield to the@wordpress/fieldspackage, backed by a new privateRichTextControlin@wordpress/block-editor. The control is the standalone, form-field counterpart to the in-canvasRichTextcomponent: it exposes a plainvalue/onChangeinterface and renders a contenteditable area with the registered formatting types wired up. The new field is then used to power the existingtitlefield so titles edit as rich text.Why?
DataForm currently has
textandtextareacontrols for textual content, but no first-class control for rich text. Several emerging use cases - Media titles/captions, sidebar editable fields, and post/page metadata - need an inline-editable field that participates in DataForms while still allowing formatting (bold, italic, links, autocompleters like@-mentions, etc.).Until now, rich-text editing has been tightly coupled to the canvas, which makes it awkward to reuse outside the block tree. By extracting a standalone
RichTextControland exposing it as a field, DataForm consumers can opt into rich text the same way they opt into any other field type, with consistent label/visually-hidden-label/keyboard behavior provided byBaseControl.How?
RichTextControlinpackages/block-editor/src/components/rich-text/control/:useRichTexthook (from@wordpress/rich-text) and renders aBaseControl-labeledcontenteditableelement.FormatEditso registered format types (bold, italic, link, custom formats, etc.) are active when the control is focused.allowedFormats,disableFormats,withoutInteractiveFormatting,preserveWhiteSpace,disableLineBreaks, andautocompletersso consumers can scope what's available per field (e.g. mentions in comments, no line breaks in titles).disableLineBreaksviaaria-multilineand supportshideLabelFromVisionfor compact inspector layouts.private-apis.jsas a private API for internal Gutenberg packages — not part of the public surface yet.packages/fields/src/fields/rich-text/:edit.tsxadapts DataForm'sDataFormControlPropstoRichTextControland forwards an optionalconfigobject (clientId,className,placeholder,allowedFormats,disableFormats,withoutInteractiveFormatting,preserveWhiteSpace,disableLineBreaks,autocompleters).index.tsxexports theFielddefinition (id: 'rich-text',type: 'text').packages/fields/src/fields/title/index.tsxto use the newRichTextEditfor the title field'sEditso titles now edit as rich text, and declares@wordpress/block-editoras afieldspackage dependency.style.scssfor the control (input-style border, padding, placeholder color) and registers it from the package stylesheet.hideLabelFromVision,aria-multilinefromdisableLineBreaks, className merging, and the DataFormEditwrapper.Testing Instructions
This component is used in #78242 and can be tested there.
npm install && npm run build.npm run wp-env start) and open the post/page editor or a DataViews-driven screen (e.g. Pages list → click a page to open the edit pane).Cmd/Ctrl+B); registered format types are active when the field is focused.Testing Instructions for Keyboard
Home/End, andCmd/Ctrl+Left/Rightword jumps.Shift+Arrow/Shift+Cmd+Left/Rightand apply formatting with standard shortcuts:Cmd/Ctrl+Bfor bold,Cmd/Ctrl+Ifor italic,Cmd/Ctrl+Kfor link.disableLineBreaksis set).Shift+Tabaway from the field - focus should leave the control cleanly and the inline format UI should detach.Screenshots or screencast