Skip to content

[lexical-react] Bug Fix: the node context menu leaves the native menu alone when it has nothing to show - #9022

Closed
LeSingh1 wants to merge 1 commit into
facebook:mainfrom
LeSingh1:fix/context-menu-no-visible-items
Closed

[lexical-react] Bug Fix: the node context menu leaves the native menu alone when it has nothing to show#9022
LeSingh1 wants to merge 1 commit into
facebook:mainfrom
LeSingh1:fix/context-menu-no-visible-items

Conversation

@LeSingh1

@LeSingh1 LeSingh1 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Description

NodeContextMenuPlugin suppresses the browser's context menu and opens its own before it knows whether it has anything to put in it:

function onContextMenu(e: MouseEvent) {
  e.preventDefault();          // <- unconditional
  refs.setPositionReference({ ... });

  let visibleItems: ContextMenuType[] = [];
  if (items) {
    editor.read(() => {
      const node = $getNearestNodeFromDOMNode(e.target as Element) ?? $getRoot();
      if (node) {
        visibleItems = items!.filter(option =>
          option.$showOn ? option.$showOn(node) : true,
        );
      }
    });
  }
  ...
  setIsOpen(true);             // <- unconditional
}

$showOn is documented as the per-node predicate that decides "whether the item is shown for the right-clicked node", so it is expected that a given node hides some items — and a node that hides all of them is the natural consequence of that, not a misuse. When it happens the user gets the worst of both: no native menu, because preventDefault already ran; no visible menu, because the rendered <div> has no children; and the page cannot be scrolled, because the plugin still mounts

<FloatingOverlay lockScroll={true}>

around it. The only way out is to click somewhere to dismiss an overlay that shows nothing. Right-clicking to reach Copy, Paste, or Inspect on such a node is simply not possible.

This computes visibleItems first and returns early when it is empty, before preventDefault and before setIsOpen(true). Everything else is unchanged; the item-building code just moves below the filter it always depended on. No API or serialization change.

Test plan

New unit test packages/lexical-react/src/__tests__/unit/LexicalNodeContextMenuPlugin.test.tsx. The second case is the control — the menu must still replace the native one when it does have an item — and passes before and after.

Before

 ❯ packages/lexical-react/src/__tests__/unit/LexicalNodeContextMenuPlugin.test.tsx (2 tests | 1 failed)
   × leaves the native context menu alone when no item is shown
     AssertionError: expected true to be false // Object.is equality
   ✓ opens and replaces the native context menu when an item is shown

 Test Files  1 failed (1)
      Tests  1 failed | 1 passed (2)

After

 Test Files  1 passed (1)
      Tests  2 passed (2)

Package suite is unchanged:

$ npx vitest run packages/lexical-react
 Test Files  32 passed (32)
      Tests  184 passed (184)

… alone when it has nothing to show

## Description

`NodeContextMenuPlugin` suppresses the browser's context menu and opens its own before it knows whether it has anything to put in it:

```tsx
function onContextMenu(e: MouseEvent) {
  e.preventDefault();          // <- unconditional
  refs.setPositionReference({ ... });

  let visibleItems: ContextMenuType[] = [];
  if (items) {
    editor.read(() => {
      const node = $getNearestNodeFromDOMNode(e.target as Element) ?? $getRoot();
      if (node) {
        visibleItems = items!.filter(option =>
          option.$showOn ? option.$showOn(node) : true,
        );
      }
    });
  }
  ...
  setIsOpen(true);             // <- unconditional
}
```

`$showOn` is documented as the per-node predicate that decides "whether the item is shown for the right-clicked node", so it is expected that a given node hides some items — and a node that hides *all* of them is the natural consequence of that, not a misuse. When it happens the user gets the worst of both: no native menu, because `preventDefault` already ran; no visible menu, because the rendered `<div>` has no children; and the page cannot be scrolled, because the plugin still mounts

```tsx
<FloatingOverlay lockScroll={true}>
```

around it. The only way out is to click somewhere to dismiss an overlay that shows nothing. Right-clicking to reach Copy, Paste, or Inspect on such a node is simply not possible.

This computes `visibleItems` first and returns early when it is empty, before `preventDefault` and before `setIsOpen(true)`. Everything else is unchanged; the item-building code just moves below the filter it always depended on. No API or serialization change.

## Test plan

New unit test `packages/lexical-react/src/__tests__/unit/LexicalNodeContextMenuPlugin.test.tsx`. The second case is the control — the menu must still replace the native one when it does have an item — and passes before and after.

### Before

```
 ❯ packages/lexical-react/src/__tests__/unit/LexicalNodeContextMenuPlugin.test.tsx (2 tests | 1 failed)
   × leaves the native context menu alone when no item is shown
     AssertionError: expected true to be false // Object.is equality
   ✓ opens and replaces the native context menu when an item is shown

 Test Files  1 failed (1)
      Tests  1 failed | 1 passed (2)
```

### After

```
 Test Files  1 passed (1)
      Tests  2 passed (2)
```

Package suite is unchanged:

```
$ npx vitest run packages/lexical-react
 Test Files  32 passed (32)
      Tests  184 passed (184)
```
@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

@LeSingh1 is attempting to deploy a commit to the Meta Open Source Team on Vercel.

A member of the Team first needs to authorize it.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 9, 2026
@LeSingh1

Copy link
Copy Markdown
Contributor Author

Consolidated into #9051 with the other PRs that share this defect, per @etrepum's note on #9027 and @mayrang's on #9035. Same fix and same tests, one review.

@LeSingh1 LeSingh1 closed this Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant