Skip to content

Notes: support notes spanning multiple blocks - #80009

Open
adamsilverstein wants to merge 21 commits into
trunkfrom
feature/73416-multi-block-notes
Open

Notes: support notes spanning multiple blocks#80009
adamsilverstein wants to merge 21 commits into
trunkfrom
feature/73416-multi-block-notes

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Jul 8, 2026

Copy link
Copy Markdown
Member

What?

Adds support for Notes that span multiple adjacent blocks, so a reviewer can leave one note on feedback that flows from one block into the next (a paragraph into the following paragraph, a heading and its follow-up, and so on).

Fixes #73416.

Why?

Today a Note anchors to a single block, forcing reviewers to pick one arbitrary block when their feedback really applies to a range that crosses block boundaries. Cross-block Notes is a tracked WordPress 7.1 Notes enhancement (part of #76316).

How?

A multi-block Note is modelled as an inline note whose core/note marker spans several blocks, all sharing one note id - reusing the shipped inline-notes marker infrastructure (#78218). The block editor's selection state is the only primitive that expresses a range from an offset in one block to an offset in another; a new reader turns it into per-block segments:

  • the first block is marked from the caret to the end of its text,
  • interior blocks are marked in full,
  • the last block is marked from its start to the caret.

Each spanned block gets the note id in its metadata.noteId, and where a segment covers text, a shared <mark class="wp-note" data-id="N">. Because the marker lives in the content (not stored offsets), merge/split/move stability largely comes for free, and the existing highlight CSS, front-end marker strip, and floating-thread alignment already target every marker with a given data-id regardless of which block holds it.

Since the inline "Add note" button lives in the rich-text format toolbar (unmounted during a multi-block selection) and the single-block menu item only shows for one selected block, a new "Add note" entry appears in the block options (⋮) menu when the selection spans more than one block.

Key changes

  • collab-sidebar/utils.js: readMultiBlockSelection() (selection → ordered per-block segments) and findRichTextAttributeKey() (interior-block attribute detection).
  • collab-sidebar/hooks.js: onCreate writes the shared marker + metadata over every segment; useNoteThreads anchors each note to its topmost block and emits it once; onDelete/resolve strip the marker + metadata across every spanned block.
  • collab-sidebar/add-note-to-selection-menu-item.js: the multi-block entry point (via BlockSettingsMenuControls).

Testing Instructions

Test in WordPress Playground

  1. On a post that supports Notes, insert three paragraphs with some text.
  2. Select text starting in the first paragraph and dragging into the third.
  3. Open the block options (⋮) menu and choose Add note; type a note and submit.
  4. Confirm a single Note thread appears, anchored at the first block, and that all three blocks are highlighted (they share one marker id).
  5. Edit around the range (split/merge/move a block) and confirm the highlight follows the text.
  6. Delete the note and confirm every block's highlight is removed and the text is intact.
  7. View the post on the front end and confirm no <mark> markers leak into the output.

Automated tests

  • Unit: npm run test:unit packages/editor/src/components/collab-sidebar (segment reader, attribute detection).
  • E2E: the Multi-block notes describe in test/e2e/specs/editor/various/block-notes.spec.js.

Out of scope (follow-ups)

  • Whole-block sets, non-text blocks, and non-adjacent / cross-root selections.
  • Multi-block spotlight dimming (the reducer stores a single client id today); the per-marker tint provides the visual anchor.
  • Robust metadata.noteId re-sync when a split moves a marker into a brand-new block (the marker still renders and delete still cleans it up).

Screenshots

A comment spanning two blocks:
image

Add two helpers to the collab-sidebar utils:

- readMultiBlockSelection() turns the block editor's selection state (the
  only primitive expressing a range from an offset in one block to an
  offset in another) into an ordered list of per-block segments: the first
  block from the caret to the end of its attribute, interior blocks in
  full, the last block from 0 to the caret. Reversed selections are
  normalized to document order; collapsed, single-block, and cross-root
  selections return null.
- findRichTextAttributeKey() detects a block's primary editable attribute
  as the first one whose value is a RichTextData instance, so interior
  blocks are marked without block-type introspection.

These describe where a shared core/note marker should be applied so a note
can span multiple adjacent blocks. Covered by unit tests.

Part of #73416.
Make the notes data layer multi-block aware:

- onCreate now resolves the anchor as an ordered list of segments (a
  single-block inline selection, a cross-block text selection, or the
  selected block as a block-level anchor) and writes the note id into each
  spanned block's metadata plus, where a segment covers text, a shared
  core/note marker.
- useNoteThreads maps each note id to its topmost (first, document order)
  block so a floating thread aligns to the start of the range, and emits
  each note once even when it is listed in several blocks' metadata.
- onDelete and clearInlineNoteMarker now scan every block, stripping the
  note's metadata id and inline marker wherever they appear, so a deleted
  or resolved multi-block note leaves nothing behind.

Part of #73416.
The inline "Add note" button lives in the rich-text format toolbar, which
is unmounted while multiple blocks are selected, and the single-block menu
item only shows for one selected block, so a cross-block selection has no
trigger today.

Add an "Add note" item to the block options menu via
BlockSettingsMenuControls, shown when the selection spans more than one
block. It opens the new-note form without selecting a block or toggling
spotlight - either would collapse the cross-block text selection before
onCreate can read it to place the shared marker.

Part of #73416.
Add a "Multi-block notes" describe to the block notes spec:

- selecting text across several paragraphs and choosing "Add note" from
  the block options menu anchors a single thread to every spanned block,
  each carrying a core/note marker that shares one data-id.
- deleting a multi-block note strips the marker from every block it spans
  while leaving the text intact.

Part of #73416.
@github-actions github-actions Bot added the [Package] Editor /packages/editor label Jul 8, 2026
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Size Change: +964 B (+0.01%)

Total Size: 7.72 MB

📦 View Changed
Filename Size Change
build/scripts/block-editor/index.min.js 421 kB +5 B (0%)
build/scripts/editor/index.min.js 497 kB +959 B (+0.19%)

compressed-size-action

The cross-block text selection collapses to a single block once focus
enters the sidebar note form, so reading it live at save time produced a
single-block note (one marker) instead of one spanning every block.

Capture the per-block marker segments at trigger time, while the selection
is still live, and stash them on the pending "new" note via selectNote's
options; onCreate consumes them (new getPendingNoteSegments selector),
falling back to the live selection for single-block/inline notes. Every
note entry point calls selectNote, so the stashed segments are naturally
cleared when a different note is started.

Part of #73416.
Select upward from the bottom block instead of clicking the top block:
the selected block's toolbar popover renders above its block, so clicking
the top paragraph could be intercepted by it. Working from the bottom
block keeps the click target clear.

Part of #73416.
The new-note form only renders when a single block is selected
(add-note.js bails on an empty getSelectedBlockClientId), but a multi-block
text selection has no single selected block, so triggering the form left
it empty and no note could be created.

Select the first spanned block after capturing the segments: the form now
renders, and because the segments were captured before this selection
change, onCreate still marks every block in the original range.

Part of #73416.
…cement

Creating a note across a multi-block selection produced no inline markers.
The captured per-block segments were stashed on the pending note's
`selectNote` options, but the focus-reset and block-transition effects
re-dispatch `selectNote` without options and wiped the segments before the
note saved, so `onCreate` fell back to a block-level anchor with no markers.

Hold the captured segments in a dedicated `pendingNoteSegments` store field
that those reactive `selectNote` calls can't clobber, and clear it once the
note is created or the form is dismissed.

Also surface the multi-block "Add note" through the same `NoteIconSlotFill`
slot as the single-block item, so it sits in the block actions group (after
"Add after") instead of the tools group. The two entry-point components are
consolidated into one that branches on the selection count.
Selecting across blocks with the keyboard left the first block's segment
empty when the range started on a block boundary, so its marker was dropped
and the three-block assertion flaked. Establish the cross-block text range
through the store so every spanned block reliably carries a text segment; the
menu, form, and submit that follow still exercise the real UI.
@github-actions github-actions Bot added the [Package] Block editor /packages/block-editor label Jul 8, 2026
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Flaky tests detected in df3d0f3.
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/29374081686
📝 Reported issues:

Selecting "Add note" across two or more blocks opened the new-note form
and then immediately discarded it, so nothing appeared. Opening the form
selects the anchor block, which collapses the cross-block selection and
briefly moves DOM focus onto the block (or to the document body). The
form's blur handler treated that transient focus loss as the user
dismissing the note and cancelled it before it ever rendered.

Only cancel the form when focus moves to another real element: ignore a
blur whose relatedTarget is null (focus went nowhere), which is the
transient state during the selection collapse. Also defer opening the
form until the collapse and its focus move have settled, so the input
keeps focus and the user can type right away.

Wire the new-note keyboard shortcut to the whole selection when more than
one block is selected, mirroring the "Add note" menu, and show the same
shortcut on the multi-block menu item so it matches the single-block one.
@adamsilverstein
adamsilverstein marked this pull request as ready for review July 9, 2026 03:15
@adamsilverstein
adamsilverstein requested a review from ellatrix as a code owner July 9, 2026 03:15
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Warning: Type of PR label mismatch

To merge this PR, it requires exactly 1 label indicating the type of PR. Other labels are optional and not being checked here.

  • Required label: Any label starting with [Type].
  • Labels found: [Status] In Progress, [Package] Editor, [Package] Block editor, [Feature] Notes.

Read more about Type labels in Gutenberg. Don't worry if you don't have the required permissions to add labels; the PR reviewer should be able to help with the task.

@github-actions

github-actions Bot commented Jul 9, 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: adamsilverstein <adamsilverstein@git.wordpress.org>
Co-authored-by: jeffpaul <jeffpaul@git.wordpress.org>

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

@adamsilverstein adamsilverstein added [Status] In Progress Tracking issues with work in progress [Feature] Notes Phase 3 of the Gutenberg roadmap around block commenting labels Jul 9, 2026
@adamsilverstein adamsilverstein self-assigned this Jul 9, 2026
adamsilverstein and others added 6 commits July 8, 2026 20:33
Selecting a note spotlights the editor and selects the note's block, so
every other block dims. A note that spans several blocks only selected its
anchor, leaving the rest of its own range dimmed.

Track every spanned block on the thread and multi-select the range, which
keeps all of them at full opacity under the spotlight. `getSelectedBlockClientId`
returns null for a multi-selection, so the sidebar falls back to the first
block of the range to keep the note in context.
Rename the multi-block notes modules from .js to .ts/.tsx and type
them, since the editor package disables checkJs and plain .js modules
were never type-checked. Add JSDoc param docs to note-form.js and
note.js so their optional props infer correctly at the typed call
sites, and drop the ignored second argument previously passed to
disableComplementaryArea.
…block-notes

# Conflicts:
#	packages/editor/src/components/collab-sidebar/notes.tsx
CI type-checks a clean tree where @wordpress/block-editor and
@wordpress/interface cannot resolve type declarations (no types field
and no built entry points at check time), so the @ts-expect-error
directives on those imports are required. They only appear unused in
a locally built tree where the package artifacts exist.
A manual report suggested selecting a note spanning three paragraphs no
longer lit every covered block, unlike the two-block case. The behavior
could not be reproduced at HEAD in any flow (keyboard, drag, Shift+Click
selections; both sidebars; wp-env and Playground builds), but the
two-block test left the interior-block case uncovered. Pin the expected
behavior so a real regression here fails CI.
Resolve conflicts in the notes sidebar: keep trunk's rich-text note form,
focus-popover carve-outs and speak()-based resolve/reopen announcements,
while preserving the multi-block segment anchoring and TypeScript casts.
…block-notes

# Conflicts:
#	packages/editor/src/components/collab-sidebar/add-note.tsx
#	packages/editor/src/components/collab-sidebar/note-form.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] Notes Phase 3 of the Gutenberg roadmap around block commenting [Package] Block editor /packages/block-editor [Package] Editor /packages/editor [Status] In Progress Tracking issues with work in progress

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support Notes on content spanning multiple blocks

1 participant