1+ import type { AriaRole , CSSProperties } from "react" ;
2+
13import type { AccessibilityNode } from "../../api/types" ;
24import {
35 accessibilityKind ,
6+ accessibilityIdentifier ,
47 accessibilityRootFrame ,
58 buildAccessibilityTree ,
69 findAccessibilityItem ,
10+ flattenAccessibilityTree ,
711 primaryAccessibilityText ,
812 validFrame ,
913} from "./accessibilityTree" ;
@@ -21,6 +25,11 @@ export function AccessibilityOverlay({
2125} : AccessibilityOverlayProps ) {
2226 const rootFrame = accessibilityRootFrame ( roots ) ;
2327 const tree = buildAccessibilityTree ( roots ) ;
28+ const overlayItems = rootFrame
29+ ? flattenAccessibilityTree ( tree ) . filter ( ( item ) =>
30+ validFrame ( item . node . frame ) ,
31+ )
32+ : [ ] ;
2433 const selected = selectedId
2534 ? framedNode ( findAccessibilityItem ( tree , selectedId ) ?. node )
2635 : null ;
@@ -29,18 +38,37 @@ export function AccessibilityOverlay({
2938 ? framedNode ( findAccessibilityItem ( tree , hoveredId ) ?. node )
3039 : null ;
3140
32- if ( ! rootFrame || ( ! selected && ! hovered ) ) {
41+ if ( ! rootFrame ) {
42+ return null ;
43+ }
44+ if ( overlayItems . length === 0 && ! selected && ! hovered ) {
3345 return null ;
3446 }
3547
3648 return (
37- < div className = "accessibility-overlay" aria-hidden = "true" >
38- { hovered ? (
39- < NodeRect node = { hovered } rootFrame = { rootFrame } variant = "hovered" />
40- ) : null }
41- { selected ? (
42- < NodeRect node = { selected } rootFrame = { rootFrame } variant = "selected" />
43- ) : null }
49+ < div
50+ aria-label = "Simulator accessibility overlay"
51+ className = "accessibility-overlay"
52+ >
53+ < div className = "accessibility-dom-overlay" >
54+ { overlayItems . map ( ( item ) => (
55+ < AccessibilityDomNode
56+ depth = { item . depth }
57+ id = { item . id }
58+ key = { item . id }
59+ node = { item . node }
60+ rootFrame = { rootFrame }
61+ />
62+ ) ) }
63+ </ div >
64+ < div className = "accessibility-visual-overlay" aria-hidden = "true" >
65+ { hovered ? (
66+ < NodeRect node = { hovered } rootFrame = { rootFrame } variant = "hovered" />
67+ ) : null }
68+ { selected ? (
69+ < NodeRect node = { selected } rootFrame = { rootFrame } variant = "selected" />
70+ ) : null }
71+ </ div >
4472 </ div >
4573 ) ;
4674}
@@ -96,3 +124,104 @@ function NodeRect({
96124 </ div >
97125 ) ;
98126}
127+
128+ function AccessibilityDomNode ( {
129+ depth,
130+ id,
131+ node,
132+ rootFrame,
133+ } : {
134+ depth : number ;
135+ id : string ;
136+ node : AccessibilityNode ;
137+ rootFrame : { height : number ; width : number ; x : number ; y : number } ;
138+ } ) {
139+ if ( ! validFrame ( node . frame ) ) {
140+ return null ;
141+ }
142+
143+ const label = accessibilityDomLabel ( node ) ;
144+ const kind = accessibilityKind ( node ) ;
145+ const role = accessibilityDomRole ( kind ) ;
146+
147+ return (
148+ < div
149+ aria-checked = {
150+ role === "checkbox" || role === "switch"
151+ ? ( node . checked ?? undefined )
152+ : undefined
153+ }
154+ aria-disabled = { node . enabled === false ? true : undefined }
155+ aria-label = { label }
156+ aria-level = { depth + 1 }
157+ aria-selected = { node . selected ?? undefined }
158+ className = "accessibility-dom-node"
159+ data-simdeck-accessibility-id = { id }
160+ data-simdeck-accessibility-identifier = {
161+ accessibilityIdentifier ( node ) || undefined
162+ }
163+ data-simdeck-accessibility-kind = { kind }
164+ data-simdeck-accessibility-source = { node . source || undefined }
165+ role = { role }
166+ style = { frameStyle ( node . frame , rootFrame ) }
167+ title = { label }
168+ />
169+ ) ;
170+ }
171+
172+ function frameStyle (
173+ frame : { height : number ; width : number ; x : number ; y : number } ,
174+ rootFrame : { height : number ; width : number ; x : number ; y : number } ,
175+ ) : CSSProperties {
176+ return {
177+ height : `${ ( frame . height / rootFrame . height ) * 100 } %` ,
178+ left : `${ ( ( frame . x - rootFrame . x ) / rootFrame . width ) * 100 } %` ,
179+ top : `${ ( ( frame . y - rootFrame . y ) / rootFrame . height ) * 100 } %` ,
180+ width : `${ ( frame . width / rootFrame . width ) * 100 } %` ,
181+ } ;
182+ }
183+
184+ function accessibilityDomLabel ( node : AccessibilityNode ) : string {
185+ const text = primaryAccessibilityText ( node ) ;
186+ const identifier = accessibilityIdentifier ( node ) ;
187+ const kind = accessibilityKind ( node ) ;
188+ if ( text && identifier && text !== identifier ) {
189+ return `${ kind } : ${ text } (${ identifier } )` ;
190+ }
191+ return text || identifier || kind ;
192+ }
193+
194+ function accessibilityDomRole ( kind : string ) : AriaRole {
195+ const normalized = kind . toLowerCase ( ) ;
196+ if ( normalized . includes ( "button" ) ) {
197+ return "button" ;
198+ }
199+ if ( normalized . includes ( "checkbox" ) ) {
200+ return "checkbox" ;
201+ }
202+ if ( normalized . includes ( "switch" ) ) {
203+ return "switch" ;
204+ }
205+ if (
206+ normalized . includes ( "textfield" ) ||
207+ normalized . includes ( "text field" ) ||
208+ normalized . includes ( "textbox" ) ||
209+ normalized . includes ( "searchfield" )
210+ ) {
211+ return "textbox" ;
212+ }
213+ if ( normalized . includes ( "slider" ) ) {
214+ return "slider" ;
215+ }
216+ if ( normalized . includes ( "image" ) || normalized . includes ( "icon" ) ) {
217+ return "img" ;
218+ }
219+ if (
220+ normalized . includes ( "text" ) ||
221+ normalized . includes ( "label" ) ||
222+ normalized . includes ( "static" )
223+ ) {
224+ return "text" ;
225+ }
226+ return "group" ;
227+ }
0 commit comments