Explorar o código

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
Malik Glossop hai 3 meses
pai
achega
bf22bf9eed

+ 38 - 36
services/web/frontend/js/features/source-editor/components/editor-context-menu.tsx

@@ -49,42 +49,44 @@ const EditorContextMenuContent: FC = memo(function EditorContextMenuContent() {
 
   return (
     <Dropdown show onToggle={onToggle}>
-      <DropdownMenu
-        ref={menuRef}
-        show
-        tabIndex={0}
-        className="dropdown-menu-unpositioned"
-        onKeyDown={event => {
-          switch (event.code) {
-            case 'Escape':
-            case 'Tab':
-              event.preventDefault()
-              closeMenu()
-              break
-          }
-        }}
-      >
-        {menuItems.map((menuItem, index) => (
-          <Fragment key={index}>
-            {menuItem.separatorAbove && <DropdownDivider />}
-            <DropdownListItem>
-              <DropdownItem
-                as="button"
-                onClick={() => menuItem.handler()}
-                disabled={menuItem.disabled}
-                trailingIcon={
-                  menuItem.shortcut ? (
-                    <span>{menuItem.shortcut}</span>
-                  ) : undefined
-                }
-              >
-                {menuItem.label}
-              </DropdownItem>
-            </DropdownListItem>
-          </Fragment>
-        ))}
-        <EditorContextMenuFeedback />
-      </DropdownMenu>
+      <div onContextMenu={event => event.preventDefault()}>
+        <DropdownMenu
+          ref={menuRef}
+          show
+          tabIndex={0}
+          className="dropdown-menu-unpositioned"
+          onKeyDown={event => {
+            switch (event.code) {
+              case 'Escape':
+              case 'Tab':
+                event.preventDefault()
+                closeMenu()
+                break
+            }
+          }}
+        >
+          {menuItems.map((menuItem, index) => (
+            <Fragment key={index}>
+              {menuItem.separatorAbove && <DropdownDivider />}
+              <DropdownListItem>
+                <DropdownItem
+                  as="button"
+                  onClick={() => menuItem.handler()}
+                  disabled={menuItem.disabled}
+                  trailingIcon={
+                    menuItem.shortcut ? (
+                      <span>{menuItem.shortcut}</span>
+                    ) : undefined
+                  }
+                >
+                  {menuItem.label}
+                </DropdownItem>
+              </DropdownListItem>
+            </Fragment>
+          ))}
+          <EditorContextMenuFeedback />
+        </DropdownMenu>
+      </div>
     </Dropdown>
   )
 })

+ 4 - 0
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 {

+ 36 - 0
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(
+        <TestContainer>
+          <EditorProviders scope={scope}>
+            <CodeMirrorEditor />
+          </EditorProviders>
+        </TestContainer>
+      )
+
+      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')
+    })
+  })
 })