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:
- First, on the synthetic
drop event re-dispatched by the SideMenu plugin.
- 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
- Open the BlockNote demo with a
table block (no merged cells required).
- Insert at least 5 rows.
- Drag the second row using the row handle and drop it after the last row.
- Expected: only the dragged row is moved.
- 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
Sponsor
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
dropHandlerregistered byTableHandlesViewis invoked twice for a singleuser drop:
dropevent re-dispatched by the SideMenu plugin.dropevent as it bubbles topmView.root.Between the two invocations, ProseMirror commits the first transaction,
view.update()refreshesTableHandlesView.state.block, and the secondinvocation operates on the updated block — moving an additional row.
Affected version
@blocknote/core@0.46.2Root Cause Analysis
SideMenuViewregisters adroplistener in capture phase and re-dispatchesa synthetic
dropevent whenever it considers the drop point outside theeditor bounds:
For a table-row drop,
isDropPointis true andisDropWithinEditorBoundsissometimes false, so the synthetic dispatch fires. The synthetic event bubbles
to
pmView.root, whereTableHandlesView.dropHandleris registered (bubblephase). Then the original event continues bubbling and triggers
dropHandleragain.
TableHandlesView.dropHandlerdoes not checkevent.syntheticand doesnot clear
state.draggingStateafter handling, so both invocations:state.draggingState !== undefinedguard,state.block(refreshed byview.update()between calls),moveRowagain with the new row at indexoriginalIndex.Result: a second row is moved.
Confirmation Logs
Instrumented
dropHandlershowed two invocations on a single user drop:Both calls share
instanceId=1(oneTableHandlesView), confirming this is asingle instance receiving two events — the synthetic one (from SideMenu) and
the real one.
Suggested Fix
A. Skip synthetic events in
TableHandles.dropHandler(mirrors whatSideMenuView's own handlers already do):We've shipped (A) as a
yarn patchworkaround in our app and verified the bugis 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:
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
tableblock (no merged cells required).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
Sponsor