Skip to content

Support (or document) trigger="contextMenu" on collection items (GridListItem / TreeItem / Row) #10444

Description

@RobHannay

Provide a general summary of the feature here

trigger="contextMenu" (new in v1.20.0) works well for a standalone trigger, but there doesn't appear to be a supported way to use it on a collection item — a GridListItem, TreeItem, or table Row. Right-clicking a row to open the same menu its kebab button opens seems like one of the most common reasons to want a context menu, so I suspect this is either a gap or a pattern I've failed to find.

Either a supported API for it, or docs guidance on the intended approach, would help.

🤔 Expected Behavior?

Being able to attach a context menu to a collection item, so that right-click / long-press / Shift+F10 on a row opens the same menu the row's kebab button opens.

Collection items already look like valid trigger candidates: the docs say MenuTrigger "works with any pressable React Aria component", and rows are pressable —

  • useGridListItemuseSelectableItemusePress (useSelectableItem.ts:409)
  • usePress reads PressResponderContext (usePress.ts:112) and merges the remaining DOM props into its result (usePress.ts:1010)

so a row would consume MenuTrigger's onContextMenu / onKeyDown correctly if it could receive them.

😯 Current Behavior

1. MenuTrigger can't wrap a collection item. MenuTrigger must render as an ancestor of its trigger, but inside a collection it hits the useIsHidden guard and returns null during the collection-building pass, so the item never registers. Wrapping a TreeItem removes every row from the tree — the collection renders empty.

2. Wrapping the row's content in <Pressable> works, but warns once per row. The Pressable child ends up being a div with no interactive ARIA role. The row itself is role="row" (RAC's Tree is a treegrid), which isn't in Pressable's allowed-role list, so there's nothing valid to put the role on:

<Pressable> child must have an interactive ARIA role.

Adding excludeFromTabOrder just swaps it for the other warning:

<Pressable> child must be focusable. Please ensure the tabIndex prop is passed through.

Putting a fake interactive role on a div inside a treegrid row seems worse than the warning, so neither option feels right.

3. Every example uses a standalone <Button>. packages/react-aria-components/stories/Menu.stories.tsx:532, packages/@react-spectrum/s2/stories/Menu.stories.tsx:409, and packages/react-aria-components/test/Menu.test.tsx:652 / :701, plus both docs sites. As a side note, the docs example makes a non-interactive 250×150 region into a <button> purely to satisfy the pressable requirement — it's announced as a button but does nothing when pressed, which reads a little odd as the canonical example.

💁 Possible Solution

useContextMenu works today and looks like the intended escape hatch — contextMenuProps spread straight onto a TreeItem / GridListItem keeps all the platform handling, with no wrapper and no warnings. If that's the recommended pattern for collections, documenting it on the Menu page (or on GridList / Tree / Table) would settle it.

The one thing it loses versus MenuTrigger is positioning the menu at the pointer, which needs getTargetRect — that isn't part of Popover's public types, so it currently needs a @ts-expect-error. Exposing it, or otherwise providing a supported way to anchor a popover to a point, would close that gap.

🔦 Context

We have a collections sidebar built on Tree, where each row has a kebab menu (rename, share, delete, …). We wanted right-click on the row to open that same menu, keeping the kebab button as-is. The kebab stays either way — the context menu is an addition, not a replacement.

We landed on <Pressable> around the row content, which works, but it restructures the row purely to host the trigger and logs a warning per row in development, so it doesn't feel like the intended shape.

💻 Examples

Wrapping the item — collection renders empty:

// Every row disappears: MenuTrigger returns null during the collection build,
// so no TreeItem is ever registered.
<Tree aria-label="Collections">
  <MenuTrigger trigger="contextMenu">
    <TreeItem textValue="Design">
      <TreeItemContent>Design</TreeItemContent>
    </TreeItem>
    <Popover>
      <Menu>
        <MenuItem>Rename…</MenuItem>
        <MenuItem>Delete…</MenuItem>
      </Menu>
    </Popover>
  </MenuTrigger>
</Tree>

Wrapping the content — works, but warns once per row:

<TreeItem textValue="Design">
  <TreeItemContent>
    <MenuTrigger trigger="contextMenu">
      {/* warns: child must have an interactive ARIA role */}
      <Pressable><div className="row-content">Design</div></Pressable>
      <Popover>
        <Menu>
          <MenuItem>Rename…</MenuItem>
          <MenuItem>Delete…</MenuItem>
        </Menu>
      </Popover>
    </MenuTrigger>
  </TreeItemContent>
</TreeItem>

Possibly related: #7197 (pass onContextMenu in RAC Tab), #7987 (useLongPress with a Table Row — event and labelling props get filtered out), #9863 (right-click a table row containing a link).

Versions: react-aria-components@1.20.0, react-aria@3.51.0, react-stately@3.49.0, React 19.

🧢 Your Company/Team

Metaview

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions