Skip to content

Ghost placeholder + sidebar descriptor for moved blocks - #78412

Closed
adamsilverstein wants to merge 15 commits into
suggest-mode-6c-move-uifrom
try/suggest-mode-move-ghost
Closed

adamsilverstein wants to merge 15 commits into
suggest-mode-6c-move-uifrom
try/suggest-mode-move-ghost

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented May 18, 2026 •

Copy link
Copy Markdown
Member

Closes #78409. Stacked on #77979 (base branch suggest-mode-6c-move-ui).

What

Follow-up to the structural-suggestion stack, addressing review feedback from @saroshaga on #77979: a block-move suggestion previously only decorated the block at its new position, and the sidebar text was vague ("Moved <block> from a previous block to a different block").

This PR adds:

  1. Ghost placeholder at the original position — a non-interactive, struck-through "ghost" of the moved block (block icon + title + one-line excerpt) rendered where the block came from, tinted with the suggester's avatar color via the existing --suggestion-author-color variable so it visually pairs with the decorated block at the destination.
  2. Meaningful sidebar descriptor — BlockMoveDiff now computes direction/distance from the captured fromIndex vs. the live index: "Moved up 3 blocks" / "Moved down 1 block" for same-parent moves, "Moved into Group" / "Moved to top level" for cross-parent moves.

How

