Skip to content

Commit 74c9430

Browse files
authored
fix(custom-selection): respect disableContextMenu so right-click isn't dead (SD-2944) (#3171)
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.
1 parent 97e3526 commit 74c9430

2 files changed

Lines changed: 63 additions & 0 deletions

File tree

packages/super-editor/src/editors/v1/extensions/custom-selection/custom-selection.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,20 @@ export const CustomSelection = Extension.create({
198198
return false;
199199
}
200200

201+
// SD-2944: when the consumer has turned off SuperDoc's
202+
// built-in right-click menu (`disableContextMenu: true`),
203+
// let the browser native menu (or the consumer's own
204+
// `contextmenu` listener) take over. Without this guard,
205+
// `preventDefault()` below would suppress every right-click
206+
// even though the built-in menu UI also refuses to open,
207+
// leaving right-click dead on plain text inside the editor.
208+
// The mousedown-side selection preservation still runs, so
209+
// a consumer rendering their own menu still sees the
210+
// visible selection underneath.
211+
if (editor.options?.disableContextMenu) {
212+
return false;
213+
}
214+
201215
// Prevent context menu from removing focus/selection
202216
event.preventDefault();
203217
const { selection } = view.state;

packages/super-editor/src/editors/v1/extensions/custom-selection/custom-selection.test.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,55 @@ describe('CustomSelection plugin', () => {
132132
expect(handled).toBe(false);
133133
expect(event.preventDefault).toHaveBeenCalled();
134134
expect(view.dispatch).toHaveBeenCalled();
135+
});
136+
137+
// SD-2944: when the consumer turns off SuperDoc's built-in
138+
// right-click menu, the editor must NOT call `preventDefault` on
139+
// contextmenu. Otherwise both the built-in UI (which is now off)
140+
// and the browser's native menu are suppressed and right-click on
141+
// plain text is dead. The mousedown-side selection preservation
142+
// still runs so a consumer rendering their own menu sees the
143+
// visible selection underneath.
144+
it('does not call preventDefault when disableContextMenu is true (lets the browser/consumer menu through)', () => {
145+
const { editor, plugin, view } = createEnvironment();
146+
editor.options.disableContextMenu = true;
147+
148+
const event = {
149+
preventDefault: vi.fn(),
150+
detail: 0,
151+
button: 2,
152+
clientX: 120,
153+
clientY: 140,
154+
type: 'contextmenu',
155+
};
156+
157+
const handled = plugin.props.handleDOMEvents.contextmenu(view, event);
158+
159+
expect(handled).toBe(false);
160+
expect(event.preventDefault).not.toHaveBeenCalled();
161+
// No selection-preservation transaction either: the built-in menu
162+
// is the only consumer of that path, and skipping the dispatch
163+
// keeps this branch a true pass-through.
164+
expect(view.dispatch).not.toHaveBeenCalled();
165+
});
166+
167+
it('still preserves selection (calls preventDefault) when disableContextMenu is false', () => {
168+
const { editor, plugin, view } = createEnvironment();
169+
editor.options.disableContextMenu = false;
170+
171+
const event = {
172+
preventDefault: vi.fn(),
173+
detail: 0,
174+
button: 2,
175+
clientX: 120,
176+
clientY: 140,
177+
type: 'contextmenu',
178+
};
179+
180+
plugin.props.handleDOMEvents.contextmenu(view, event);
181+
182+
expect(event.preventDefault).toHaveBeenCalled();
183+
expect(view.dispatch).toHaveBeenCalled();
135184

136185
const dispatchedTr = view.dispatch.mock.calls[0][0];
137186
expect(dispatchedTr.getMeta(CustomSelectionPluginKey)).toMatchObject({

0 commit comments

Comments
 (0)