diff --git a/packages/super-editor/src/editors/v1/extensions/custom-selection/custom-selection.js b/packages/super-editor/src/editors/v1/extensions/custom-selection/custom-selection.js index f2edef3fda..2b6515bb9c 100644 --- a/packages/super-editor/src/editors/v1/extensions/custom-selection/custom-selection.js +++ b/packages/super-editor/src/editors/v1/extensions/custom-selection/custom-selection.js @@ -198,6 +198,20 @@ export const CustomSelection = Extension.create({ return false; } + // SD-2944: when the consumer has turned off SuperDoc's + // built-in right-click menu (`disableContextMenu: true`), + // let the browser native menu (or the consumer's own + // `contextmenu` listener) take over. Without this guard, + // `preventDefault()` below would suppress every right-click + // even though the built-in menu UI also refuses to open, + // leaving right-click dead on plain text inside the editor. + // The mousedown-side selection preservation still runs, so + // a consumer rendering their own menu still sees the + // visible selection underneath. + if (editor.options?.disableContextMenu) { + return false; + } + // Prevent context menu from removing focus/selection event.preventDefault(); const { selection } = view.state; diff --git a/packages/super-editor/src/editors/v1/extensions/custom-selection/custom-selection.test.js b/packages/super-editor/src/editors/v1/extensions/custom-selection/custom-selection.test.js index 0f6d0514bc..56a3fc3cea 100644 --- a/packages/super-editor/src/editors/v1/extensions/custom-selection/custom-selection.test.js +++ b/packages/super-editor/src/editors/v1/extensions/custom-selection/custom-selection.test.js @@ -132,6 +132,55 @@ describe('CustomSelection plugin', () => { expect(handled).toBe(false); expect(event.preventDefault).toHaveBeenCalled(); expect(view.dispatch).toHaveBeenCalled(); + }); + + // SD-2944: when the consumer turns off SuperDoc's built-in + // right-click menu, the editor must NOT call `preventDefault` on + // contextmenu. Otherwise both the built-in UI (which is now off) + // and the browser's native menu are suppressed and right-click on + // plain text is dead. The mousedown-side selection preservation + // still runs so a consumer rendering their own menu sees the + // visible selection underneath. + it('does not call preventDefault when disableContextMenu is true (lets the browser/consumer menu through)', () => { + const { editor, plugin, view } = createEnvironment(); + editor.options.disableContextMenu = true; + + const event = { + preventDefault: vi.fn(), + detail: 0, + button: 2, + clientX: 120, + clientY: 140, + type: 'contextmenu', + }; + + const handled = plugin.props.handleDOMEvents.contextmenu(view, event); + + expect(handled).toBe(false); + expect(event.preventDefault).not.toHaveBeenCalled(); + // No selection-preservation transaction either: the built-in menu + // is the only consumer of that path, and skipping the dispatch + // keeps this branch a true pass-through. + expect(view.dispatch).not.toHaveBeenCalled(); + }); + + it('still preserves selection (calls preventDefault) when disableContextMenu is false', () => { + const { editor, plugin, view } = createEnvironment(); + editor.options.disableContextMenu = false; + + const event = { + preventDefault: vi.fn(), + detail: 0, + button: 2, + clientX: 120, + clientY: 140, + type: 'contextmenu', + }; + + plugin.props.handleDOMEvents.contextmenu(view, event); + + expect(event.preventDefault).toHaveBeenCalled(); + expect(view.dispatch).toHaveBeenCalled(); const dispatchedTr = view.dispatch.mock.calls[0][0]; expect(dispatchedTr.getMeta(CustomSelectionPluginKey)).toMatchObject({