Presentational only — no block-editor store mutation, nothing syncs via CRDT. The block continues to exist exactly once in the tree.

  • move-ghost-index.js — pure buildMoveGhostIndex turning pending-move markers into an { after, before } anchor index (null-anchor / first-child and missing-anchor fall back to the old parent's first sibling; empty old parent → no ghost).
  • overlay-context.js — a single O(n) scan in SuggestionOverlayProvider exposes the index on context (a stable signature string keeps useSelect referentially stable; the index rebuilds via useMemo only on placement-affecting changes), avoiding an O(n²) per-block scan.
  • with-suggestion-overlay.js — the existing withSuggestionBlockClassName HOC renders <SuggestionMoveGhost> siblings around the anchor block.
  • suggestion-move-ghost.js — the presentational ghost (aria-hidden, pointer-events: none, never in the block list).
  • content-suggestion.scss — canvas-loaded ghost styling reusing the stack's suggestion-color tokens.
  • suggestion-diff.js — exported pure describeMove + rewritten BlockMoveDiff.

Out of scope (deferred)

A drawn connector/arrow between the ghost and the destination is intentionally not included — it needs an SVG overlay spanning two DOM positions inside the scrolling iframe canvas (reposition on scroll/resize), a separate risk profile. Tracked for later evaluation in #78409.

Testing

  • 170 suggestion-mode unit tests pass (15 new: pure index builder, ghost component, descriptor function, HOC ghost rendering). Lint + stylelint clean. Production build clean.
  • Manual matrix to verify (Suggest mode, login admin/password): move a paragraph up/down within a parent; move the first child out of a Group; move a top-level block into a Group; two moves in one doc; confirm a second peer sees no extra block and serialized content is unchanged.

Refs #77434, #77979.

@github-actions

github-actions Bot commented May 18, 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: saroshaga <saroshaga@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 requested review from Mamaduka and t-hamano and removed request for ajitbohra, ellatrix, fabiankaegy and juanmaguitar May 18, 2026 17:52
@adamsilverstein adamsilverstein added [Type] Enhancement A suggestion for improvement. [Feature] Notes Phase 3 of the Gutenberg roadmap around block commenting labels May 18, 2026
@adamsilverstein
adamsilverstein force-pushed the try/suggest-mode-move-ghost branch from d7a8301 to 6cc7282 Compare May 18, 2026 17:55
@github-actions

github-actions Bot commented May 18, 2026 •

Copy link
Copy Markdown

Size Change: +1.46 kB (+0.02%)

Total Size: 7.52 MB

📦 View Changed
Filename Size Change
build/scripts/editor/index.min.js 487 kB +1.09 kB (+0.23%)
build/styles/block-editor/content-rtl.css 5.92 kB +92 B (+1.58%)
build/styles/block-editor/content-rtl.min.css 4.47 kB +89 B (+2.03%)
build/styles/block-editor/content.css 5.92 kB +94 B (+1.61%)
build/styles/block-editor/content.min.css 4.47 kB +87 B (+1.99%)

compressed-size-action

@saroshaga

Copy link
Copy Markdown

The move ghost placeholder worked for me in Playground: I could see the “Moved from here” placeholder at the original position and the dotted “Suggested move” treatment at the destination.

image

A few minor follow-up questions:

  1. For ghost placement, could we confirm what happens when the block that used to be before the moved block has also moved? For example, if C is moved and its original previous sibling was B, but B is also moved elsewhere, would the ghost for C follow B to its new location? Should the ghost only use that previous sibling as an anchor if it is still in the original parent and is not itself pending-moved?

  2. What should happen when the moved block was the only child in its old parent, for example moving the only paragraph out of a Group? Since there is no remaining sibling anchor, it looks like no ghost is rendered. That may leave reviewers without an original-position cue for that case.

  3. For accessibility, the ghost is aria-hidden, and the destination “Suggested move” label is CSS-generated. Should there be a non-focusable accessible cue, such as a role="note" / visually-hidden label, so screen-reader users can also tell where the move came from?

@github-actions github-actions Bot added the [Package] Edit Site /packages/edit-site label Jun 16, 2026
@adamsilverstein

Copy link
Copy Markdown
Member Author

Thanks for the testing and the sharp follow-ups - all three are addressed:

  1. Ghost following a moved anchor - good catch, this was a real bug. buildMoveGhostIndex only checked that the anchor still existed, but a pending-moved previous sibling still exists, so the ghost trailed it to its destination. Anchor selection now excludes any pending-moved block (both the recorded previous-sibling and the first-child fallback), with the same exclusion mirrored in the overlay move-signature so the memo stays in sync. (44a4ef2d85e)

  2. Only child / no surviving sibling - now handled. Added an insideParent fallback so the ghost renders just below the emptied old parent. It's skipped at the root and when the old parent is itself gone or pending-moved (anchoring there would drag the ghost along). One caveat: it renders adjacent to the container rather than literally inside it - the editor.BlockListBlock filter can't inject into an empty parent's inner-block region - but the empty container plus the "Moved from here" label keep the origin clear. (1f8deed3e62)

  3. Accessibility - the ghost is now a role="note" with a visually-hidden "Original position of moved " sentence (the decorative label/excerpt stay aria-hidden to avoid a double read). The destination tab text also moved out of CSS content into a data-suggestion-move-label attribute (so it's translatable, not hardcoded English) and gained a visually-hidden destination cue. (3a489bd330a)

Each change has unit/HOC coverage. Thanks again!

@adamsilverstein
adamsilverstein force-pushed the suggest-mode-6c-move-ui branch from f2d1c3a to 688d72e Compare June 17, 2026 20:19
@adamsilverstein
adamsilverstein force-pushed the try/suggest-mode-move-ghost branch from 454ce57 to 9d3b2a7 Compare June 17, 2026 20:19
@adamsilverstein
adamsilverstein force-pushed the suggest-mode-6c-move-ui branch from 688d72e to 8b90242 Compare June 17, 2026 21:06
@adamsilverstein
adamsilverstein force-pushed the try/suggest-mode-move-ghost branch from 9d3b2a7 to c877200 Compare June 17, 2026 21:06
@adamsilverstein
adamsilverstein force-pushed the suggest-mode-6c-move-ui branch from 8b90242 to b5e52e8 Compare June 17, 2026 21:13
@adamsilverstein
adamsilverstein force-pushed the try/suggest-mode-move-ghost branch from c877200 to 497eb54 Compare June 17, 2026 21:13
@adamsilverstein
adamsilverstein force-pushed the suggest-mode-6c-move-ui branch from b5e52e8 to ce3c626 Compare June 17, 2026 21:20
@adamsilverstein
adamsilverstein force-pushed the try/suggest-mode-move-ghost branch from 497eb54 to 21d380f Compare June 17, 2026 21:21
useSelect returns a stable signature string; the index is rebuilt via
useMemo only when placement-affecting state changes, avoiding the
@wordpress/data 'different values' re-render warning.
A move ghost marks where a block came from by rendering next to the
block's old previous sibling. When that sibling is itself pending-moved
it still exists in the tree, so the prior `blockExists` check accepted
it as an anchor and the ghost followed the sibling to its new position
instead of staying at the original spot.

Exclude every pending-moved block from anchor selection in
`buildMoveGhostIndex` (both the recorded previous-sibling anchor and the
first-child fallback), and mirror the exclusion in the overlay
move-signature so the memoized index recomputes when only an anchor's
moved-state changes.
The move ghost was fully aria-hidden and the destination "Suggested
move" tab was a hardcoded English string baked into CSS `content`, so
screen-reader users got no signal that a block moved and non-English
users saw an untranslated tab.

- Ghost: expose it as a `role="note"` with a VisuallyHidden
  original-position sentence; keep the decorative label/icon/excerpt
  aria-hidden so the struck-through old content isn't read as live text.
- Destination: render the tab text from a `data-suggestion-move-label`
  attribute (set by the HOC, translated via `__()`) instead of a CSS
  string literal, and add a VisuallyHidden "Suggested move destination"
  cue so the move is announced on the canvas, not just in the sidebar.

Tests cover the note role, the hidden decorative duplicates, the
localized data attribute, and the destination cue.
When a moved block had no surviving sibling in its old parent (it was an
only child, or every sibling also moved away), the ghost had nothing to
anchor to and silently disappeared, leaving no original-position cue for
that case.

Add an `insideParent` index keyed on the old parent so the ghost renders
just below the now-empty container. It's skipped at the root (no parent
block to anchor on) and when the old parent is itself gone or
pending-moved (anchoring there would drag the ghost to the parent's
destination, the same displacement the anchor guard already prevents).
The overlay move-signature gains a parent-usable bit so the memoized
index recomputes when only the parent's existence or moved-state changes.

Unit tests cover the only-child fallback, the all-siblings-moved case,
the moved/removed-parent guard, and the root no-op; an HOC test asserts
the ghost renders below the emptied parent.
…nchor-guard commit

Commit 44a4ef2 (the move-ghost anchor guard) inadvertently staged
11 unrelated files left over from a stray lint --fix pass, including a
broken edit to resizable-frame (an eslint-disable directive was removed,
leaving an empty JSX expression) and removed playwright-await disables in
perf-utils. Restore all 11 files to their pre-suggestion state; none of
them belong to the move-ghost feature.
@adamsilverstein
adamsilverstein force-pushed the suggest-mode-6c-move-ui branch from ce3c626 to 55be6bc Compare June 18, 2026 06:35
@adamsilverstein
adamsilverstein force-pushed the try/suggest-mode-move-ghost branch from 21d380f to 43ec8b0 Compare June 18, 2026 06:36
@adamsilverstein adamsilverstein changed the title Suggest mode: ghost placeholder for moved blocks + meaningful sidebar descriptor Suggest mode: Ghost placeholder + sidebar descriptor for moved blocks Jun 30, 2026
@adamsilverstein adamsilverstein changed the title Suggest mode: Ghost placeholder + sidebar descriptor for moved blocks Ghost placeholder + sidebar descriptor for moved blocks Jun 30, 2026
@adamsilverstein

Copy link
Copy Markdown
Member Author

Superseded by the fresh-stack restructure of Suggest mode (#73411).

The feature has been re-sliced from this 16-deep stack onto current trunk as a clean 2-PR stack:

The whole feature now sits behind a Suggestion Mode experiment (Settings → Experiments → Collaboration). Closing in favour of the new stack — details: #73411 (comment)

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 [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants