From fd69ff3dc78a9a3c4782ee70bc718aacc15345f6 Mon Sep 17 00:00:00 2001 From: Rob Hannay Date: Sun, 26 Jul 2026 19:15:55 +0100 Subject: [PATCH 1/4] fix: DialogTrigger inside Tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collection triggers render nothing inside another collection's hidden pass so the outer collection doesn't inherit items from the inner one. #10019 did this for MenuTrigger, Select and ComboBox, but DialogTrigger was missed, so a dialog containing collection content still renders detached during an ancestor collection's scan and throws — for example a Menu inside a popover dialog, which builds its items with a null selection manager. Mirrors the MenuTrigger fix. --- packages/react-aria-components/src/Dialog.tsx | 10 +++++- .../react-aria-components/test/Tabs.test.js | 36 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/react-aria-components/src/Dialog.tsx b/packages/react-aria-components/src/Dialog.tsx index 0d78f95ef18..a321bf29f68 100644 --- a/packages/react-aria-components/src/Dialog.tsx +++ b/packages/react-aria-components/src/Dialog.tsx @@ -42,6 +42,7 @@ import React, { import {RootMenuTriggerStateContext} from './Menu'; import {TextContext} from './Text'; import {useId} from 'react-aria/useId'; +import {useIsHidden} from 'react-aria/private/collections/Hidden'; import {useMenuTriggerState} from 'react-stately/useMenuTriggerState'; import {useOverlayTrigger} from 'react-aria/useOverlayTrigger'; @@ -77,11 +78,18 @@ export const OverlayTriggerStateContext = createContext(null); let {triggerProps, overlayProps} = useOverlayTrigger({type: 'dialog'}, state, buttonRef); diff --git a/packages/react-aria-components/test/Tabs.test.js b/packages/react-aria-components/test/Tabs.test.js index d5d34e78e07..8913b012de9 100644 --- a/packages/react-aria-components/test/Tabs.test.js +++ b/packages/react-aria-components/test/Tabs.test.js @@ -20,6 +20,7 @@ import { } from '@react-spectrum/test-utils-internal'; import {Button} from '../src/Button'; import {ComboBox} from '../src/ComboBox'; +import {Dialog, DialogTrigger} from '../src/Dialog'; import {Input} from '../src/Input'; import {Label} from '../src/Label'; import {ListBox, ListBoxItem} from '../src/ListBox'; @@ -1079,4 +1080,39 @@ describe('Tabs', () => { expect(menu.getOptions()).toHaveLength(3); await menu.close(); }); + + it('supports DialogTrigger inside Tabs', async () => { + let tree = render( + +
+ + First Tab + Second Tab + + + + + + + Item 1 + Item 2 + + + + +
+ First Tab content + Second Tab content +
+ ); + + let tester = testUtilUser.createTester('Tabs', {root: tree.getByRole('tablist')}); + expect(tester.getTabs().length).toBe(2); + + let trigger = tree.getByRole('button'); + await user.click(trigger); + + let dialog = tree.getByRole('dialog'); + expect(within(dialog).getAllByRole('menuitem')).toHaveLength(2); + }); }); From 91247fbccc45a4d233990b1acd305e9847d57c47 Mon Sep 17 00:00:00 2001 From: Rob Hannay Date: Sun, 26 Jul 2026 19:27:05 +0100 Subject: [PATCH 2/4] Move the hidden check below the hooks Returning early between hooks made useRef, useOverlayTrigger and useId conditional. MenuTrigger runs all of its hooks before bailing; do the same here. --- packages/react-aria-components/src/Dialog.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/react-aria-components/src/Dialog.tsx b/packages/react-aria-components/src/Dialog.tsx index a321bf29f68..945fab68383 100644 --- a/packages/react-aria-components/src/Dialog.tsx +++ b/packages/react-aria-components/src/Dialog.tsx @@ -83,13 +83,6 @@ export function DialogTrigger(props: DialogTriggerProps): JSX.Element | null { // This is needed to handle submenus. let state = useMenuTriggerState(props); - // If within a collection (e.g. Tabs), render nothing. - // Not using createHideableComponent for this because that also creates a forwardRef. - let isHidden = useIsHidden(); - if (isHidden) { - return null; - } - let buttonRef = useRef(null); let {triggerProps, overlayProps} = useOverlayTrigger({type: 'dialog'}, state, buttonRef); @@ -102,6 +95,13 @@ export function DialogTrigger(props: DialogTriggerProps): JSX.Element | null { // oxlint-disable-next-line react/react-compiler overlayProps['aria-labelledby'] = triggerProps.id; + // If within a collection (e.g. Tabs), render nothing. + // Not using createHideableComponent for this because that also creates a forwardRef. + let isHidden = useIsHidden(); + if (isHidden) { + return null; + } + return ( Date: Mon, 27 Jul 2026 08:37:10 +1000 Subject: [PATCH 3/4] Update packages/react-aria-components/test/Tabs.test.js --- packages/react-aria-components/test/Tabs.test.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/react-aria-components/test/Tabs.test.js b/packages/react-aria-components/test/Tabs.test.js index 8913b012de9..2a7ff74c37e 100644 --- a/packages/react-aria-components/test/Tabs.test.js +++ b/packages/react-aria-components/test/Tabs.test.js @@ -1091,13 +1091,11 @@ describe('Tabs', () => { - - - - Item 1 - Item 2 - - + + + Item 1 + Item 2 + From 2f76b9651d2edefcc011e7326bd27e186c422a72 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Mon, 27 Jul 2026 08:37:51 +1000 Subject: [PATCH 4/4] Apply suggestion from @snowystinger fix lint --- packages/react-aria-components/test/Tabs.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-aria-components/test/Tabs.test.js b/packages/react-aria-components/test/Tabs.test.js index 2a7ff74c37e..087915a5570 100644 --- a/packages/react-aria-components/test/Tabs.test.js +++ b/packages/react-aria-components/test/Tabs.test.js @@ -20,7 +20,7 @@ import { } from '@react-spectrum/test-utils-internal'; import {Button} from '../src/Button'; import {ComboBox} from '../src/ComboBox'; -import {Dialog, DialogTrigger} from '../src/Dialog'; +import {DialogTrigger} from '../src/Dialog'; import {Input} from '../src/Input'; import {Label} from '../src/Label'; import {ListBox, ListBoxItem} from '../src/ListBox';