@@ -4,6 +4,7 @@ import { createRenderer, isJSDOM, popupConformanceTests } from '#test-utils';
44import { expect } from 'chai' ;
55import { spy } from 'sinon' ;
66import { Combobox } from '@base-ui-components/react/combobox' ;
7+ import { Popover } from '@base-ui-components/react/popover' ;
78import { Dialog } from '@base-ui-components/react/dialog' ;
89import { Field } from '@base-ui-components/react/field' ;
910import { Form } from '@base-ui-components/react/form' ;
@@ -38,41 +39,164 @@ describe('<Combobox.Root />', () => {
3839 combobox : true ,
3940 } ) ;
4041
41- it ( 'does not focus input when closing via trigger click (input inside popup)' , async ( ) => {
42- const { user } = await render (
43- < Combobox . Root items = { [ 'One' , 'Two' , 'Three' ] } >
44- < Combobox . Trigger data-testid = "trigger" >
45- < Combobox . Value />
46- </ Combobox . Trigger >
47- < Combobox . Portal >
48- < Combobox . Positioner >
49- < Combobox . Popup aria-label = "Demo" >
50- < Combobox . Input data-testid = "input" />
51- < Combobox . List >
52- { ( item : string ) => (
53- < Combobox . Item key = { item } value = { item } >
54- { item }
55- </ Combobox . Item >
56- ) }
57- </ Combobox . List >
58- </ Combobox . Popup >
59- </ Combobox . Positioner >
60- </ Combobox . Portal >
61- </ Combobox . Root > ,
62- ) ;
42+ describe ( 'focus management' , ( ) => {
43+ it ( 'does not focus input when closing via trigger click (input inside popup)' , async ( ) => {
44+ const { user } = await render (
45+ < Combobox . Root items = { [ 'One' , 'Two' , 'Three' ] } >
46+ < Combobox . Trigger data-testid = "trigger" >
47+ < Combobox . Value />
48+ </ Combobox . Trigger >
49+ < Combobox . Portal >
50+ < Combobox . Positioner >
51+ < Combobox . Popup aria-label = "Demo" >
52+ < Combobox . Input data-testid = "input" />
53+ < Combobox . List >
54+ { ( item : string ) => (
55+ < Combobox . Item key = { item } value = { item } >
56+ { item }
57+ </ Combobox . Item >
58+ ) }
59+ </ Combobox . List >
60+ </ Combobox . Popup >
61+ </ Combobox . Positioner >
62+ </ Combobox . Portal >
63+ </ Combobox . Root > ,
64+ ) ;
6365
64- const trigger = screen . getByTestId ( 'trigger' ) ;
65- await user . click ( trigger ) ;
66+ const trigger = screen . getByTestId ( 'trigger' ) ;
67+ await user . click ( trigger ) ;
6668
67- expect ( await screen . findByRole ( 'listbox' ) ) . not . to . equal ( null ) ;
69+ expect ( await screen . findByRole ( 'listbox' ) ) . not . to . equal ( null ) ;
6870
69- const input = await waitFor ( ( ) =>
70- screen . getAllByRole ( 'combobox' ) . find ( ( element ) => element . tagName === 'INPUT' ) ,
71- ) ;
72- expect ( input ) . toHaveFocus ( ) ;
71+ const input = await waitFor ( ( ) =>
72+ screen . getAllByRole ( 'combobox' ) . find ( ( element ) => element . tagName === 'INPUT' ) ,
73+ ) ;
74+ expect ( input ) . toHaveFocus ( ) ;
75+
76+ await user . click ( trigger ) ;
77+ expect ( trigger ) . toHaveFocus ( ) ;
78+ } ) ;
79+
80+ it ( 'applies aria-hidden to outside nodes when the input renders outside the popup' , async ( ) => {
81+ await render (
82+ < div >
83+ < button data-testid = "before" > before</ button >
84+ < Combobox . Root defaultOpen >
85+ < Combobox . Input />
86+ < Combobox . Portal >
87+ < Combobox . Positioner >
88+ < Combobox . Popup >
89+ < Combobox . List >
90+ < Combobox . Item value = "apple" > Apple</ Combobox . Item >
91+ </ Combobox . List >
92+ </ Combobox . Popup >
93+ </ Combobox . Positioner >
94+ </ Combobox . Portal >
95+ </ Combobox . Root >
96+ < button data-testid = "after" > after</ button >
97+ </ div > ,
98+ ) ;
99+
100+ await waitFor ( ( ) => {
101+ expect ( screen . getByTestId ( 'before' ) . closest ( '[aria-hidden="true"]' ) ) . not . to . equal ( null ) ;
102+ expect ( screen . getByTestId ( 'after' ) . closest ( '[aria-hidden="true"]' ) ) . not . to . equal ( null ) ;
103+ } ) ;
104+ expect ( screen . getByRole ( 'listbox' ) ) . not . to . have . attribute ( 'aria-hidden' ) ;
105+ } ) ;
106+
107+ it ( 'does not apply aria-hidden to outside nodes when the input renders inside the popup' , async ( ) => {
108+ await render (
109+ < div >
110+ < button data-testid = "before" > before</ button >
111+ < Combobox . Root defaultOpen >
112+ < Combobox . Trigger > Toggle</ Combobox . Trigger >
113+ < Combobox . Portal >
114+ < Combobox . Positioner >
115+ < Combobox . Popup >
116+ < Combobox . Input />
117+ < Combobox . List >
118+ < Combobox . Item value = "apple" > Apple</ Combobox . Item >
119+ </ Combobox . List >
120+ </ Combobox . Popup >
121+ </ Combobox . Positioner >
122+ </ Combobox . Portal >
123+ </ Combobox . Root >
124+ < button data-testid = "after" > after</ button >
125+ </ div > ,
126+ ) ;
127+
128+ expect ( screen . getByTestId ( 'before' ) ) . not . to . have . attribute ( 'aria-hidden' , 'true' ) ;
129+ expect ( screen . getByTestId ( 'after' ) ) . not . to . have . attribute ( 'aria-hidden' , 'true' ) ;
130+ expect ( screen . getByRole ( 'listbox' ) ) . not . to . have . attribute ( 'aria-hidden' ) ;
131+ } ) ;
73132
74- await user . click ( trigger ) ;
75- expect ( trigger ) . toHaveFocus ( ) ;
133+ it ( 'applies aria-hidden to outside nodes when the input renders inside the popup and is modal' , async ( ) => {
134+ await render (
135+ < div >
136+ < button data-testid = "before" > before</ button >
137+ < Combobox . Root modal defaultOpen >
138+ < Combobox . Trigger > Toggle</ Combobox . Trigger >
139+ < Combobox . Portal >
140+ < Combobox . Positioner >
141+ < Combobox . Popup >
142+ < Combobox . Input />
143+ < Combobox . List >
144+ < Combobox . Item value = "apple" > Apple</ Combobox . Item >
145+ </ Combobox . List >
146+ </ Combobox . Popup >
147+ </ Combobox . Positioner >
148+ </ Combobox . Portal >
149+ </ Combobox . Root >
150+ < button data-testid = "after" > after</ button >
151+ </ div > ,
152+ ) ;
153+
154+ expect ( screen . getByTestId ( 'before' ) . closest ( '[aria-hidden="true"]' ) ) . not . to . equal ( null ) ;
155+ expect ( screen . getByTestId ( 'after' ) . closest ( '[aria-hidden="true"]' ) ) . not . to . equal ( null ) ;
156+ expect ( screen . getByRole ( 'listbox' ) ) . not . to . have . attribute ( 'aria-hidden' ) ;
157+ } ) ;
158+
159+ it ( 'closes when tabbing forward from the last tabbable element inside a popover' , async ( ) => {
160+ const { user } = await render (
161+ < React . Fragment >
162+ < Popover . Root defaultOpen >
163+ < Popover . Trigger > Toggle</ Popover . Trigger >
164+ < Popover . Portal >
165+ < Popover . Positioner >
166+ < Popover . Popup >
167+ < Combobox . Root defaultOpen >
168+ < Combobox . Input />
169+ < Combobox . Portal >
170+ < Combobox . Positioner >
171+ < Combobox . Popup data-testid = "combobox-popup" >
172+ < Combobox . List >
173+ < Combobox . Item value = "apple" > Apple</ Combobox . Item >
174+ </ Combobox . List >
175+ </ Combobox . Popup >
176+ </ Combobox . Positioner >
177+ </ Combobox . Portal >
178+ </ Combobox . Root >
179+ </ Popover . Popup >
180+ </ Popover . Positioner >
181+ </ Popover . Portal >
182+ </ Popover . Root >
183+ < button data-testid = "after" />
184+ </ React . Fragment > ,
185+ ) ;
186+
187+ await flushMicrotasks ( ) ;
188+
189+ const input = screen . getByRole ( 'combobox' ) ;
190+ expect ( screen . queryByTestId ( 'combobox-popup' ) ) . not . to . equal ( null ) ;
191+
192+ await user . click ( input ) ;
193+ await user . tab ( ) ;
194+
195+ await waitFor ( ( ) => {
196+ expect ( screen . queryByTestId ( 'combobox-popup' ) ) . to . equal ( null ) ;
197+ } ) ;
198+ expect ( screen . getByTestId ( 'after' ) ) . toHaveFocus ( ) ;
199+ } ) ;
76200 } ) ;
77201
78202 describe ( 'selection behavior' , ( ) => {
0 commit comments