Skip to content

DataViews: Add a richtext control backed by a new @wordpress/rich-text-control package#78471

Open
adamsilverstein wants to merge 42 commits into
trunkfrom
move/rich-text-control-package
Open

DataViews: Add a richtext control backed by a new @wordpress/rich-text-control package#78471
adamsilverstein wants to merge 42 commits into
trunkfrom
move/rich-text-control-package

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented May 20, 2026

Copy link
Copy Markdown
Member

Summary

Adds rich text editing support to DataViews/DataForms through two pieces:

  1. A new low-level package, @wordpress/rich-text-control, that owns the standalone RichTextControl form-field primitive.
  2. A new richtext DataForm control in @wordpress/dataviews, backed by that package. Fields opt into it declaratively with Edit: 'richtext' (the title field is migrated as the first consumer).

Alternative to #75275, and supersedes #78825 (its richtext DataForm control was merged into this branch as the preferred approach).

Why

RichTextControl is 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-editor would 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 richtext DataForm control means fields reference it by name (Edit: 'richtext') rather than importing an Edit component, keeping @wordpress/fields free of any block-editor dependency.

What changed

New @wordpress/rich-text-control package

  • Owns RichTextControl as a direct (non-private) export. Depends only on @wordpress/rich-text, @wordpress/components, @wordpress/compose, @wordpress/element, and @wordpress/private-apis.
  • Vendors the helpers it needs: getAllowedFormats, the keyboard-shortcut / input-event contexts, the event-listener modules, and a BlockContext-free FormatEdit. @wordpress/block-editor's canvas RichText keeps its own copies and is otherwise unchanged.
  • Shared contexts/input-event/keyboard-shortcut primitives now live in @wordpress/rich-text.
  • RichTextControl no longer accepts the autocompleters prop 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 richtext DataForm control in @wordpress/dataviews

  • packages/dataviews/src/components/dataform-controls/richtext.tsx wraps RichTextControl and is registered in the FORM_CONTROLS registry.
  • Fields opt in via Edit: 'richtext'; rich-text-specific options (allowedFormats, disableFormats, clientId, preserveWhiteSpace, etc.) are passed through the field config.
  • Extends the DataViews field-api types so the control config is typed.
  • The title field is migrated to 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-controlsrichtext control tests pass (5/5).
  • npm run test:unit -- packages/block-editor — no regression from extracting the shared helpers.
  • Browser sanity check (title + a rich text field render and accept input in DataForms).

Notes

If you prefer the simpler diff, #75275 still works. The architectural cost of that approach (transitive block-editor dependency, weight when consumed from npm) is the only thing this PR exists to fix.

talldan and others added 19 commits May 14, 2026 07:38
…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.
@github-actions

github-actions Bot commented May 20, 2026

Copy link
Copy Markdown

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 props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: talldan <talldanwp@git.wordpress.org>
Co-authored-by: adamsilverstein <adamsilverstein@git.wordpress.org>
Co-authored-by: mirka <0mirka00@git.wordpress.org>
Co-authored-by: andrewserong <andrewserong@git.wordpress.org>
Co-authored-by: ntsekouras <ntsekouras@git.wordpress.org>
Co-authored-by: Mamaduka <mamaduka@git.wordpress.org>
Co-authored-by: ciampo <mciampini@git.wordpress.org>
Co-authored-by: t-hamano <wildworks@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions github-actions Bot added [Package] Format library /packages/format-library [Package] Private APIs /packages/private-apis [Package] Fields /packages/fields labels May 20, 2026
@github-actions

github-actions Bot commented May 20, 2026

Copy link
Copy Markdown

Size Change: +8.38 kB (+0.1%)

Total Size: 8.61 MB

📦 View Changed
Filename Size Change
build/modules/content-types/index.min.js 161 kB +1.65 kB (+1.04%)
build/scripts/block-editor/index.min.js 382 kB +1.45 kB (+0.38%)
build/scripts/edit-site/index.min.js 299 kB +1.61 kB (+0.54%)
build/scripts/editor/index.min.js 473 kB +1.76 kB (+0.37%)
build/scripts/format-library/index.min.js 30.9 kB -1 B (0%)
build/scripts/media-utils/index.min.js 116 kB +1.62 kB (+1.42%)
build/scripts/private-apis/index.min.js 1.14 kB +7 B (+0.62%)
build/scripts/rich-text/index.min.js 14.4 kB +279 B (+1.98%)

compressed-size-action

@github-actions

github-actions Bot commented May 20, 2026

Copy link
Copy Markdown

Flaky tests detected in 9d52a7b.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/27789333399
📝 Reported issues:

@adamsilverstein adamsilverstein removed the request for review from juanmaguitar May 20, 2026 17:34

export const inputEventContext = createContext();
inputEventContext.displayName = 'inputEventContext';
// These contexts live in `@wordpress/rich-text` so that lower-level rich text

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need these comments?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

nope, removed

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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..

@adamsilverstein adamsilverstein Jun 18, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

@ntsekouras

Copy link
Copy Markdown
Contributor

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.

@adamsilverstein

Copy link
Copy Markdown
Member Author

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.

@github-actions github-actions Bot added the [Package] DataViews /packages/dataviews label Jun 9, 2026
@talldan

talldan commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Even if Autocomplete already depends on @wordpress/rich-text, it's IMO a sub-optimal dependency chain, and we shouldn't double down on it.

Which part is sub-optimal in your opinion? It looks like the only thing rich-text adds is the @wordpress/data dependency, which I think it used for storing formats that are added at runtime. Probably not an ideal dep. Other than that the two packages seem to share the same deps already.

There's definitely future risk if rich-text adds more dependencies.

@adamsilverstein adamsilverstein changed the title Fields package: Add RichText field (via new @wordpress/rich-text-control package) DataViews: Add a richtext control backed by a new @wordpress/rich-text-control package Jun 9, 2026
@adamsilverstein

Copy link
Copy Markdown
Member Author

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.

I merge in and closed the other PR in favor of this one.

…ol-package

# Conflicts:
#	packages/dataviews/CHANGELOG.md
#	packages/fields/CHANGELOG.md
@ciampo

ciampo commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Even if Autocomplete already depends on @wordpress/rich-text, it's IMO a sub-optimal dependency chain, and we shouldn't double down on it.

Which part is sub-optimal in your opinion? It looks like the only thing rich-text adds is the @wordpress/data dependency, which I think it used for storing formats that are added at runtime. Probably not an ideal dep. Other than that the two packages seem to share the same deps already.

There's definitely future risk if rich-text adds more dependencies.

On a conceptual level, it's about decoupling a package that should be focused on UI-only componentry (@wordpress/components) from dependencies related to how the Gutenberg app works. Decoupling it makes it a lot easier to maintain and compose, also outside of the Gutenberg repository.

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
…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.
@github-actions github-actions Bot removed the [Package] Fields /packages/fields label Jun 18, 2026
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.
@adamsilverstein adamsilverstein requested a review from a team as a code owner June 18, 2026 17:50
@github-actions github-actions Bot added the [Package] Components /packages/components label Jun 18, 2026
- 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.
@github-actions github-actions Bot removed the [Package] Components /packages/components label Jun 18, 2026
The richtext control's code lives in this PR (#78471), so link the
CHANGELOG entry here instead of #78825. Satisfies the dataviews
CHANGELOG check.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Block editor /packages/block-editor [Package] DataViews /packages/dataviews [Package] Format library /packages/format-library [Package] Private APIs /packages/private-apis [Package] Rich text /packages/rich-text [Type] Feature New feature to highlight in changelogs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants