Skip to content

Commit 9cbf2ad

Browse files
authored
fix(table): prevent crash when pressing Enter in a table cell (#2793)
1 parent e4fb5c1 commit 9cbf2ad

2 files changed

Lines changed: 129 additions & 14 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { TextSelection } from "prosemirror-state";
2+
import { CellSelection } from "prosemirror-tables";
3+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
4+
5+
import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
6+
import type { PartialBlock } from "../defaultBlocks.js";
7+
8+
/**
9+
* @vitest-environment jsdom
10+
*/
11+
12+
function pressEnter(editor: BlockNoteEditor) {
13+
const view = editor.prosemirrorView;
14+
const event = new KeyboardEvent("keydown", { key: "Enter" });
15+
view.someProp("handleKeyDown", (f) => f(view, event));
16+
}
17+
18+
const testDocument: PartialBlock[] = [
19+
{
20+
id: "table-0",
21+
type: "table",
22+
content: {
23+
type: "tableContent",
24+
rows: [
25+
{ cells: ["Cell 1", "Cell 2", "Cell 3"] },
26+
{ cells: ["Cell 4", "Cell 5", "Cell 6"] },
27+
{ cells: ["Cell 7", "Cell 8", "Cell 9"] },
28+
],
29+
},
30+
},
31+
];
32+
33+
describe("Table Enter keyboard shortcut", () => {
34+
let editor: BlockNoteEditor;
35+
const div = document.createElement("div");
36+
37+
beforeAll(() => {
38+
editor = BlockNoteEditor.create();
39+
editor.mount(div);
40+
});
41+
42+
afterAll(() => {
43+
editor._tiptapEditor.destroy();
44+
editor = undefined as any;
45+
});
46+
47+
beforeEach(() => {
48+
editor.replaceBlocks(editor.document, testDocument);
49+
});
50+
51+
function posInCell(cellText: string): number {
52+
const view = editor.prosemirrorView;
53+
let pos = -1;
54+
view.state.doc.descendants((node, nodePos) => {
55+
if (pos === -1 && node.isText && node.text === cellText) {
56+
pos = nodePos;
57+
}
58+
return true;
59+
});
60+
if (pos === -1) {
61+
throw new Error(`Cell with text "${cellText}" not found`);
62+
}
63+
return pos;
64+
}
65+
66+
function setCursorInCell(cellText: string, offset = 1) {
67+
const pos = posInCell(cellText);
68+
editor.transact((tr) =>
69+
tr.setSelection(TextSelection.create(tr.doc, pos + offset)),
70+
);
71+
}
72+
73+
it("moves the selection to the cell below", () => {
74+
setCursorInCell("Cell 5");
75+
76+
pressEnter(editor);
77+
78+
const parentText =
79+
editor.prosemirrorView.state.selection.$head.parent.textContent;
80+
expect(parentText).toBe("Cell 8");
81+
});
82+
83+
it("does not crash and is a no-op on the last row", () => {
84+
setCursorInCell("Cell 8");
85+
86+
const before = editor.document;
87+
expect(() => pressEnter(editor)).not.toThrow();
88+
expect(editor.document).toStrictEqual(before);
89+
});
90+
91+
it("does not crash with a (non-empty) text selection in the last row", () => {
92+
const start = posInCell("Cell 8");
93+
editor.transact((tr) =>
94+
tr.setSelection(TextSelection.create(tr.doc, start, start + 4)),
95+
);
96+
97+
const before = editor.document;
98+
expect(() => pressEnter(editor)).not.toThrow();
99+
expect(editor.document).toStrictEqual(before);
100+
});
101+
102+
it("does not crash with a multi-cell selection", () => {
103+
const view = editor.prosemirrorView;
104+
const cellPositions: number[] = [];
105+
view.state.doc.descendants((node, pos) => {
106+
if (node.type.name === "tableCell" || node.type.name === "tableHeader") {
107+
cellPositions.push(pos);
108+
}
109+
return true;
110+
});
111+
112+
editor.transact((tr) =>
113+
tr.setSelection(
114+
CellSelection.create(
115+
tr.doc,
116+
cellPositions[0],
117+
cellPositions[1],
118+
) as any,
119+
),
120+
);
121+
122+
const before = editor.document;
123+
expect(() => pressEnter(editor)).not.toThrow();
124+
expect(editor.document).toStrictEqual(before);
125+
});
126+
});

packages/core/src/blocks/Table/TableExtension.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,15 @@ export const TableExtension = Extension.create({
3535
return {
3636
// Moves the selection to the cell below.
3737
Enter: () => {
38-
if (
39-
this.editor.state.selection.$head.parent.type.name !==
40-
"tableParagraph"
41-
) {
38+
if (!isInTable(this.editor.state)) {
4239
return false;
4340
}
4441

4542
return this.editor.commands.command(({ state, dispatch }) => {
46-
if (!isInTable(state)) {
47-
return false;
48-
}
49-
5043
const $cell = selectionCell(state);
51-
const $nextCell = nextCell($cell, "vert", 1);
52-
53-
if (!$nextCell) {
54-
return false;
55-
}
44+
const $nextCell = $cell ? nextCell($cell, "vert", 1) : null;
5645

57-
if (dispatch) {
46+
if ($nextCell && dispatch) {
5847
dispatch(
5948
state.tr
6049
.setSelection(

0 commit comments

Comments
 (0)