Skip to content

Commit f05905a

Browse files
fix: Improper accessing of editor DOM element (#2234)
Co-authored-by: yousefed <yousefdardiry@gmail.com>
1 parent f23482d commit f05905a

File tree

6 files changed

+93
-7
lines changed

6 files changed

+93
-7
lines changed

packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export async function handleFileInsertion<
160160

161161
insertedBlockId = editor.transact((tr) => {
162162
const posInfo = getNearestBlockPos(tr.doc, pos.pos);
163-
const blockElement = editor.prosemirrorView.dom.querySelector(
163+
const blockElement = editor.domElement?.querySelector(
164164
`[data-id="${posInfo.node.attrs.id}"]`,
165165
);
166166

packages/core/src/editor/BlockNoteEditor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,7 @@ export class BlockNoteEditor<
777777
if (this.headless) {
778778
return;
779779
}
780-
this.prosemirrorView.dom.blur();
780+
this.domElement?.blur();
781781
}
782782

783783
// TODO move to extension

packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,12 @@ export const CreateLinkButton = () => {
9494
}
9595
};
9696

97-
editor.prosemirrorView.dom.addEventListener("keydown", callback);
97+
editor.domElement?.addEventListener("keydown", callback);
9898

9999
return () => {
100-
editor.prosemirrorView.dom.removeEventListener("keydown", callback);
100+
editor.domElement?.removeEventListener("keydown", callback);
101101
};
102-
}, [editor.prosemirrorView, editor.headless]);
102+
}, [editor.domElement]);
103103

104104
if (state === undefined) {
105105
return null;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { BlockNoteEditor } from "@blocknote/core";
2+
import { BlockNoteView } from "@blocknote/mantine";
3+
import "@blocknote/mantine/style.css";
4+
import { FormattingToolbar } from "@blocknote/react";
5+
import { flushSync } from "react-dom";
6+
import { createRoot } from "react-dom/client";
7+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
8+
9+
describe("FormattingToolbar unmount", () => {
10+
let div: HTMLDivElement;
11+
12+
beforeEach(() => {
13+
div = document.createElement("div");
14+
document.body.appendChild(div);
15+
});
16+
17+
afterEach(() => {
18+
document.body.removeChild(div);
19+
});
20+
21+
it("should not throw error when unmounting editor with FormattingToolbar", () => {
22+
const editor = BlockNoteEditor.create();
23+
24+
const root = createRoot(div);
25+
26+
// Mount the editor with FormattingToolbar
27+
// This will cause CreateLinkButton to mount and register its event listeners
28+
flushSync(() => {
29+
root.render(
30+
<BlockNoteView editor={editor}>
31+
<FormattingToolbar />
32+
</BlockNoteView>,
33+
);
34+
});
35+
36+
// Unmount should not throw error
37+
// Before the fix in commit 17bff6362, this would throw:
38+
// "Cannot read properties of undefined (reading 'removeEventListener')"
39+
// because CreateLinkButton's useEffect cleanup tries to access
40+
// editor.prosemirrorView.dom.removeEventListener()
41+
// but prosemirrorView.dom is undefined when the editor is being destroyed
42+
expect(() => {
43+
root.unmount();
44+
}).not.toThrow();
45+
46+
// Cleanup
47+
editor._tiptapEditor.destroy();
48+
});
49+
50+
it("should handle rapid mount/unmount cycles", () => {
51+
// Test multiple mount/unmount cycles
52+
for (let i = 0; i < 3; i++) {
53+
const editor = BlockNoteEditor.create();
54+
const root = createRoot(div);
55+
56+
flushSync(() => {
57+
root.render(
58+
<BlockNoteView editor={editor}>
59+
<FormattingToolbar />
60+
</BlockNoteView>,
61+
);
62+
});
63+
64+
// Should not throw on unmount
65+
expect(() => {
66+
root.unmount();
67+
}).not.toThrow();
68+
69+
editor._tiptapEditor.destroy();
70+
}
71+
});
72+
});

tests/vite.config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import * as path from "path";
22
import { defineConfig } from "vite";
3-
import eslintPlugin from "vite-plugin-eslint";
43

54
// https://vitejs.dev/config/
65
export default defineConfig((conf) => ({
76
test: {
87
environment: "jsdom",
98
setupFiles: ["./vitestSetup.ts"],
10-
include: ["./src/unit/**/*.test.ts"],
9+
include: ["./src/unit/**/*.test.ts", "./src/unit/**/*.test.tsx"],
1110
},
1211
resolve: {
1312
alias:

tests/vitestSetup.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,18 @@ class DragEventMock extends Event {
3333
};
3434
}
3535
(global as any).DragEvent = DragEventMock;
36+
37+
// Mock matchMedia for Mantine
38+
Object.defineProperty(window, "matchMedia", {
39+
writable: true,
40+
value: (query: string) => ({
41+
matches: false,
42+
media: query,
43+
onchange: null,
44+
addListener: () => {}, // deprecated
45+
removeListener: () => {}, // deprecated
46+
addEventListener: () => {},
47+
removeEventListener: () => {},
48+
dispatchEvent: () => {},
49+
}),
50+
});

0 commit comments

Comments
 (0)