From 94ecb30d5431390ddf82a5f3c6808cadb6cbcb1f Mon Sep 17 00:00:00 2001 From: Kevin Fitzpatrick Date: Fri, 1 May 2026 11:16:55 -0700 Subject: [PATCH] fix(extension-floating-menu): guard updatePosition against destroyed editor view (#7764) * chore(changeset): add changeset for floating-menu updatePosition guard * fix(extension-floating-menu): guard updatePosition against destroyed editor view * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: bdbch <6538827+bdbch@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .changeset/angry-bees-prove.md | 5 +++++ .../__tests__/floating-menu-plugin.spec.ts | 21 +++++++++++++++++++ .../src/floating-menu-plugin.ts | 4 ++++ 3 files changed, 30 insertions(+) create mode 100644 .changeset/angry-bees-prove.md diff --git a/.changeset/angry-bees-prove.md b/.changeset/angry-bees-prove.md new file mode 100644 index 0000000000..b2d4293fc9 --- /dev/null +++ b/.changeset/angry-bees-prove.md @@ -0,0 +1,5 @@ +--- +'@tiptap/extension-floating-menu': patch +--- + +Added a safeguard to avoid `TypeError: Cannot read properties of null (reading 'domFromPos')` being thrown when the editor was being destroyed diff --git a/packages/extension-floating-menu/__tests__/floating-menu-plugin.spec.ts b/packages/extension-floating-menu/__tests__/floating-menu-plugin.spec.ts index 9d4c3a61ad..e7a97a9902 100644 --- a/packages/extension-floating-menu/__tests__/floating-menu-plugin.spec.ts +++ b/packages/extension-floating-menu/__tests__/floating-menu-plugin.spec.ts @@ -225,3 +225,24 @@ describe('FloatingMenuView cross-contamination', () => { editor.destroy() }) }) + +describe('FloatingMenuView destroy safety', () => { + it('updatePosition should not call coordsAtPos when the editor view is detached from the DOM', () => { + const editor = createEditor() + const view = createFloatingMenuView(editor) + const coordsSpy = vi.spyOn(editor.view, 'coordsAtPos') + + try { + // Simulate the real-world teardown race: the editor is destroyed while a + // pending updatePosition call (debounced resize/scroll) is still in flight. + // ProseMirror's destroy removes view.dom from its parent and nulls docView; + // without a guard, posToDOMRect -> coordsAtPos throws on the null docView. + editor.destroy() + + expect(() => view.updatePosition()).not.toThrow(/Cannot read properties of null \(reading 'domFromPos'\)/) + expect(coordsSpy).not.toHaveBeenCalled() + } finally { + view.destroy() + } + }) +}) diff --git a/packages/extension-floating-menu/src/floating-menu-plugin.ts b/packages/extension-floating-menu/src/floating-menu-plugin.ts index e6ea4d41a4..e2a0b93475 100644 --- a/packages/extension-floating-menu/src/floating-menu-plugin.ts +++ b/packages/extension-floating-menu/src/floating-menu-plugin.ts @@ -432,6 +432,10 @@ export class FloatingMenuView { } updatePosition() { + if (!this.view?.dom?.parentNode) { + return + } + const { selection } = this.editor.state const domRect = posToDOMRect(this.view, selection.from, selection.to)