Skip to content

fix: keep drag.dragover cheap and depth-aware#2639

Closed
christianhg wants to merge 2 commits into
mainfrom
fix/drop-position-collapse-indirection
Closed

fix: keep drag.dragover cheap and depth-aware#2639
christianhg wants to merge 2 commits into
mainfrom
fix/drop-position-collapse-indirection

Conversation

@christianhg
Copy link
Copy Markdown
Member

@christianhg christianhg commented May 10, 2026

drag.dragover fires at 50–100Hz during an internal drag. Today the drop-indicator computation goes through the behavior pipeline as a pair of behaviors registered via createDropPositionBehaviorsConfig. The drag.dragover behavior recomputes getDragSelection, getSelectedBlocks, and isSelectingEntireBlocks on every event, even though all three derive from dragOrigin.selection which is stable for the entire drag. The same-block check is dragged._key === dropFocus._key - a depth-1 comparison from the same bug class as #2638 (keys are sibling-unique, not tree-unique).

The behavior shape was the wrong layer for what the code actually does. Behaviors compose synthetic events that transform the editor model. This handler reads position from a DOM event and writes to a React state setter via an effect action. It's a UI affordance, not a transform.

This PR moves the drop-indicator computation into useDropPosition directly, alongside the React state it drives. The dragged-blocks list and entire-blocks check are computed once with useMemo keyed on internalDrag. Per dragover does one getEnclosingBlock for the cursor's block plus an Array.some over the small dragged-blocks list, then setDropPosition. Same-block discrimination uses pathEquals instead of key compare, which closes the depth-shared-key bug class.

Editable calls the returned updateDropPosition from its existing handleDragOver callback, alongside the existing editor.send({type: 'behavior event', ...}) forwarding (so consumer plugins listening on drag.dragover keep working).

createDropPositionBehaviorsConfig and the two behaviors it returned are deleted. The DropPosition type moves to use-drop-position.ts.

7 existing dnd browser tests pass on chromium. 5 unit tests for resolveElementDropPosition (the per-element matcher from #2638) pass.

PR #2555 (the WeakMap memo on dragOrigin) is superseded by this - closing.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 10, 2026

🦋 Changeset detected

Latest commit: b1aab4f

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

@vercel
Copy link
Copy Markdown

vercel Bot commented May 10, 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 11, 2026 6:39am
portable-text-example-basic Ready Ready Preview, Comment May 11, 2026 6:39am
portable-text-playground Ready Ready Preview, Comment May 11, 2026 6:39am

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 10, 2026

📦 Bundle Stats — @portabletext/editor

Compared against main (1b64415c)

@portabletext/editor

Metric Value vs main (1b64415)
Internal (raw) 745.6 KB +1.4 KB, +0.2%
Internal (gzip) 143.1 KB +277 B, +0.2%
Bundled (raw) 1.35 MB +1.4 KB, +0.1%
Bundled (gzip) 304.1 KB +299 B, +0.1%
Import time 98ms -1ms, -0.7%

@portabletext/editor/behaviors

Metric Value vs main (1b64415)
Internal (raw) 467 B -
Internal (gzip) 207 B -
Bundled (raw) 424 B -
Bundled (gzip) 171 B -
Import time 2ms +0ms, +0.4%

@portabletext/editor/plugins

Metric Value vs main (1b64415)
Internal (raw) 3.6 KB -
Internal (gzip) 1021 B -
Bundled (raw) 3.4 KB -
Bundled (gzip) 952 B -
Import time 8ms -0ms, -0.5%

@portabletext/editor/selectors

Metric Value vs main (1b64415)
Internal (raw) 76.2 KB -33 B, -0.0%
Internal (gzip) 14.3 KB -24 B, -0.2%
Bundled (raw) 72.4 KB -33 B, -0.0%
Bundled (gzip) 13.3 KB -35 B, -0.3%
Import time 8ms -0ms, -0.5%

@portabletext/editor/traversal

Metric Value vs main (1b64415)
Internal (raw) 9.2 KB -
Internal (gzip) 2.4 KB -
Bundled (raw) 9.3 KB -
Bundled (gzip) 2.4 KB -
Import time 5ms +0ms, +0.8%

@portabletext/editor/utils

Metric Value vs main (1b64415)
Internal (raw) 30.6 KB -
Internal (gzip) 6.5 KB -
Bundled (raw) 28.4 KB -
Bundled (gzip) 6.1 KB -
Import time 6ms -0ms, -2.8%

🗺️ . · ./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.

@christianhg
Copy link
Copy Markdown
Member Author

Closing — after the fixup that restored the behavior-level preventDefault, this PR ends up doing the same guard compute twice per dragover (once in the behavior, once in the hook) without a real win. The depth-shared-key bug (_key === _key comparison missing the depth disambiguation #2638 fixed in the render layer) is a one-line swap in the existing behavior, not a restructure. Will open that as a focused fix.

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