diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.test.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.test.ts index bd1b98cb78..a1797c0299 100644 --- a/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.test.ts +++ b/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.test.ts @@ -306,4 +306,23 @@ describe("SuggestionMenu", () => { editor._tiptapEditor.destroy(); }); + + it("does not throw when the menu opens while not editable (#2701)", () => { + const editor = createEditor(); + editor.setTextCursorPosition(editor.document[0].id, "end"); + + // Toggling to non-editable right before the menu opens (as a controlled + // `editable` prop does under load) means the menu view never gets shown, so + // its `state` stays undefined. The stop/non-editable update path used to + // call `emitUpdate` unconditionally and throw. + editor.isEditable = false; + + expect(() => + editor + .getExtension(SuggestionMenu)! + .openSuggestionMenu("/", { deleteTriggerCharacter: true }), + ).not.toThrow(); + + editor._tiptapEditor.destroy(); + }); }); diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts index 62d01c77a6..01178bb432 100644 --- a/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts +++ b/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts @@ -84,10 +84,15 @@ class SuggestionMenuView { this.pluginState = stopped ? prev : next; if (stopped || !this.editor.isEditable) { + // If the menu was never actually shown (`state` is still undefined - e.g. + // it was opened while the editor was not editable, which happens when the + // editable prop is toggled right before the menu opens), there is nothing + // to hide, and calling `emitUpdate` here would throw "Attempting to update + // uninitialized suggestions menu" (#2701). if (this.state) { this.state.show = false; + this.emitUpdate(this.pluginState!.triggerCharacter); } - this.emitUpdate(this.pluginState!.triggerCharacter); return; }