11import { createElement , type AriaRole , type CSSProperties } from "react" ;
22
33import type { AccessibilityNode } from "../../api/types" ;
4+ import { mapDisplayedPointToNaturalOrientation } from "../viewport/viewportMath" ;
45import {
56 accessibilityKind ,
67 accessibilityIdentifier ,
@@ -16,13 +17,15 @@ import {
1617interface AccessibilityOverlayProps {
1718 hoveredId : string | null ;
1819 roots : AccessibilityNode [ ] ;
20+ rotationQuarterTurns ?: number ;
1921 selectedId : string ;
2022 skeletonVisible ?: boolean ;
2123}
2224
2325export function AccessibilityOverlay ( {
2426 hoveredId,
2527 roots,
28+ rotationQuarterTurns = 0 ,
2629 selectedId,
2730 skeletonVisible = false ,
2831} : AccessibilityOverlayProps ) {
@@ -61,6 +64,7 @@ export function AccessibilityOverlay({
6164 key = { item . id }
6265 node = { item . node }
6366 rootFrame = { rootFrame }
67+ rotationQuarterTurns = { rotationQuarterTurns }
6468 />
6569 ) ) }
6670 </ div >
@@ -71,15 +75,26 @@ export function AccessibilityOverlay({
7175 key = { `skeleton-${ item . id } ` }
7276 node = { item . node }
7377 rootFrame = { rootFrame }
78+ rotationQuarterTurns = { rotationQuarterTurns }
7479 variant = "skeleton"
7580 />
7681 ) )
7782 : null }
7883 { hovered ? (
79- < NodeRect node = { hovered } rootFrame = { rootFrame } variant = "hovered" />
84+ < NodeRect
85+ node = { hovered }
86+ rootFrame = { rootFrame }
87+ rotationQuarterTurns = { rotationQuarterTurns }
88+ variant = "hovered"
89+ />
8090 ) : null }
8191 { selected ? (
82- < NodeRect node = { selected } rootFrame = { rootFrame } variant = "selected" />
92+ < NodeRect
93+ node = { selected }
94+ rootFrame = { rootFrame }
95+ rotationQuarterTurns = { rotationQuarterTurns }
96+ variant = "selected"
97+ />
8398 ) : null }
8499 </ div >
85100 </ div >
@@ -107,20 +122,18 @@ function framedNode(
107122function NodeRect ( {
108123 node,
109124 rootFrame,
125+ rotationQuarterTurns,
110126 variant,
111127} : {
112128 node : AccessibilityNode ;
113129 rootFrame : { height : number ; width : number ; x : number ; y : number } ;
130+ rotationQuarterTurns : number ;
114131 variant : "hovered" | "selected" | "skeleton" ;
115132} ) {
116133 if ( ! validFrame ( node . frame ) ) {
117134 return null ;
118135 }
119136
120- const left = ( ( node . frame . x - rootFrame . x ) / rootFrame . width ) * 100 ;
121- const top = ( ( node . frame . y - rootFrame . y ) / rootFrame . height ) * 100 ;
122- const width = ( node . frame . width / rootFrame . width ) * 100 ;
123- const height = ( node . frame . height / rootFrame . height ) * 100 ;
124137 const label =
125138 variant === "skeleton"
126139 ? ""
@@ -129,12 +142,7 @@ function NodeRect({
129142 return (
130143 < div
131144 className = { `accessibility-rect ${ variant } ` }
132- style = { {
133- height : `${ height } %` ,
134- left : `${ left } %` ,
135- top : `${ top } %` ,
136- width : `${ width } %` ,
137- } }
145+ style = { frameStyle ( node . frame , rootFrame , rotationQuarterTurns ) }
138146 >
139147 { label ? < span > { label } </ span > : null }
140148 </ div >
@@ -146,11 +154,13 @@ function AccessibilityDomNode({
146154 id,
147155 node,
148156 rootFrame,
157+ rotationQuarterTurns,
149158} : {
150159 depth : number ;
151160 id : string ;
152161 node : AccessibilityNode ;
153162 rootFrame : { height : number ; width : number ; x : number ; y : number } ;
163+ rotationQuarterTurns : number ;
154164} ) {
155165 if ( ! validFrame ( node . frame ) ) {
156166 return null ;
@@ -200,19 +210,40 @@ function AccessibilityDomNode({
200210 "data-simdeck-inspector-id" : node . inspectorId || undefined ,
201211 "data-simdeck-uikit-id" : node . uikitId || undefined ,
202212 role,
203- style : frameStyle ( node . frame , rootFrame ) ,
213+ style : frameStyle ( node . frame , rootFrame , rotationQuarterTurns ) ,
204214 } ) ;
205215}
206216
217+ // Native-ax frames arrive in DISPLAY (interface) space. The overlay lives
218+ // inside the shell container that CSS-rotates by `rotationQuarterTurns`, so the
219+ // frame is converted back into the device's natural (pre-rotation) space here;
220+ // the shared shell rotation then carries the overlay and the video to the
221+ // displayed orientation together. `rotationQuarterTurns === 0` is the identity,
222+ // so portrait devices keep their existing behavior.
207223function frameStyle (
208224 frame : { height : number ; width : number ; x : number ; y : number } ,
209225 rootFrame : { height : number ; width : number ; x : number ; y : number } ,
226+ rotationQuarterTurns : number ,
210227) : CSSProperties {
228+ const start = mapDisplayedPointToNaturalOrientation (
229+ {
230+ x : ( frame . x - rootFrame . x ) / rootFrame . width ,
231+ y : ( frame . y - rootFrame . y ) / rootFrame . height ,
232+ } ,
233+ rotationQuarterTurns ,
234+ ) ;
235+ const end = mapDisplayedPointToNaturalOrientation (
236+ {
237+ x : ( frame . x + frame . width - rootFrame . x ) / rootFrame . width ,
238+ y : ( frame . y + frame . height - rootFrame . y ) / rootFrame . height ,
239+ } ,
240+ rotationQuarterTurns ,
241+ ) ;
211242 return {
212- height : `${ ( frame . height / rootFrame . height ) * 100 } %` ,
213- left : `${ ( ( frame . x - rootFrame . x ) / rootFrame . width ) * 100 } %` ,
214- top : `${ ( ( frame . y - rootFrame . y ) / rootFrame . height ) * 100 } %` ,
215- width : `${ ( frame . width / rootFrame . width ) * 100 } %` ,
243+ height : `${ Math . abs ( end . y - start . y ) * 100 } %` ,
244+ left : `${ Math . min ( start . x , end . x ) * 100 } %` ,
245+ top : `${ Math . min ( start . y , end . y ) * 100 } %` ,
246+ width : `${ Math . abs ( end . x - start . x ) * 100 } %` ,
216247 } ;
217248}
218249
0 commit comments