DataViews: Add a richtext control backed by a new @wordpress/rich-text-control package#78471
DataViews: Add a richtext control backed by a new @wordpress/rich-text-control package#78471adamsilverstein wants to merge 42 commits into
Conversation
…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.
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`.
`@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.
`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.
`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.
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.
…l package The new control is intended for standalone form fields and does not touch the block tree or selection, so it does not belong in @wordpress/block-editor. Hosting it there forced @wordpress/fields to take a dependency on the entire block-editor module graph just to render a single form input. Move RichTextControl into a new lower-level package that depends only on @wordpress/rich-text plus @wordpress/components/compose/element. The new package vendors the small helpers it owns (getAllowedFormats, the keyboard and input-event contexts, the two event-listener modules, and a BlockContext-free FormatEdit) so block-editor's canvas RichText keeps its own copies untouched. The autocompleters prop is dropped from this initial release; it depended on block-editor's autocomplete component and no current consumer wires it. @wordpress/fields now imports the control via the new package's private API and the @wordpress/block-editor dependency is removed.
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Size Change: +8.38 kB (+0.1%) Total Size: 8.61 MB 📦 View Changed
|
|
Flaky tests detected in 9d52a7b. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/27789333399
|
|
|
||
| export const inputEventContext = createContext(); | ||
| inputEventContext.displayName = 'inputEventContext'; | ||
| // These contexts live in `@wordpress/rich-text` so that lower-level rich text |
There was a problem hiding this comment.
Do we need these comments?
The richTextField definition was an orphan: nothing imports it, it is not exported from the package's public index, and it is not registered in any field list. The shared RichTextEdit component it referenced is still used directly by the title field, so only the field stub is removed. Addresses review feedback on #78825.
| const titleField: Field< CommonPost > = { | ||
| type: 'text', | ||
| id: 'title', | ||
| Edit: RichTextEdit, |
There was a problem hiding this comment.
I don't think we should update the title field here. Even though you could have html in titles, there were nuances (that I don't recall exactly) and that's why we don't support this in the editor too for so long. There were tries in the past (including an approach from me) but were closed.
Do you have a use case for this change? If yes, we should at least investigate better..
There was a problem hiding this comment.
Fair point. The richtext integration is moving to #78825, which takes a different approach and does not modify the title field; the change here will be dropped in favor of that.
|
It's a bit harder to try to look at both PRs.. Maybe we should land at the direction (my preference is the approach in the other PR) and have a single one to review and get to the finish line. |
Makes sense, I had opened two so we could consider both approaches, but 78825 seems to be preferred so far. I'll merge that in and we can continue reviews here. |
Which part is sub-optimal in your opinion? It looks like the only thing There's definitely future risk if |
I merge in and closed the other PR in favor of this one. |
…ol-package # Conflicts: # packages/dataviews/CHANGELOG.md # packages/fields/CHANGELOG.md
On a conceptual level, it's about decoupling a package that should be focused on UI-only componentry ( |
The handlers iterated here are input event callbacks, not keyboard shortcuts; the old name was a copy-paste leftover from the shortcuts listener.
…ol-package # Conflicts: # packages/fields/CHANGELOG.md
…into move/rich-text-control-package
…ol-package # Conflicts: # packages/fields/CHANGELOG.md
The title-field richtext integration moves to #78825, which takes a different approach and does not modify the title field. Restore the field to its trunk state here.
The entry describes #78825's work (and links it); it belongs there, not in this PR, now that the title field change has been reverted here.
syncpack requires all workspaces to share the same semver range for a dependency. Match the repo-wide `^2.1.1` for clsx instead of an exact pin.
Adding the @wordpress/rich-text-control package shifts the generated `as` prop type union for BaseControl, so the docs:build output drifts. Commit the regenerated README to satisfy the check-local-changes gate.
- Update package-lock.json rich-text-control entry to clsx ^2.1.1 so the lockfile matches package.json (npm install otherwise drifts it). - Revert the base-control README change: a clean install generates the trunk version; the earlier regeneration was an artifact of a local node_modules mismatch, not a real drift from this PR.
The trunk merge released 16.0.0/16.0.1, stranding the richtext control New Features entry under the released 16.0.0 section. Move it back into the Unreleased section so the CHANGELOG check recognizes it.
Summary
Adds rich text editing support to DataViews/DataForms through two pieces:
@wordpress/rich-text-control, that owns the standaloneRichTextControlform-field primitive.richtextDataForm control in@wordpress/dataviews, backed by that package. Fields opt into it declaratively withEdit: 'richtext'(the title field is migrated as the first consumer).Alternative to #75275, and supersedes #78825 (its
richtextDataForm control was merged into this branch as the preferred approach).Why
RichTextControlis intentionally decoupled from the block tree and block-editor selection — its JSDoc bills it as a "standalone form field" primitive. Hosting it inside@wordpress/block-editorwould force every consumer to take a hard dependency on the entire block-editor module graph just to render a single form input.Exposing it instead as a registered
richtextDataForm control means fields reference it by name (Edit: 'richtext') rather than importing anEditcomponent, keeping@wordpress/fieldsfree of any block-editor dependency.What changed
New
@wordpress/rich-text-controlpackageRichTextControlas a direct (non-private) export. Depends only on@wordpress/rich-text,@wordpress/components,@wordpress/compose,@wordpress/element, and@wordpress/private-apis.getAllowedFormats, the keyboard-shortcut / input-event contexts, the event-listener modules, and aBlockContext-freeFormatEdit.@wordpress/block-editor's canvasRichTextkeeps its own copies and is otherwise unchanged.@wordpress/rich-text.RichTextControlno longer accepts theautocompletersprop in this initial release — that path depended on block-editor's autocomplete component and no current consumer wires it. Can be added back as a follow-up if needed.New
richtextDataForm control in@wordpress/dataviewspackages/dataviews/src/components/dataform-controls/richtext.tsxwrapsRichTextControland is registered in theFORM_CONTROLSregistry.Edit: 'richtext'; rich-text-specific options (allowedFormats,disableFormats,clientId,preserveWhiteSpace, etc.) are passed through the fieldconfig.Edit: 'richtext'as the first consumer.Test plan
npm run test:unit -- packages/rich-text-control— control suite passes.npm run test:unit -- packages/dataviews/src/components/dataform-controls—richtextcontrol tests pass (5/5).npm run test:unit -- packages/block-editor— no regression from extracting the shared helpers.Notes
If you prefer the simpler diff, #75275 still works. The architectural cost of that approach (transitive
block-editordependency, weight when consumed from npm) is the only thing this PR exists to fix.