Skip to content

Table row drag moves two rows instead of one due to duplicate dropHandler invocation #2691

@LimChaeJune

Description

@LimChaeJune

What’s broken?

Bug Description

Dragging a row in a table sometimes moves two rows (the dragged row plus
an adjacent one) instead of one. The duplicate move happens because the
dropHandler registered by TableHandlesView is invoked twice for a single
user drop:

  1. First, on the synthetic drop event re-dispatched by the SideMenu plugin.
  2. Then, on the original drop event as it bubbles to pmView.root.

Between the two invocations, ProseMirror commits the first transaction,
view.update() refreshes TableHandlesView.state.block, and the second
invocation operates on the updated block — moving an additional row.

Affected version

@blocknote/core@0.46.2

Root Cause Analysis

SideMenuView registers a drop listener in capture phase and re-dispatches
a synthetic drop event whenever it considers the drop point outside the
editor bounds:

// packages/core/src/extensions/SideMenu/SideMenuPlugin.ts
this.pmView.root.addEventListener("drop", this.onDrop, true);
// ...
onDrop = (event) => {
  if (event.synthetic) return;
  const t = this.getDragEventContext(event);
  // ...
  if (!t.isDropWithinEditorBounds && t.isDropPoint) {
    this.dispatchSyntheticEvent(event);
  }
  // ...
};

dispatchSyntheticEvent(e) {
  const t = new Event(e.type, e);
  // ...
  t.synthetic = true;
  this.pmView.dom.dispatchEvent(t);
}

For a table-row drop, isDropPoint is true and isDropWithinEditorBounds is
sometimes false, so the synthetic dispatch fires. The synthetic event bubbles
to pmView.root, where TableHandlesView.dropHandler is registered (bubble
phase). Then the original event continues bubbling and triggers dropHandler
again.

TableHandlesView.dropHandler does not check event.synthetic and does
not clear state.draggingState after handling, so both invocations:

  • pass the state.draggingState !== undefined guard,
  • read state.block (refreshed by view.update() between calls),
  • call moveRow again with the new row at index originalIndex.

Result: a second row is moved.

Confirmation Logs

Instrumented dropHandler showed two invocations on a single user drop:

[BN dropHandler ENTRY] instanceId=1 isSynthetic=true  eventTarget=DIV
eventPhase=3 defaultPrevented=false
[BN row-drop] before  { originalIndex: 1, targetRowIndex: 8, snapshotRows: 9,
blockId: ... }
[BN row-drop] after moveRow { newRowsCount: 9 }
[BN dropHandler ENTRY] instanceId=1 isSynthetic=undefined eventTarget=DIV
eventPhase=3 defaultPrevented=true
[BN row-drop] before  { originalIndex: 1, targetRowIndex: 8, snapshotRows: 9,
blockId: ... }
[BN row-drop] after moveRow { newRowsCount: 9 }

Both calls share instanceId=1 (one TableHandlesView), confirming this is a
single instance receiving two events — the synthetic one (from SideMenu) and
the real one.

Suggested Fix

A. Skip synthetic events in TableHandles.dropHandler (mirrors what
SideMenuView's own handlers already do):

We've shipped (A) as a yarn patch workaround in our app and verified the bug
is gone.

I'd be happy to open a PR with this fix — would that be okay? Let me know
if you'd prefer approach A or something different, and I'll send it along.

What did you expect to happen?

Dragging a single table row should move only that one row to the drop target.

For example, in a table with rows R1–R10:

  • When the user grabs the row handle of R2 and drops it after R10,
  • The expected order is: R1, R3, R4, R5, R6, R7, R8, R9, R10, R2
  • Only R2 should be repositioned. All other rows should keep their original
    order.

Instead, the dragged row and the row immediately after it (e.g., R2 and R3)
both get moved to the drop target, producing: R1, R4, R5, R6, R7, R8, R9, R10,
R2, R3.

Steps to reproduce

  1. Open the BlockNote demo with a table block (no merged cells required).
  2. Insert at least 5 rows.
  3. Drag the second row using the row handle and drop it after the last row.
  4. Expected: only the dragged row is moved.
  5. Actual: the dragged row and the row originally below it are both moved
    to the bottom.

BlockNote version

v0.46.2

Environment

Chrome 146.0.7680.178 (arm64) , macOS 26.3.1 , React 18

Additional context

No response

Contribution

  • I'd be interested in contributing a fix for this issue

Sponsor

  • I'm a sponsor and would appreciate if you could look into this sooner than later 💖

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions