adobe / adobe/react-spectrum

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

Open
#10,444 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
15.9k
Forks
1.6k
Avg merge
3d 9m
Merged PRs (30d)
59

Description

### 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 —

- `useGridListItem` → `useSelectableItem` → `usePress` (`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 `` 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:

```
child must have an interactive ARIA role.
```

Adding `excludeFromTabOrder` just swaps it for the other warning:

```
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 ``.** `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 `` 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 `` 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:

```tsx
// Every row disappears: MenuTrigger returns null during the collection build,
// so no TreeItem is ever registered.



Design



Rename…
Delete…


```

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

```tsx



{/* warns: child must have an interactive ARIA role */}

Design



Rename…
Delete…



```

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

Contributor guide

Open the contributing guide

Research direction

Start with useContextMenu and MenuTrigger, then compare collection-item event handling in useSelectableItem.ts:409 and usePress.ts:112/1010. Inspect the Menu stories and tests named in the issue to determine whether the supported outcome is documentation or an API change. Done means a collection row has a supported, warning-free context-menu path with documented behavior and coverage.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
accessibility, frontend
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.