Collection components in the tab row throw; trigger guards can't cover triggerless popovers
- Dominant language
- TypeScript
- Stars
- 15.9k
- Forks
- 1.6k
- Avg merge
- 3d 9m
- Merged PRs (30d)
- 59
Description
### Provide a general summary of the issue here
Collection components rendered alongside `TabList` inside `Tabs` throw, because they render during the `Tabs` collection's hidden pass and contribute items to the `Tabs` document instead of building their own.
#10019 addressed this by making `MenuTrigger`, `Select` and `ComboBox` hideable, and #10367 adds `DialogTrigger`. Two shapes remain that a per-trigger guard doesn't reach — and the second can't be reached by that strategy at all, because there is no trigger component to guard.
### 🤔 Expected Behavior?
Collection components in the tab row build their own collection and render normally.
### 😯 Current Behavior
**1. A `TagGroup` in the tab row** (e.g. chips for currently-applied filters, beside the tabs):
```
TypeError: Cannot destructure property 'onAction' of '_utils.listMap.get(...)' as it is undefined.
```
**2. A standalone controlled `Popover`** (`triggerRef` + `isOpen`, no trigger component) containing a `Menu`:
```
TypeError: Cannot read properties of null (reading 'isDisabled') // useMenuItem.ts:173
```
The second is the interesting one: the hideable-trigger approach can't cover it, since the popover is positioned via `triggerRef` rather than wrapped in a `MenuTrigger`/`DialogTrigger`.
Reduced further, a bare `Menu`, `ListBox` or `GridList` as a sibling of `TabList` throws for the same reason (three different internal errors). Those are minimal reductions rather than realistic usage — the two above are the ones I would actually expect people to write.
### 💁 Possible Solution
No PR, since this is a design call rather than a patch.
The per-trigger guards stop the *gateway* rendering. The underlying rule — a collection component should not contribute items to an unrelated ancestor's collection document — isn't enforced anywhere, so each new shape needs its own guard, and shapes without a trigger have nowhere to put one.
`CollectionBuilder` is where the rule could live:
```tsx
// If a document was provided above us, we're already in a hidden tree. Just render the content.
let doc = useContext(CollectionDocumentContext);
if (doc) {
return props.content as ReactElement;
}
```
Correct for a nested collection that genuinely belongs to the ancestor (submenu items joining the parent menu, `Select`'s `ListBox`); wrong for an independent collection root that merely happens to be nested. Distinguishing those — perhaps opt-in from the components that need inheritance — would cover all of these at once.
### 🔦 Context
Found while fixing `DialogTrigger` (#10367) for a tab row containing a filter popover. That PR fixes our case; this issue is only about the shapes it doesn't reach.
### 💻 Code Sample
```jsx
function inTabs(node) {
return (
First Tab
Second Tab
{node}
A
B
);
}
// 1. TagGroup in the tab row
render(inTabs(
Filter A
));
// 2. standalone controlled Popover holding a Menu
function StandalonePopover() {
let ref = useRef(null);
return (
<>
Open
Item 1
);
}
render(inTabs());
```
Control: the identical `Menu` element renders fine with no `Tabs` wrapper, and nested inside plain `div`s — the `Tabs` wrapper is the only variable.
### 🌍 Your Environment
| Software | Version(s) |
| --- | --- |
| react-aria-components | 1.19.0 / `main` (with #10367 applied) |
| Browser | n/a (reproduces in jsdom) |
| Operating System | macOS |
Contributor guide
Research direction
Start by inspecting CollectionBuilder and CollectionDocumentContext, then reproduce the TagGroup and controlled Popover cases from the issue inside Tabs. Determine how independent collection roots can avoid contributing to an ancestor collection without breaking collections that intentionally inherit it, such as submenu items and Select's ListBox. Done means these examples render normally while legitimate nested collection behavior remains intact.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100