diff --git a/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts index 76a52d6ac6..ced8f59b14 100644 --- a/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts +++ b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts @@ -160,7 +160,7 @@ export async function handleFileInsertion< insertedBlockId = editor.transact((tr) => { const posInfo = getNearestBlockPos(tr.doc, pos.pos); - const blockElement = editor.prosemirrorView.dom.querySelector( + const blockElement = editor.domElement?.querySelector( `[data-id="${posInfo.node.attrs.id}"]`, ); diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx index cfba83cc23..34f53b0119 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx @@ -93,12 +93,12 @@ export const CreateLinkButton = () => { } }; - editor.prosemirrorView.dom.addEventListener("keydown", callback); + editor.domElement?.addEventListener("keydown", callback); return () => { - editor.prosemirrorView.dom.removeEventListener("keydown", callback); + editor.domElement?.removeEventListener("keydown", callback); }; - }, [editor.prosemirrorView, editor.headless]); + }, [editor.domElement]); if (state === undefined) { return null; diff --git a/tests/src/unit/react/BlockNoteViewRemount.test.tsx b/tests/src/unit/react/BlockNoteViewRemount.test.tsx new file mode 100644 index 0000000000..4e0689153c --- /dev/null +++ b/tests/src/unit/react/BlockNoteViewRemount.test.tsx @@ -0,0 +1,72 @@ +import { BlockNoteEditor } from "@blocknote/core"; +import { BlockNoteView } from "@blocknote/mantine"; +import "@blocknote/mantine/style.css"; +import { FormattingToolbar } from "@blocknote/react"; +import { flushSync } from "react-dom"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +describe("FormattingToolbar unmount", () => { + let div: HTMLDivElement; + + beforeEach(() => { + div = document.createElement("div"); + document.body.appendChild(div); + }); + + afterEach(() => { + document.body.removeChild(div); + }); + + it("should not throw error when unmounting editor with FormattingToolbar", () => { + const editor = BlockNoteEditor.create(); + + const root = createRoot(div); + + // Mount the editor with FormattingToolbar + // This will cause CreateLinkButton to mount and register its event listeners + flushSync(() => { + root.render( + + + , + ); + }); + + // Unmount should not throw error + // Before the fix in commit 17bff6362, this would throw: + // "Cannot read properties of undefined (reading 'removeEventListener')" + // because CreateLinkButton's useEffect cleanup tries to access + // editor.prosemirrorView.dom.removeEventListener() + // but prosemirrorView.dom is undefined when the editor is being destroyed + expect(() => { + root.unmount(); + }).not.toThrow(); + + // Cleanup + editor._tiptapEditor.destroy(); + }); + + it("should handle rapid mount/unmount cycles", () => { + // Test multiple mount/unmount cycles + for (let i = 0; i < 3; i++) { + const editor = BlockNoteEditor.create(); + const root = createRoot(div); + + flushSync(() => { + root.render( + + + , + ); + }); + + // Should not throw on unmount + expect(() => { + root.unmount(); + }).not.toThrow(); + + editor._tiptapEditor.destroy(); + } + }); +}); diff --git a/tests/vite.config.ts b/tests/vite.config.ts index b76c27de4a..8f03c6efcf 100644 --- a/tests/vite.config.ts +++ b/tests/vite.config.ts @@ -1,13 +1,12 @@ import * as path from "path"; import { defineConfig } from "vite"; -import eslintPlugin from "vite-plugin-eslint"; // https://vitejs.dev/config/ export default defineConfig((conf) => ({ test: { environment: "jsdom", setupFiles: ["./vitestSetup.ts"], - include: ["./src/unit/**/*.test.ts"], + include: ["./src/unit/**/*.test.ts", "./src/unit/**/*.test.tsx"], }, resolve: { alias: diff --git a/tests/vitestSetup.ts b/tests/vitestSetup.ts index 9a869521e0..d9fa31cb56 100644 --- a/tests/vitestSetup.ts +++ b/tests/vitestSetup.ts @@ -33,3 +33,18 @@ class DragEventMock extends Event { }; } (global as any).DragEvent = DragEventMock; + +// Mock matchMedia for Mantine +Object.defineProperty(window, "matchMedia", { + writable: true, + value: (query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, // deprecated + removeListener: () => {}, // deprecated + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => {}, + }), +});