Skip to content

Commit 03ec76e

Browse files
committed
Enhance accessibility overlay with new styles and structure for better visibility and interaction
1 parent 1e9e011 commit 03ec76e

2 files changed

Lines changed: 154 additions & 8 deletions

File tree

client/src/features/accessibility/AccessibilityOverlay.tsx

Lines changed: 137 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import type { AriaRole, CSSProperties } from "react";
2+
13
import type { AccessibilityNode } from "../../api/types";
24
import {
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+
}

client/src/styles/components.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1667,6 +1667,23 @@
16671667
pointer-events: none;
16681668
}
16691669

1670+
.accessibility-dom-overlay,
1671+
.accessibility-visual-overlay {
1672+
position: absolute;
1673+
inset: 0;
1674+
pointer-events: none;
1675+
}
1676+
1677+
.accessibility-dom-node {
1678+
position: absolute;
1679+
min-width: 1px;
1680+
min-height: 1px;
1681+
overflow: hidden;
1682+
border: 0;
1683+
opacity: 0;
1684+
pointer-events: auto;
1685+
}
1686+
16701687
.touch-interaction-overlay {
16711688
position: absolute;
16721689
inset: 0;

0 commit comments

Comments
 (0)