Skip to content

Commit ef272a1

Browse files
committed
Improve hierarchy source selection
1 parent fc84356 commit ef272a1

8 files changed

Lines changed: 160 additions & 36 deletions

File tree

client/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
content="width=device-width, initial-scale=1.0, user-scalable=no"
88
/>
99
<meta name="color-scheme" content="light dark" />
10-
<link rel="favicon" type="image/png" href="/simdeck.png" />
10+
<link rel="icon" type="image/png" sizes="256x256" href="/simdeck.png" />
1111
<title>SimDeck</title>
1212
<script>
1313
try {

client/src/app/AppShell.tsx

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import {
9090
DEFAULT_VIEWPORT_STATE,
9191
DEBUG_VISIBLE_STORAGE_KEY,
9292
HIERARCHY_VISIBLE_STORAGE_KEY,
93+
nextAccessibilitySourcePreference,
9394
readPersistedUiState,
9495
readStoredAccessibilitySource,
9596
readStoredFlag,
@@ -912,24 +913,13 @@ export function AppShell({
912913
setAccessibilityError(
913914
roots.length === 0 ? (snapshot.fallbackReason ?? "") : "",
914915
);
915-
if (
916-
snapshot.source === "native-ax" &&
917-
availableSources.includes("nativescript") &&
918-
accessibilityPreferredSource !== "nativescript"
919-
) {
920-
setAccessibilityPreferredSource("nativescript");
921-
} else if (
922-
snapshot.source === "native-ax" &&
923-
availableSources.includes("swiftui") &&
924-
accessibilityPreferredSource !== "swiftui"
925-
) {
926-
setAccessibilityPreferredSource("swiftui");
927-
}
928-
if (
929-
accessibilityPreferredSource !== "auto" &&
930-
!availableSources.includes(accessibilityPreferredSource)
931-
) {
932-
setAccessibilityPreferredSource(snapshot.source);
916+
const nextPreferredSource = nextAccessibilitySourcePreference(
917+
accessibilityPreferredSource,
918+
snapshot.source,
919+
availableSources,
920+
);
921+
if (nextPreferredSource) {
922+
setAccessibilityPreferredSource(nextPreferredSource);
933923
}
934924
if (roots.length === 0) {
935925
setAccessibilitySelectedId("");

client/src/app/uiState.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { describe, expect, it } from "vitest";
22

33
import {
44
DEFAULT_VIEWPORT_STATE,
5+
nextAccessibilitySourcePreference,
6+
preferredRicherAccessibilitySource,
57
readStoredFlag,
68
sanitizeAccessibilitySources,
79
sanitizePersistedUiState,
@@ -23,6 +25,41 @@ describe("uiState", () => {
2325
).toEqual(["nativescript", "swiftui", "in-app-inspector", "native-ax"]);
2426
});
2527

28+
it("prefers a richer hierarchy source over native accessibility", () => {
29+
expect(
30+
preferredRicherAccessibilitySource([
31+
"native-ax",
32+
"in-app-inspector",
33+
"react-native",
34+
]),
35+
).toBe("react-native");
36+
expect(preferredRicherAccessibilitySource(["native-ax"])).toBeNull();
37+
});
38+
39+
it("moves away from native accessibility when a richer source is available", () => {
40+
expect(
41+
nextAccessibilitySourcePreference("auto", "native-ax", [
42+
"react-native",
43+
"native-ax",
44+
]),
45+
).toBe("react-native");
46+
expect(
47+
nextAccessibilitySourcePreference("native-ax", "native-ax", [
48+
"react-native",
49+
"native-ax",
50+
]),
51+
).toBeNull();
52+
});
53+
54+
it("uses a richer fallback when the selected source disappears", () => {
55+
expect(
56+
nextAccessibilitySourcePreference("swiftui", "native-ax", [
57+
"in-app-inspector",
58+
"native-ax",
59+
]),
60+
).toBe("in-app-inspector");
61+
});
62+
2663
it("sanitizes persisted viewport state and falls back to defaults", () => {
2764
const sanitized = sanitizePersistedUiState({
2865
bundleIDValue: 123 as unknown as string,

client/src/app/uiState.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,39 @@ export function sanitizeAccessibilitySources(
114114
return ACCESSIBILITY_SOURCE_ORDER.filter((source) => sourceSet.has(source));
115115
}
116116

117+
export function preferredRicherAccessibilitySource(
118+
availableSources: AccessibilitySource[],
119+
): AccessibilitySource | null {
120+
return (
121+
ACCESSIBILITY_SOURCE_ORDER.find(
122+
(source) => source !== "native-ax" && availableSources.includes(source),
123+
) ?? null
124+
);
125+
}
126+
127+
export function nextAccessibilitySourcePreference(
128+
currentPreference: AccessibilitySourcePreference,
129+
snapshotSource: AccessibilitySource,
130+
availableSources: AccessibilitySource[],
131+
): AccessibilitySourcePreference | null {
132+
const richerSource = preferredRicherAccessibilitySource(availableSources);
133+
if (
134+
snapshotSource === "native-ax" &&
135+
currentPreference !== "native-ax" &&
136+
richerSource &&
137+
currentPreference !== richerSource
138+
) {
139+
return richerSource;
140+
}
141+
if (
142+
currentPreference !== "auto" &&
143+
!availableSources.includes(currentPreference)
144+
) {
145+
return richerSource ?? snapshotSource;
146+
}
147+
return null;
148+
}
149+
117150
export function isAccessibilitySource(
118151
value: unknown,
119152
): value is AccessibilitySource {

client/src/features/accessibility/AccessibilityInspector.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,12 @@ export function AccessibilityInspector({
256256
>
257257
{sourceOptions.map((option) => (
258258
<button
259+
aria-pressed={option === source}
259260
className={`hierarchy-source-pill source-${option} ${option === source ? "active" : ""}`}
260261
disabled={option === source || !selectedSimulator?.isBooted}
261262
key={option}
262263
onClick={() => onSourceChange(option)}
263-
title={`Show ${sourceLabel(option)} hierarchy`}
264+
title={`${option === source ? "Showing" : "Show"} ${sourceLabel(option)} hierarchy`}
264265
type="button"
265266
>
266267
{sourceLabel(option)}

client/src/features/accessibility/accessibilityTree.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,42 @@ describe("findAccessibilityItemAtPoint", () => {
209209
expect(item?.node.type).toBe("Label");
210210
expect(item?.id).toBe("0.0");
211211
});
212+
213+
it("ignores module-qualified transparent UIKit tab bar containers", () => {
214+
const roots: AccessibilityNode[] = [
215+
{
216+
type: "UIWindow",
217+
frame: { x: 0, y: 0, width: 402, height: 874 },
218+
children: [
219+
{
220+
type: "Label",
221+
title: "Dashboard",
222+
frame: { x: 24, y: 160, width: 220, height: 44 },
223+
source: "in-app-inspector",
224+
},
225+
{
226+
className: "UIKit._UITabBarContainerView",
227+
source: "in-app-inspector",
228+
title: "UIKit._UITabBarContainerView",
229+
type: "UIKit._UITabBarContainerView",
230+
frame: { x: 0, y: 0, width: 402, height: 874 },
231+
children: [
232+
{
233+
className: "UIKit.UIView",
234+
source: "in-app-inspector",
235+
title: "UIKit.UIView",
236+
type: "UIKit.UIView",
237+
frame: { x: 0, y: 825, width: 402, height: 49 },
238+
},
239+
],
240+
},
241+
],
242+
},
243+
];
244+
245+
const item = findAccessibilityItemAtPoint(roots, { x: 0.12, y: 0.208 });
246+
247+
expect(item?.node.type).toBe("Label");
248+
expect(item?.id).toBe("0.0");
249+
});
212250
});

client/src/features/accessibility/accessibilityTree.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -206,26 +206,35 @@ function isTransparentHitTestBlocker(item: AccessibilityTreeItem): boolean {
206206

207207
const rawClassName = cleanText(node.className);
208208
const type = cleanText(node.type);
209-
if (!isTransparentContainerClass(rawClassName) && type !== "UIView") {
209+
if (
210+
!isTransparentContainerClass(rawClassName) &&
211+
!isTransparentContainerClass(type)
212+
) {
210213
return false;
211214
}
212215

213216
return !hasMeaningfulNodeContent(node);
214217
}
215218

216219
function isTransparentContainerClass(value: string | null): boolean {
220+
const className = unqualifiedClassName(value);
217221
return (
218-
value === "UIView" ||
219-
value === "UITransitionView" ||
220-
value === "UIDropShadowView" ||
221-
value === "UIViewControllerWrapperView" ||
222-
value === "UINavigationTransitionView" ||
223-
value === "_UITouchPassthroughView" ||
224-
value === "_UIFloatingBarContainerView" ||
225-
Boolean(value?.includes("FloatingBarHostingView"))
222+
className === "UIView" ||
223+
className === "UITransitionView" ||
224+
className === "UIDropShadowView" ||
225+
className === "UIViewControllerWrapperView" ||
226+
className === "UINavigationTransitionView" ||
227+
className === "_UITabBarContainerView" ||
228+
className === "_UITouchPassthroughView" ||
229+
className === "_UIFloatingBarContainerView" ||
230+
Boolean(className?.includes("FloatingBarHostingView"))
226231
);
227232
}
228233

234+
function unqualifiedClassName(value: string | null): string | null {
235+
return value?.split(".").pop()?.trim() || value;
236+
}
237+
229238
function hasMeaningfulNodeContent(node: AccessibilityNode): boolean {
230239
const generatedNames = new Set(
231240
[node.type, node.className, node.role, "UIView", "UIKit View"]

client/src/styles/components.css

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@
587587
align-items: center;
588588
max-width: 150px;
589589
overflow: hidden;
590+
position: relative;
590591
padding: 2px 6px;
591592
border: 1px solid var(--border-subtle);
592593
border-radius: 999px;
@@ -605,12 +606,6 @@
605606
color: var(--text);
606607
}
607608

608-
.hierarchy-source-pill.active {
609-
border-color: color-mix(in srgb, var(--accent) 58%, var(--border));
610-
background: color-mix(in srgb, var(--accent) 18%, transparent);
611-
color: var(--text);
612-
}
613-
614609
.hierarchy-source-pill.source-in-app-inspector {
615610
border-color: color-mix(in srgb, var(--success) 50%, var(--border));
616611
background: var(--success-bg);
@@ -642,7 +637,28 @@
642637
}
643638

644639
.hierarchy-source-pill.active {
645-
box-shadow: inset 0 0 0 1px currentColor;
640+
gap: 5px;
641+
padding-inline: 7px 8px;
642+
border-color: var(--accent);
643+
background: var(--accent);
644+
color: var(--accent-text);
645+
box-shadow:
646+
inset 0 0 0 1px color-mix(in srgb, var(--accent-text) 44%, transparent),
647+
0 0 0 2px color-mix(in srgb, var(--accent) 35%, transparent);
648+
opacity: 1;
649+
}
650+
651+
.hierarchy-source-pill.active::before {
652+
content: "";
653+
width: 6px;
654+
height: 6px;
655+
flex: 0 0 auto;
656+
border-radius: 999px;
657+
background: currentColor;
658+
}
659+
660+
.hierarchy-source-pill:disabled {
661+
cursor: default;
646662
}
647663

648664
.hierarchy-source-note {

0 commit comments

Comments
 (0)