diff --git a/packages/super-editor/src/editors/v1/components/context-menu/ContextMenu.vue b/packages/super-editor/src/editors/v1/components/context-menu/ContextMenu.vue index c7b5daef28..fec0c34e1e 100644 --- a/packages/super-editor/src/editors/v1/components/context-menu/ContextMenu.vue +++ b/packages/super-editor/src/editors/v1/components/context-menu/ContextMenu.vue @@ -4,7 +4,7 @@ import { Selection } from 'prosemirror-state'; import { isCellSelection } from '@extensions/table/tableHelpers/isCellSelection.js'; import { ContextMenuPluginKey } from '../../extensions/context-menu/context-menu.js'; import { getPropsByItemId, resolveContextMenuCommandEditor } from './utils.js'; -import { shouldBypassContextMenu } from '../../utils/contextmenu-helpers.js'; +import { shouldBypassContextMenu, isWithinResizeOverlay } from '../../utils/contextmenu-helpers.js'; import { moveCursorToMouseEvent } from '../cursor-helpers.js'; import { getEditorSurfaceElement } from '../../core/helpers/editorSurface.js'; import { getItems } from './menuItems.js'; @@ -367,6 +367,13 @@ const isEventWithinContextMenuTargets = (event) => { return false; } + // The table/image resize overlays render as siblings of the editor surface, not inside it. + // Right-clicks landing on their handles must still open the editor context menu instead of + // falling through to the browser's native menu. + if (isWithinResizeOverlay(target)) { + return true; + } + return getContextMenuTargets().some( (surface) => surface === target || (typeof surface?.contains === 'function' && surface.contains(target)), ); diff --git a/packages/super-editor/src/editors/v1/components/context-menu/tests/contextmenu-helpers.test.js b/packages/super-editor/src/editors/v1/components/context-menu/tests/contextmenu-helpers.test.js index 557d1f686d..507e627c04 100644 --- a/packages/super-editor/src/editors/v1/components/context-menu/tests/contextmenu-helpers.test.js +++ b/packages/super-editor/src/editors/v1/components/context-menu/tests/contextmenu-helpers.test.js @@ -1,5 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { shouldBypassContextMenu, shouldAllowNativeContextMenu } from '../../../utils/contextmenu-helpers.js'; +import { + shouldBypassContextMenu, + shouldAllowNativeContextMenu, + isWithinResizeOverlay, +} from '../../../utils/contextmenu-helpers.js'; describe('context menu helpers', () => { it('returns false for standard right click', () => { @@ -47,3 +51,35 @@ describe('context menu helpers', () => { expect(shouldAllowNativeContextMenu(event)).toBe(true); }); }); + +describe('isWithinResizeOverlay', () => { + it('returns true for a target inside the table resize overlay', () => { + const overlay = document.createElement('div'); + overlay.className = 'superdoc-table-resize-overlay'; + const handle = document.createElement('div'); + handle.className = 'resize-handle'; + overlay.appendChild(handle); + + expect(isWithinResizeOverlay(handle)).toBe(true); + expect(isWithinResizeOverlay(overlay)).toBe(true); + }); + + it('returns true for a target inside the image resize overlay', () => { + const overlay = document.createElement('div'); + overlay.className = 'superdoc-image-resize-overlay'; + const handle = document.createElement('div'); + overlay.appendChild(handle); + + expect(isWithinResizeOverlay(handle)).toBe(true); + }); + + it('returns false for editor content and non-element targets', () => { + const paragraph = document.createElement('p'); + paragraph.textContent = 'hello'; + + expect(isWithinResizeOverlay(paragraph)).toBe(false); + expect(isWithinResizeOverlay(document.createTextNode('hello'))).toBe(false); + expect(isWithinResizeOverlay(null)).toBe(false); + expect(isWithinResizeOverlay(undefined)).toBe(false); + }); +}); diff --git a/packages/super-editor/src/editors/v1/utils/contextmenu-helpers.js b/packages/super-editor/src/editors/v1/utils/contextmenu-helpers.js index 0bd8f54b08..108c598121 100644 --- a/packages/super-editor/src/editors/v1/utils/contextmenu-helpers.js +++ b/packages/super-editor/src/editors/v1/utils/contextmenu-helpers.js @@ -36,3 +36,18 @@ export { shouldAllowNativeContextMenu }; export const shouldBypassContextMenu = shouldAllowNativeContextMenu; export const shouldUseNativeContextMenu = shouldAllowNativeContextMenu; + +/** + * The table and image resize overlays render as siblings of the editor surface rather than + * inside it. A right-click that lands on one of their handles therefore falls outside the + * normal context-menu target surfaces, which would let the browser's native menu appear. + * This identifies those overlay targets so the editor can still own the context menu. + * @param {EventTarget | null | undefined} target + * @returns {boolean} + */ +export const isWithinResizeOverlay = (target) => { + return ( + typeof target?.closest === 'function' && + Boolean(target.closest('.superdoc-table-resize-overlay, .superdoc-image-resize-overlay')) + ); +};