@@ -158,6 +158,116 @@ describe("Shadow DOM", () => {
158158 expect ( log ) . toContain ( "end of document" ) ;
159159 } ) ;
160160
161+ it ( "should resolve aria-owns referencing an element inside shadow DOM" , async ( ) => {
162+ // Container with aria-owns pointing to an ID inside a shadow root
163+ const owner = document . createElement ( "div" ) ;
164+ owner . setAttribute ( "role" , "listbox" ) ;
165+ owner . setAttribute ( "aria-label" , "Owner" ) ;
166+ owner . setAttribute ( "aria-owns" , "shadow-option" ) ;
167+ document . body . appendChild ( owner ) ;
168+
169+ const host = document . createElement ( "div" ) ;
170+ document . body . appendChild ( host ) ;
171+
172+ const shadow = host . attachShadow ( { mode : "open" } ) ;
173+ const option = document . createElement ( "div" ) ;
174+ option . setAttribute ( "role" , "option" ) ;
175+ option . setAttribute ( "id" , "shadow-option" ) ;
176+ option . textContent = "Shadow Option" ;
177+ shadow . appendChild ( option ) ;
178+
179+ await virtual . start ( { container : document . body } ) ;
180+
181+ while ( ( await virtual . lastSpokenPhrase ( ) ) !== "end of document" ) {
182+ await virtual . next ( ) ;
183+ }
184+
185+ const log = await virtual . spokenPhraseLog ( ) ;
186+
187+ // The owned element inside shadow DOM should be found and reparented.
188+ // Listbox and option roles include extra ARIA attribute announcements.
189+ const hasListbox = log . some ( ( p ) => p . includes ( "listbox, Owner" ) ) ;
190+ const hasOption = log . some (
191+ ( p ) => p . includes ( "option, Shadow Option" ) && p . includes ( "position 1" )
192+ ) ;
193+ expect ( hasListbox ) . toBe ( true ) ;
194+ expect ( hasOption ) . toBe ( true ) ;
195+ } ) ;
196+
197+ it ( "should resolve aria-flowto referencing an element inside shadow DOM" , async ( ) => {
198+ const host = document . createElement ( "div" ) ;
199+ document . body . appendChild ( host ) ;
200+
201+ const shadow = host . attachShadow ( { mode : "open" } ) ;
202+ const target = document . createElement ( "div" ) ;
203+ target . setAttribute ( "id" , "flow-target" ) ;
204+ target . setAttribute ( "role" , "region" ) ;
205+ target . setAttribute ( "aria-label" , "Target Region" ) ;
206+ target . textContent = "Flow target content" ;
207+ shadow . appendChild ( target ) ;
208+
209+ // Source element with aria-flowto pointing into shadow DOM
210+ const source = document . createElement ( "button" ) ;
211+ source . setAttribute ( "aria-flowto" , "flow-target" ) ;
212+ source . textContent = "Source" ;
213+ document . body . appendChild ( source ) ;
214+
215+ await virtual . start ( { container : document . body } ) ;
216+
217+ while ( ( await virtual . lastSpokenPhrase ( ) ) !== "end of document" ) {
218+ await virtual . next ( ) ;
219+ }
220+
221+ const log = await virtual . spokenPhraseLog ( ) ;
222+
223+ // Both the source and the shadow DOM target should be in the tree.
224+ // aria-flowto adds "alternate reading order" annotations to spoken output.
225+ const hasSource = log . some ( ( p ) => p . includes ( "button, Source" ) ) ;
226+ const hasTarget = log . some ( ( p ) => p . includes ( "region, Target Region" ) ) ;
227+ expect ( hasSource ) . toBe ( true ) ;
228+ expect ( hasTarget ) . toBe ( true ) ;
229+ } ) ;
230+
231+ it ( "should resolve aria-owns referencing an element nested two shadow levels deep" , async ( ) => {
232+ const owner = document . createElement ( "div" ) ;
233+ owner . setAttribute ( "role" , "listbox" ) ;
234+ owner . setAttribute ( "aria-label" , "Deep Owner" ) ;
235+ owner . setAttribute ( "aria-owns" , "deep-option" ) ;
236+ document . body . appendChild ( owner ) ;
237+
238+ // Level 1: outer shadow host
239+ const outerHost = document . createElement ( "div" ) ;
240+ document . body . appendChild ( outerHost ) ;
241+ const outerShadow = outerHost . attachShadow ( { mode : "open" } ) ;
242+
243+ // Level 2: inner shadow host inside outer shadow
244+ const innerHost = document . createElement ( "div" ) ;
245+ outerShadow . appendChild ( innerHost ) ;
246+ const innerShadow = innerHost . attachShadow ( { mode : "open" } ) ;
247+
248+ // The target element is inside the innermost shadow root
249+ const option = document . createElement ( "div" ) ;
250+ option . setAttribute ( "role" , "option" ) ;
251+ option . setAttribute ( "id" , "deep-option" ) ;
252+ option . textContent = "Deep Option" ;
253+ innerShadow . appendChild ( option ) ;
254+
255+ await virtual . start ( { container : document . body } ) ;
256+
257+ while ( ( await virtual . lastSpokenPhrase ( ) ) !== "end of document" ) {
258+ await virtual . next ( ) ;
259+ }
260+
261+ const log = await virtual . spokenPhraseLog ( ) ;
262+
263+ const hasListbox = log . some ( ( p ) => p . includes ( "listbox, Deep Owner" ) ) ;
264+ const hasOption = log . some (
265+ ( p ) => p . includes ( "option, Deep Option" ) && p . includes ( "position 1" )
266+ ) ;
267+ expect ( hasListbox ) . toBe ( true ) ;
268+ expect ( hasOption ) . toBe ( true ) ;
269+ } ) ;
270+
161271 it ( "should not traverse closed shadow roots" , async ( ) => {
162272 const host = document . createElement ( "div" ) ;
163273 document . body . appendChild ( host ) ;
0 commit comments