@@ -26,6 +26,7 @@ import type { Box } from './domUtils';
26
26
export type AriaNode = AriaProps & {
27
27
role : AriaRole | 'fragment' | 'iframe' ;
28
28
name : string ;
29
+ ref ?: number ;
29
30
children : ( AriaNode | string ) [ ] ;
30
31
element : Element ;
31
32
box : Box ;
@@ -36,28 +37,24 @@ export type AriaNode = AriaProps & {
36
37
export type AriaSnapshot = {
37
38
root : AriaNode ;
38
39
elements : Map < number , Element > ;
39
- generation : number ;
40
- ids : Map < Element , number > ;
41
40
} ;
42
41
43
- export function generateAriaTree ( rootElement : Element , generation : number , options ?: { forAI ?: boolean } ) : AriaSnapshot {
42
+ type AriaRef = {
43
+ role : string ;
44
+ name : string ;
45
+ ref : number ;
46
+ } ;
47
+
48
+ let lastRef = 0 ;
49
+
50
+ export function generateAriaTree ( rootElement : Element , options ?: { forAI ?: boolean } ) : AriaSnapshot {
44
51
const visited = new Set < Node > ( ) ;
45
52
46
53
const snapshot : AriaSnapshot = {
47
54
root : { role : 'fragment' , name : '' , children : [ ] , element : rootElement , props : { } , box : box ( rootElement ) , receivesPointerEvents : true } ,
48
55
elements : new Map < number , Element > ( ) ,
49
- generation,
50
- ids : new Map < Element , number > ( ) ,
51
56
} ;
52
57
53
- const addElement = ( element : Element ) => {
54
- const id = snapshot . elements . size + 1 ;
55
- snapshot . elements . set ( id , element ) ;
56
- snapshot . ids . set ( element , id ) ;
57
- } ;
58
-
59
- addElement ( rootElement ) ;
60
-
61
58
const visit = ( ariaNode : AriaNode , node : Node ) => {
62
59
if ( visited . has ( node ) )
63
60
return ;
@@ -91,10 +88,12 @@ export function generateAriaTree(rootElement: Element, generation: number, optio
91
88
}
92
89
}
93
90
94
- addElement ( element ) ;
95
91
const childAriaNode = toAriaNode ( element , options ) ;
96
- if ( childAriaNode )
92
+ if ( childAriaNode ) {
93
+ if ( childAriaNode . ref )
94
+ snapshot . elements . set ( childAriaNode . ref , element ) ;
97
95
ariaNode . children . push ( childAriaNode ) ;
96
+ }
98
97
processElement ( childAriaNode || ariaNode , element , ariaChildren ) ;
99
98
} ;
100
99
@@ -150,9 +149,32 @@ export function generateAriaTree(rootElement: Element, generation: number, optio
150
149
return snapshot ;
151
150
}
152
151
152
+ function ariaRef ( element : Element , role : string , name : string , options ?: { forAI ?: boolean } ) : number | undefined {
153
+ if ( ! options ?. forAI )
154
+ return undefined ;
155
+
156
+ let ariaRef : AriaRef | undefined ;
157
+ ariaRef = ( element as any ) . _ariaRef ;
158
+ if ( ! ariaRef || ariaRef . role !== role || ariaRef . name !== name ) {
159
+ ariaRef = { role, name, ref : ++ lastRef } ;
160
+ ( element as any ) . _ariaRef = ariaRef ;
161
+ }
162
+ return ariaRef . ref ;
163
+ }
164
+
153
165
function toAriaNode ( element : Element , options ?: { forAI ?: boolean } ) : AriaNode | null {
154
- if ( element . nodeName === 'IFRAME' )
155
- return { role : 'iframe' , name : '' , children : [ ] , props : { } , element, box : box ( element ) , receivesPointerEvents : true } ;
166
+ if ( element . nodeName === 'IFRAME' ) {
167
+ return {
168
+ role : 'iframe' ,
169
+ name : '' ,
170
+ ref : ariaRef ( element , 'iframe' , '' , options ) ,
171
+ children : [ ] ,
172
+ props : { } ,
173
+ element,
174
+ box : box ( element ) ,
175
+ receivesPointerEvents : true
176
+ } ;
177
+ }
156
178
157
179
const defaultRole = options ?. forAI ? 'generic' : null ;
158
180
const role = roleUtils . getAriaRole ( element ) ?? defaultRole ;
@@ -161,7 +183,17 @@ function toAriaNode(element: Element, options?: { forAI?: boolean }): AriaNode |
161
183
162
184
const name = normalizeWhiteSpace ( roleUtils . getElementAccessibleName ( element , false ) || '' ) ;
163
185
const receivesPointerEvents = roleUtils . receivesPointerEvents ( element ) ;
164
- const result : AriaNode = { role, name, children : [ ] , props : { } , element, box : box ( element ) , receivesPointerEvents } ;
186
+
187
+ const result : AriaNode = {
188
+ role,
189
+ name,
190
+ ref : ariaRef ( element , role , name , options ) ,
191
+ children : [ ] ,
192
+ props : { } ,
193
+ element,
194
+ box : box ( element ) ,
195
+ receivesPointerEvents
196
+ } ;
165
197
166
198
if ( roleUtils . kAriaCheckedRoles . includes ( role ) )
167
199
result . checked = roleUtils . getAriaChecked ( element ) ;
@@ -266,7 +298,7 @@ export type MatcherReceived = {
266
298
} ;
267
299
268
300
export function matchesAriaTree ( rootElement : Element , template : AriaTemplateNode ) : { matches : AriaNode [ ] , received : MatcherReceived } {
269
- const snapshot = generateAriaTree ( rootElement , 0 ) ;
301
+ const snapshot = generateAriaTree ( rootElement ) ;
270
302
const matches = matchesNodeDeep ( snapshot . root , template , false , false ) ;
271
303
return {
272
304
matches,
@@ -278,7 +310,7 @@ export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode
278
310
}
279
311
280
312
export function getAllByAria ( rootElement : Element , template : AriaTemplateNode ) : Element [ ] {
281
- const root = generateAriaTree ( rootElement , 0 ) . root ;
313
+ const root = generateAriaTree ( rootElement ) . root ;
282
314
const matches = matchesNodeDeep ( root , template , true , false ) ;
283
315
return matches . map ( n => n . element ) ;
284
316
}
@@ -408,10 +440,10 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, options?: { mode?: 'r
408
440
if ( ariaNode . selected === true )
409
441
key += ` [selected]` ;
410
442
if ( options ?. forAI && receivesPointerEvents ( ariaNode ) ) {
411
- const id = ariaSnapshot . ids . get ( ariaNode . element ) ;
443
+ const ref = ariaNode . ref ;
412
444
const cursor = hasPointerCursor ( ariaNode ) ? ' [cursor=pointer]' : '' ;
413
- if ( id )
414
- key += ` [ref=s ${ ariaSnapshot . generation } e${ id } ]${ cursor } ` ;
445
+ if ( ref )
446
+ key += ` [ref=e${ ref } ]${ cursor } ` ;
415
447
}
416
448
417
449
const escapedKey = indent + '- ' + yamlEscapeKeyIfNeeded ( key ) ;
0 commit comments