Notes: support notes spanning multiple blocks - #80009
Conversation
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.
|
Size Change: +964 B (+0.01%) Total Size: 7.72 MB 📦 View Changed
|
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.
|
Flaky tests detected in df3d0f3. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/29374081686
|
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.
|
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.
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. |
|
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. |
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
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/notemarker 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: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 givendata-idregardless 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) andfindRichTextAttributeKey()(interior-block attribute detection).collab-sidebar/hooks.js:onCreatewrites the shared marker + metadata over every segment;useNoteThreadsanchors 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 (viaBlockSettingsMenuControls).Testing Instructions
<mark>markers leak into the output.Automated tests
npm run test:unit packages/editor/src/components/collab-sidebar(segment reader, attribute detection).Multi-block notesdescribe intest/e2e/specs/editor/various/block-notes.spec.js.Out of scope (follow-ups)
metadata.noteIdre-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:
