diff --git a/patches/@blocknote__core@0.46.2.patch b/patches/@blocknote__core@0.46.2.patch index 9f70474d..0bb51d35 100644 --- a/patches/@blocknote__core@0.46.2.patch +++ b/patches/@blocknote__core@0.46.2.patch @@ -1,6 +1,33 @@ --- a/dist/defaultBlocks-DE5GNdJH.js +++ b/dist/defaultBlocks-DE5GNdJH.js -@@ -2761,7 +2761,7 @@ +@@ -2651,17 +2651,24 @@ + if (!a && !(o !== void 0 && r !== void 0) && !s) + return; + if (this.pluginState = s ? o : r, s || !this.editor.isEditable) { +- this.state && (this.state.show = !1), this.emitUpdate(this.pluginState.triggerCharacter); ++ if (!this.state) ++ return; ++ this.state.show = !1, this.emitUpdate(this.pluginState.triggerCharacter); + return; + } + const c = (l = this.rootEl) == null ? void 0 : l.querySelector( + `[data-decoration-id="${this.pluginState.decorationId}"]` + ); +- this.editor.isEditable && c && (this.state = { ++ if (!c) { ++ this.state && (this.state.show = !1, this.emitUpdate(this.pluginState.triggerCharacter)); ++ return; ++ } ++ this.state = { + show: !0, + referencePos: c.getBoundingClientRect().toJSON(), + query: this.pluginState.query +- }, this.emitUpdate(this.pluginState.triggerCharacter)); ++ }, this.emitUpdate(this.pluginState.triggerCharacter); + } + destroy() { + var t; +@@ -2761,7 +2768,7 @@ if (a === s) { const c = r.state.doc; for (const l of t) { @@ -53,6 +80,60 @@ } function kn(o, e) { const t = [ +--- a/src/extensions/SuggestionMenu/SuggestionMenu.ts ++++ b/src/extensions/SuggestionMenu/SuggestionMenu.ts +@@ -83,27 +83,34 @@ + this.pluginState = stopped ? prev : next; + + if (stopped || !this.editor.isEditable) { +- if (this.state) { +- this.state.show = false; +- } ++ if (!this.state) { ++ return; ++ } ++ ++ this.state.show = false; + this.emitUpdate(this.pluginState!.triggerCharacter); + + return; + } + + const decorationNode = this.rootEl?.querySelector( + `[data-decoration-id="${this.pluginState!.decorationId}"]`, + ); + +- if (this.editor.isEditable && decorationNode) { +- this.state = { +- show: true, +- referencePos: decorationNode +- .getBoundingClientRect() +- .toJSON() as DOMRect, +- query: this.pluginState!.query, +- }; +- +- this.emitUpdate(this.pluginState!.triggerCharacter!); ++ if (!decorationNode) { ++ if (this.state) { ++ this.state.show = false; ++ this.emitUpdate(this.pluginState!.triggerCharacter!); ++ } ++ ++ return; + } ++ ++ this.state = { ++ show: true, ++ referencePos: decorationNode ++ .getBoundingClientRect() ++ .toJSON() as DOMRect, ++ query: this.pluginState!.query, ++ }; ++ ++ this.emitUpdate(this.pluginState!.triggerCharacter!); + } + + destroy() { --- a/src/extensions/TableHandles/TableHandles.ts +++ b/src/extensions/TableHandles/TableHandles.ts @@ -572,9 +572,14 @@ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98d15dd4..91485112 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: patchedDependencies: '@blocknote/core@0.46.2': - hash: dc394d11ab419b36b8d306cbbb6ce39f0722e042b63b2b7d254c7d7f90782e94 + hash: 3ff0414270086514addc1fa21c7cb0d730d141d1d255ac4faca126085ddf35c7 path: patches/@blocknote__core@0.46.2.patch '@blocknote/react@0.46.2': hash: e09f7011df33f4ff92c0d3fd8c9060e62f08997d076f3e438e1e82f8c1ab2f76 @@ -27,10 +27,10 @@ importers: version: 0.78.0(zod@4.3.6) '@blocknote/code-block': specifier: ^0.46.2 - version: 0.46.2(@blocknote/core@0.46.2(patch_hash=dc394d11ab419b36b8d306cbbb6ce39f0722e042b63b2b7d254c7d7f90782e94)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)) + version: 0.46.2(@blocknote/core@0.46.2(patch_hash=3ff0414270086514addc1fa21c7cb0d730d141d1d255ac4faca126085ddf35c7)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)) '@blocknote/core': specifier: ^0.46.2 - version: 0.46.2(patch_hash=dc394d11ab419b36b8d306cbbb6ce39f0722e042b63b2b7d254c7d7f90782e94)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) + version: 0.46.2(patch_hash=3ff0414270086514addc1fa21c7cb0d730d141d1d255ac4faca126085ddf35c7)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) '@blocknote/mantine': specifier: ^0.46.2 version: 0.46.2(@floating-ui/dom@1.7.5)(@mantine/core@8.3.14(@mantine/hooks@8.3.14(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@8.3.14(react@19.2.4))(@mantine/utils@6.0.22(react@19.2.4))(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -5359,9 +5359,9 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@blocknote/code-block@0.46.2(@blocknote/core@0.46.2(patch_hash=dc394d11ab419b36b8d306cbbb6ce39f0722e042b63b2b7d254c7d7f90782e94)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0))': + '@blocknote/code-block@0.46.2(@blocknote/core@0.46.2(patch_hash=3ff0414270086514addc1fa21c7cb0d730d141d1d255ac4faca126085ddf35c7)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0))': dependencies: - '@blocknote/core': 0.46.2(patch_hash=dc394d11ab419b36b8d306cbbb6ce39f0722e042b63b2b7d254c7d7f90782e94)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) + '@blocknote/core': 0.46.2(patch_hash=3ff0414270086514addc1fa21c7cb0d730d141d1d255ac4faca126085ddf35c7)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) '@shikijs/core': 3.23.0 '@shikijs/engine-javascript': 3.23.0 '@shikijs/langs': 3.23.0 @@ -5369,7 +5369,7 @@ snapshots: '@shikijs/themes': 3.23.0 '@shikijs/types': 3.22.0 - '@blocknote/core@0.46.2(patch_hash=dc394d11ab419b36b8d306cbbb6ce39f0722e042b63b2b7d254c7d7f90782e94)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)': + '@blocknote/core@0.46.2(patch_hash=3ff0414270086514addc1fa21c7cb0d730d141d1d255ac4faca126085ddf35c7)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)': dependencies: '@emoji-mart/data': 1.2.1 '@handlewithcare/prosemirror-inputrules': 0.1.4(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6) @@ -5421,7 +5421,7 @@ snapshots: '@blocknote/mantine@0.46.2(@floating-ui/dom@1.7.5)(@mantine/core@8.3.14(@mantine/hooks@8.3.14(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@8.3.14(react@19.2.4))(@mantine/utils@6.0.22(react@19.2.4))(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@blocknote/core': 0.46.2(patch_hash=dc394d11ab419b36b8d306cbbb6ce39f0722e042b63b2b7d254c7d7f90782e94)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) + '@blocknote/core': 0.46.2(patch_hash=3ff0414270086514addc1fa21c7cb0d730d141d1d255ac4faca126085ddf35c7)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) '@blocknote/react': 0.46.2(patch_hash=e09f7011df33f4ff92c0d3fd8c9060e62f08997d076f3e438e1e82f8c1ab2f76)(@floating-ui/dom@1.7.5)(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@mantine/core': 8.3.14(@mantine/hooks@8.3.14(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@mantine/hooks': 8.3.14(react@19.2.4) @@ -5443,7 +5443,7 @@ snapshots: '@blocknote/react@0.46.2(patch_hash=e09f7011df33f4ff92c0d3fd8c9060e62f08997d076f3e438e1e82f8c1ab2f76)(@floating-ui/dom@1.7.5)(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@blocknote/core': 0.46.2(patch_hash=dc394d11ab419b36b8d306cbbb6ce39f0722e042b63b2b7d254c7d7f90782e94)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) + '@blocknote/core': 0.46.2(patch_hash=3ff0414270086514addc1fa21c7cb0d730d141d1d255ac4faca126085ddf35c7)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) '@emoji-mart/data': 1.2.1 '@floating-ui/react': 0.27.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@floating-ui/utils': 0.2.10 diff --git a/src/lib/blockNoteSuggestionMenu.regression.test.ts b/src/lib/blockNoteSuggestionMenu.regression.test.ts new file mode 100644 index 00000000..80240cf0 --- /dev/null +++ b/src/lib/blockNoteSuggestionMenu.regression.test.ts @@ -0,0 +1,96 @@ +import { SuggestionMenu } from '@blocknote/core/extensions' +import { describe, expect, it, vi } from 'vitest' + +type SuggestionPluginState = { + triggerCharacter: string + deleteTriggerCharacter: boolean + queryStartPos: () => number + query: string + decorationId: string + ignoreQueryLength?: boolean +} + +type SuggestionEditorState = Record + +type SuggestionRoot = { + addEventListener: ( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ) => void + removeEventListener: ( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, + ) => void + querySelector: (selectors: string) => Element | null +} + +type SuggestionEditorView = { + root: SuggestionRoot + state: SuggestionEditorState +} + +type SuggestionPluginView = { + update: (view: SuggestionEditorView, prevState: SuggestionEditorState) => void +} + +type SuggestionMenuPlugin = { + spec: { + key: { key: string } + view: (view: SuggestionEditorView) => SuggestionPluginView + } +} + +function createSuggestionPlugin() { + const extensionFactory = SuggestionMenu() as unknown as (context: { + editor: { isEditable: boolean } + }) => { prosemirrorPlugins: SuggestionMenuPlugin[] } + + return extensionFactory({ editor: { isEditable: true } }).prosemirrorPlugins[0] +} + +function createState( + plugin: SuggestionMenuPlugin, + pluginState?: SuggestionPluginState, +): SuggestionEditorState { + return { + [plugin.spec.key.key]: pluginState, + } +} + +function createPluginState(): SuggestionPluginState { + return { + triggerCharacter: '/', + deleteTriggerCharacter: true, + queryStartPos: () => 1, + query: '', + decorationId: 'missing-decoration', + } +} + +function createEditorView(state: SuggestionEditorState): SuggestionEditorView { + return { + root: { + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + querySelector: vi.fn(() => null), + }, + state, + } +} + +describe('patched BlockNote suggestion menu lifecycle', () => { + it('closes a suggestion menu before its decoration mounts without throwing', () => { + const plugin = createSuggestionPlugin() + const inactiveState = createState(plugin) + const activeState = createState(plugin, createPluginState()) + const editorView = createEditorView(activeState) + const pluginView = plugin.spec.view(editorView) + + expect(() => pluginView.update(editorView, inactiveState)).not.toThrow() + + editorView.state = inactiveState + expect(() => pluginView.update(editorView, activeState)).not.toThrow() + }) +})