Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading