From 16df5043869467190bc06df594a0638ff0c1eb57 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Tue, 5 May 2026 16:03:49 -0300 Subject: [PATCH] fix(custom-selection): respect disableContextMenu so right-click isn't dead (SD-2944) The contextmenu DOM handler in the custom-selection PM extension calls event.preventDefault() unconditionally to keep focus and selection visible while SuperDoc's built-in right-click menu opens. When the consumer sets disableContextMenu: true the built-in UI refuses to open, but the preventDefault still fires, so the browser's native right-click menu and any consumer-attached contextmenu listener are both suppressed. Right-click on plain text inside the editor goes dead with no menu of any kind. Short-circuits the contextmenu handler with a return false (no preventDefault, no focus-preservation transaction) when editor.options.disableContextMenu is true. Default behavior unchanged for consumers using SuperDoc's built-in menu. The mousedown-side selection preservation still runs, so a consumer rendering their own menu sees the visible selection underneath. --- .../custom-selection/custom-selection.js | 14 ++++++ .../custom-selection/custom-selection.test.js | 49 +++++++++++++++++++ 2 files changed, 63 insertions(+) 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({