Skip to content

fix: move.block guards against moving a block onto its own path#2688

Open
christianhg wants to merge 1 commit into
mainfrom
fix/move-block-same-path
Open

fix: move.block guards against moving a block onto its own path#2688
christianhg wants to merge 1 commit into
mainfrom
fix/move-block-same-path

Conversation

@christianhg
Copy link
Copy Markdown
Member

The move.block operation handler unsets the source block, then inserts the same node before or after the destination path. When the destination resolves to the same block as the source, the unset shifts the surrounding indices and the follow-up insert lands at a path that no longer points at the intended slot. The block is eaten and the surrounding value silently loses an entry.

The high-level move.block up / move.block down events do not hit this shape because they guard via getSibling, which returns undefined at the boundary and bails. Consumers reaching for the raw move.block event with caller-supplied origin and destination keys (custom drag-and-drop plugins, behaviors that compose the event from arbitrary path inputs) have no such guard - a drop on the same block was data loss.

Reproduction

editor.send({
  type: 'move.block',
  at: [{_key: 'k1'}],
  to: [{_key: 'k1'}],
})

// Before: value is now [k2, k3] (k1 is gone)
// After:  value is unchanged [k1, k2, k3]

Fix

Early return from moveBlockOperationImplementation when origin and destination resolve to the same block.

A single failing scenario in tests/event.move.block.test.tsx covers the case. Reverting the guard drops it back to red across all three browsers (chromium / firefox / webkit).

The `move.block` operation handler unsets the source block, then
inserts the same node before or after the destination path. When
the destination resolves to the same block as the source, the
`unset` shifts the surrounding indices and the follow-up `insert`
lands at a path that no longer points at the intended slot. The
block is eaten and the surrounding value silently loses an entry.

The high-level `move.block up` / `move.block down` events do not
hit this shape because they guard via `getSibling`, which returns
`undefined` at the boundary and bails. Consumers reaching for the
raw `move.block` event with caller-supplied origin and destination
keys (custom drag-and-drop plugins, behaviors that compose the
event from arbitrary path inputs) have no such guard - a drop on
the same block was data loss.

Adds an early return when origin and destination resolve to the
same block. A single failing scenario in
`tests/event.move.block.test.tsx` covers the case: sending
`move.block` with `at` and `to` pointing at the same block
leaves the value unchanged. Reverting the guard drops it back to
red across all three browsers.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
portable-text-editor-documentation Ready Ready Preview, Comment May 21, 2026 10:21pm
portable-text-example-basic Ready Ready Preview, Comment May 21, 2026 10:21pm
portable-text-playground Ready Ready Preview, Comment May 21, 2026 10:21pm
racetrack Ready Ready Preview, Comment May 21, 2026 10:21pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 21, 2026

🦋 Changeset detected

Latest commit: d537930

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@portabletext/editor Patch
@portabletext/plugin-character-pair-decorator Patch
@portabletext/plugin-emoji-picker Patch
@portabletext/plugin-input-rule Patch
@portabletext/plugin-markdown-shortcuts Patch
@portabletext/plugin-one-line Patch
@portabletext/plugin-paste-link Patch
@portabletext/plugin-sdk-value Patch
@portabletext/plugin-typeahead-picker Patch
@portabletext/plugin-typography Patch
@portabletext/toolbar Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

📦 Bundle Stats — @portabletext/editor

Compared against main (bf170214)

@portabletext/editor

Metric Value vs main (bf17021)
Internal (raw) 765.1 KB +72 B, +0.0%
Internal (gzip) 147.1 KB +14 B, +0.0%
Bundled (raw) 1.37 MB +72 B, +0.0%
Bundled (gzip) 307.9 KB +13 B, +0.0%
Import time 93ms -1ms, -1.2%

@portabletext/editor/behaviors

Metric Value vs main (bf17021)
Internal (raw) 467 B -
Internal (gzip) 207 B -
Bundled (raw) 424 B -
Bundled (gzip) 171 B -
Import time 2ms -0ms, -1.9%

@portabletext/editor/plugins

Metric Value vs main (bf17021)
Internal (raw) 2.7 KB -
Internal (gzip) 894 B -
Bundled (raw) 2.5 KB -
Bundled (gzip) 827 B -
Import time 7ms -0ms, -0.7%

@portabletext/editor/selectors

Metric Value vs main (bf17021)
Internal (raw) 79.9 KB -
Internal (gzip) 14.6 KB -
Bundled (raw) 75.9 KB -
Bundled (gzip) 13.6 KB -
Import time 8ms -0ms, -1.3%

@portabletext/editor/traversal

Metric Value vs main (bf17021)
Internal (raw) 20.0 KB -
Internal (gzip) 4.0 KB -
Bundled (raw) 20.4 KB -
Bundled (gzip) 4.0 KB -
Import time 6ms -0ms, -0.7%

@portabletext/editor/utils

Metric Value vs main (bf17021)
Internal (raw) 30.0 KB -
Internal (gzip) 6.1 KB -
Bundled (raw) 27.9 KB -
Bundled (gzip) 5.7 KB -
Import time 6ms -0ms, -0.1%

🗺️ . · ./behaviors · ./plugins · ./selectors · ./traversal · ./utils · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant