Skip to content

Commit 73f4827

Browse files
authored
fix: prevent table row drag from moving an extra adjacent row (#2703)
1 parent f6717b3 commit 73f4827

2 files changed

Lines changed: 120 additions & 1 deletion

File tree

packages/core/src/extensions/TableHandles/TableHandles.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,8 @@ export class TableHandlesView implements PluginView {
466466
event.preventDefault();
467467

468468
const { draggingState, colIndex, rowIndex } = this.state;
469+
// Clear so a re-dispatched drop short-circuits above (issue #2691).
470+
this.state.draggingState = undefined;
469471

470472
const columnWidths = this.state.block.content.columnWidths;
471473

tests/src/end-to-end/tables/tables.test.ts

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { expect } from "@playwright/test";
12
import { test } from "../../setup/setupScript.js";
2-
import { BASE_URL } from "../../utils/const.js";
3+
import { BASE_URL, TABLE_SELECTOR } from "../../utils/const.js";
34
import { compareDocToSnapshot, focusOnEditor } from "../../utils/editor.js";
45
import { executeSlashCommand } from "../../utils/slashmenu.js";
56

@@ -67,4 +68,120 @@ test.describe("Check Table interactions", () => {
6768

6869
await compareDocToSnapshot(page, "shiftEnterNewLineInCell.json");
6970
});
71+
// Regression test for https://github.com/TypeCellOS/BlockNote/issues/2691.
72+
// Drops the dragged row to the LEFT of `.bn-block-group` (where the side
73+
// menu sits). SideMenuView re-dispatches drops outside `.bn-block-group`
74+
// (within 250px) as synthetic events; without the guard in
75+
// TableHandles.dropHandler, the synthetic drop AND the original drop both
76+
// run the row-move logic, dragging an adjacent row along with the target.
77+
test("Row drag should move only the dragged row", async ({
78+
page,
79+
browserName,
80+
}) => {
81+
test.skip(
82+
browserName === "firefox",
83+
"Playwright doesn't correctly simulate drag events in Firefox.",
84+
);
85+
86+
await focusOnEditor(page);
87+
await executeSlashCommand(page, "table");
88+
89+
// Replace the default table with a deterministic 5-row × 1-col table.
90+
await page.evaluate(() => {
91+
const cellAttrs = {
92+
textColor: "default",
93+
backgroundColor: "default",
94+
textAlignment: "left",
95+
colspan: 1,
96+
rowspan: 1,
97+
colwidth: null,
98+
};
99+
const rows = ["R1", "R2", "R3", "R4", "R5"].map((label) => ({
100+
type: "tableRow",
101+
content: [
102+
{
103+
type: "tableCell",
104+
attrs: cellAttrs,
105+
content: [
106+
{
107+
type: "tableParagraph",
108+
content: [{ type: "text", text: label }],
109+
},
110+
],
111+
},
112+
],
113+
}));
114+
(
115+
window as unknown as {
116+
ProseMirror: {
117+
commands: { setContent: (doc: unknown) => void };
118+
};
119+
}
120+
).ProseMirror.commands.setContent({
121+
type: "doc",
122+
content: [
123+
{
124+
type: "blockGroup",
125+
content: [
126+
{
127+
type: "blockContainer",
128+
attrs: { id: "0" },
129+
content: [
130+
{
131+
type: "table",
132+
attrs: { textColor: "default" },
133+
content: rows,
134+
},
135+
],
136+
},
137+
],
138+
},
139+
],
140+
});
141+
});
142+
await page.waitForFunction(
143+
() => document.querySelectorAll(".bn-editor tbody tr").length === 5,
144+
);
145+
146+
// Hover R2's first cell so its row drag handle becomes visible. The
147+
// row handle has no rotate transform (the column handle does).
148+
const rows = page.locator(`${TABLE_SELECTOR} tbody tr`);
149+
await rows.nth(1).locator("td").first().hover();
150+
const handle = page
151+
.locator(".bn-table-handle")
152+
.filter({ hasNot: page.locator(`[style*="rotate"]`) })
153+
.first();
154+
await handle.waitFor({ state: "visible" });
155+
const handleBox = (await handle.boundingBox())!;
156+
157+
// Drop into the side-menu area: LEFT of `.bn-block-group`, vertically
158+
// aligned with the last row. This is outside the block-group rect but
159+
// well within the 250px range that triggers SideMenuView's synthetic
160+
// drop re-dispatch — the same condition that surfaces the bug for
161+
// real users dragging onto the side gutter.
162+
const blockGroup = (await page
163+
.locator(".bn-block-group")
164+
.first()
165+
.boundingBox())!;
166+
const lastRowBox = (await rows.nth(4).locator("td").first().boundingBox())!;
167+
const dropX = blockGroup.x - 50;
168+
const dropY = lastRowBox.y + lastRowBox.height / 2;
169+
170+
await page.mouse.move(
171+
handleBox.x + handleBox.width / 2,
172+
handleBox.y + handleBox.height / 2,
173+
{ steps: 5 },
174+
);
175+
await page.mouse.down();
176+
await page.mouse.move(dropX, dropY, { steps: 10 });
177+
await page.mouse.up();
178+
179+
const order = (
180+
await page
181+
.locator(`${TABLE_SELECTOR} tbody tr td:first-child`)
182+
.allInnerTexts()
183+
).map((t) => t.trim());
184+
// Expected: only R2 moved. Buggy (#2691): R3 follows along.
185+
expect(order).toEqual(["R1", "R3", "R4", "R5", "R2"]);
186+
});
70187
});

0 commit comments

Comments
 (0)