From bf22bf9eed49a4c1d834d01100ceb7506112c984 Mon Sep 17 00:00:00 2001 From: Malik Glossop Date: Tue, 16 Jun 2026 11:33:35 +0200 Subject: [PATCH] Merge pull request #34487 from overleaf/mg-fix-nested-context-menu Block native context menu on editor context menu and fix focus styling GitOrigin-RevId: c2d59e7acc59019b5e2777cc1bf3ad24a7d8e54e --- .../components/editor-context-menu.tsx | 74 ++++++++++--------- .../stylesheets/components/dropdown-menu.scss | 4 + .../codemirror-editor-context-menu.spec.tsx | 36 +++++++++ 3 files changed, 78 insertions(+), 36 deletions(-) diff --git a/services/web/frontend/js/features/source-editor/components/editor-context-menu.tsx b/services/web/frontend/js/features/source-editor/components/editor-context-menu.tsx index 11ff9d6a88..ce6b5e3f4c 100644 --- a/services/web/frontend/js/features/source-editor/components/editor-context-menu.tsx +++ b/services/web/frontend/js/features/source-editor/components/editor-context-menu.tsx @@ -49,42 +49,44 @@ const EditorContextMenuContent: FC = memo(function EditorContextMenuContent() { return ( - { - switch (event.code) { - case 'Escape': - case 'Tab': - event.preventDefault() - closeMenu() - break - } - }} - > - {menuItems.map((menuItem, index) => ( - - {menuItem.separatorAbove && } - - menuItem.handler()} - disabled={menuItem.disabled} - trailingIcon={ - menuItem.shortcut ? ( - {menuItem.shortcut} - ) : undefined - } - > - {menuItem.label} - - - - ))} - - +
event.preventDefault()}> + { + switch (event.code) { + case 'Escape': + case 'Tab': + event.preventDefault() + closeMenu() + break + } + }} + > + {menuItems.map((menuItem, index) => ( + + {menuItem.separatorAbove && } + + menuItem.handler()} + disabled={menuItem.disabled} + trailingIcon={ + menuItem.shortcut ? ( + {menuItem.shortcut} + ) : undefined + } + > + {menuItem.label} + + + + ))} + + +
) }) diff --git a/services/web/frontend/stylesheets/components/dropdown-menu.scss b/services/web/frontend/stylesheets/components/dropdown-menu.scss index 725ddf7561..ef3da5c97c 100644 --- a/services/web/frontend/stylesheets/components/dropdown-menu.scss +++ b/services/web/frontend/stylesheets/components/dropdown-menu.scss @@ -133,6 +133,10 @@ $dropdown-item-min-height: 36px; &:focus { background: none; + + // Bootstrap sets the hover text colour on plain :focus too; reset it so a + // mouse-focused (not :focus-visible), non-hovered item stays readable. + color: var(--dropdown-text-color); } &:focus-visible { diff --git a/services/web/test/frontend/features/source-editor/components/codemirror-editor-context-menu.spec.tsx b/services/web/test/frontend/features/source-editor/components/codemirror-editor-context-menu.spec.tsx index 372f1ac202..22ec6e1a99 100644 --- a/services/web/test/frontend/features/source-editor/components/codemirror-editor-context-menu.spec.tsx +++ b/services/web/test/frontend/features/source-editor/components/codemirror-editor-context-menu.spec.tsx @@ -1288,4 +1288,40 @@ describe('editor context menu', { scrollBehavior: false }, function () { cy.findByRole('menu').should('not.exist') }) }) + + describe('when right-clicking inside the open menu', function () { + it('should suppress the native menu and stay open', function () { + const scope = mockScope() + + cy.mount( + + + + + + ) + + cy.get('.cm-line').eq(10).rightclick() + cy.findByRole('menu').should('be.visible') + + cy.findByRole('menu').within(() => { + cy.findAllByRole('menuitem') + .first() + .then($item => { + const event = new MouseEvent('contextmenu', { + bubbles: true, + cancelable: true, + }) + // dispatchEvent returns false when preventDefault was called + const notPrevented = $item[0].dispatchEvent(event) + expect(notPrevented, 'native context menu prevented').to.equal( + false + ) + }) + }) + + // The second contextmenu event keeps the custom menu open + cy.findByRole('menu').should('be.visible') + }) + }) })