Skip to content

Commit 3bc7e3a

Browse files
committed
Compact React Native hierarchy wrapper rows
1 parent a3482fe commit 3bc7e3a

4 files changed

Lines changed: 191 additions & 3 deletions

File tree

client/src/features/accessibility/AccessibilityInspector.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ export function AccessibilityInspector({
289289
const label = hierarchyNodeLabel(item.node, kind);
290290
const sourceBadge = sourceLocationBadgeText(item.node);
291291
const chainBadge = compactedChainBadgeText(item.chain.length);
292+
const chainPath = compactedChainPathText(item);
292293
return (
293294
<div
294295
className={`hierarchy-node ${item.id === selectedItem?.id ? "selected" : ""}`}
@@ -329,11 +330,19 @@ export function AccessibilityInspector({
329330
{chainBadge ? (
330331
<span
331332
className="hierarchy-node-chain"
332-
title={`${item.chain.length} compacted wrapper nodes`}
333+
title={compactedChainTitle(item)}
333334
>
334335
{chainBadge}
335336
</span>
336337
) : null}
338+
{chainPath ? (
339+
<span
340+
className="hierarchy-node-chain-path"
341+
title={compactedChainTitle(item)}
342+
>
343+
{chainPath}
344+
</span>
345+
) : null}
337346
<span className="hierarchy-node-kind">{kind}</span>
338347
{label ? (
339348
<span className="hierarchy-node-text">{label}</span>
@@ -604,6 +613,29 @@ function compactedChainBadgeText(count: number): string {
604613
return count > 0 ? `+${count}` : "";
605614
}
606615

616+
function compactedChainPathText(
617+
item: ReturnType<typeof buildAccessibilityTree>[number],
618+
): string {
619+
if (item.chain.length === 0) {
620+
return "";
621+
}
622+
const names = item.chain.map(accessibilityKind).filter(Boolean);
623+
const tail = names.slice(-3);
624+
const prefix = names.length > tail.length ? "... / " : "";
625+
return `${prefix}${tail.join(" / ")} /`;
626+
}
627+
628+
function compactedChainTitle(
629+
item: ReturnType<typeof buildAccessibilityTree>[number],
630+
): string {
631+
if (item.chain.length === 0) {
632+
return "";
633+
}
634+
return [...item.chain.map(accessibilityKind), accessibilityKind(item.node)]
635+
.filter(Boolean)
636+
.join(" / ");
637+
}
638+
607639
function primarySourceLocation(
608640
node: AccessibilityNode,
609641
): AccessibilityNode["sourceLocation"] {

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

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,120 @@
11
import { describe, expect, it } from "vitest";
22

33
import type { AccessibilityNode } from "../../api/types";
4-
import { findAccessibilityItemAtPoint } from "./accessibilityTree";
4+
import {
5+
buildAccessibilityTree,
6+
findAccessibilityItemAtPoint,
7+
} from "./accessibilityTree";
8+
9+
describe("buildAccessibilityTree", () => {
10+
it("compacts framed React Native wrapper chains until meaningful children", () => {
11+
const roots: AccessibilityNode[] = [
12+
{
13+
source: "react-native",
14+
type: "RCTScrollContentView",
15+
title: "RCTScrollContentView",
16+
frame: { x: 0, y: 0, width: 400, height: 800 },
17+
children: [
18+
{
19+
source: "react-native",
20+
type: "RCTScrollView",
21+
title: "RCTScrollView",
22+
frame: { x: 0, y: 0, width: 400, height: 800 },
23+
children: [
24+
{
25+
source: "react-native",
26+
type: "ScrollViewContext",
27+
frame: { x: 0, y: 0, width: 400, height: 800 },
28+
children: [
29+
{ source: "react-native", type: "Text", title: "Today" },
30+
{ source: "react-native", type: "Text", title: "7d" },
31+
],
32+
},
33+
],
34+
},
35+
],
36+
},
37+
];
38+
39+
const tree = buildAccessibilityTree(roots);
40+
41+
expect(tree[0].node.type).toBe("ScrollViewContext");
42+
expect(tree[0].chain.map((node) => node.type)).toEqual([
43+
"RCTScrollContentView",
44+
"RCTScrollView",
45+
]);
46+
expect(tree[0].children).toHaveLength(2);
47+
});
48+
49+
it("keeps React Native source-location nodes visible", () => {
50+
const roots: AccessibilityNode[] = [
51+
{
52+
source: "react-native",
53+
type: "View",
54+
children: [
55+
{
56+
source: "react-native",
57+
type: "RangeAndFilterBar",
58+
sourceLocation: {
59+
file: "/app/src/components/RangeAndFilterBar.tsx",
60+
},
61+
children: [{ source: "react-native", type: "RCTView" }],
62+
},
63+
],
64+
},
65+
];
66+
67+
const tree = buildAccessibilityTree(roots);
68+
69+
expect(tree[0].node.type).toBe("RangeAndFilterBar");
70+
expect(tree[0].chain.map((node) => node.type)).toEqual(["View"]);
71+
});
72+
73+
it("keeps Expo route display names visible", () => {
74+
const roots: AccessibilityNode[] = [
75+
{
76+
source: "react-native",
77+
type: "RCTView",
78+
children: [
79+
{
80+
source: "react-native",
81+
type: "HomeLayout(./(tabs)/(home)/_layout.tsx)",
82+
title: "HomeLayout(./(tabs)/(home)/_layout.tsx)",
83+
children: [{ source: "react-native", type: "RCTView" }],
84+
},
85+
],
86+
},
87+
];
88+
89+
const tree = buildAccessibilityTree(roots);
90+
91+
expect(tree[0].node.type).toBe("HomeLayout(./(tabs)/(home)/_layout.tsx)");
92+
expect(tree[0].chain.map((node) => node.type)).toEqual(["RCTView"]);
93+
});
94+
95+
it("compacts generated numeric React Native wrapper titles", () => {
96+
const roots: AccessibilityNode[] = [
97+
{
98+
source: "react-native",
99+
type: "Wrap",
100+
title: "1",
101+
children: [
102+
{
103+
source: "react-native",
104+
type: "RCTView",
105+
title: "2",
106+
children: [{ source: "react-native", type: "Text", title: "7d" }],
107+
},
108+
],
109+
},
110+
];
111+
112+
const tree = buildAccessibilityTree(roots);
113+
114+
expect(tree[0].node.type).toBe("Text");
115+
expect(tree[0].chain.map((node) => node.type)).toEqual(["Wrap", "RCTView"]);
116+
});
117+
});
5118

6119
describe("findAccessibilityItemAtPoint", () => {
7120
it("descends through frameless wrapper nodes", () => {

client/src/features/accessibility/accessibilityTree.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,10 @@ function canCompactReactNativeNode(node: AccessibilityNode): boolean {
172172
if (node.source !== "react-native" || node.children?.length !== 1) {
173173
return false;
174174
}
175-
return !validFrame(node.frame) && !hasMeaningfulNodeContent(node);
175+
if (primarySourceLocationFile(node) || isRouteDisplayName(node)) {
176+
return false;
177+
}
178+
return !hasMeaningfulNodeContent(node);
176179
}
177180

178181
function findContainingItem(
@@ -239,10 +242,40 @@ function hasMeaningfulNodeContent(node: AccessibilityNode): boolean {
239242
node.title,
240243
].some((value) => {
241244
const text = cleanText(value);
245+
if (text && isGeneratedReactNativeText(node, text)) {
246+
return false;
247+
}
242248
return Boolean(text && !generatedNames.has(text));
243249
});
244250
}
245251

252+
function primarySourceLocationFile(node: AccessibilityNode): string {
253+
return (
254+
cleanText(node.sourceLocation?.file) ??
255+
cleanText(node.sourceLocations?.find((location) => location?.file)?.file) ??
256+
cleanText(node.sourceFile) ??
257+
""
258+
);
259+
}
260+
261+
function isRouteDisplayName(node: AccessibilityNode): boolean {
262+
return /\(\.{1,2}\/.+\.[cm]?[jt]sx?\)$/.test(cleanText(node.type) ?? "");
263+
}
264+
265+
function isGeneratedReactNativeText(
266+
node: AccessibilityNode,
267+
text: string,
268+
): boolean {
269+
if (node.source !== "react-native" || node.children?.length !== 1) {
270+
return false;
271+
}
272+
const type = cleanText(node.type);
273+
if (type === "Text" || type === "RCTText") {
274+
return false;
275+
}
276+
return /^\d+$/.test(text);
277+
}
278+
246279
function frameContainsPoint(
247280
frame: AccessibilityFrame | null | undefined,
248281
point: { x: number; y: number },

client/src/styles/components.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,16 @@
519519
white-space: nowrap;
520520
}
521521

522+
.hierarchy-node-chain-path {
523+
max-width: 260px;
524+
overflow: hidden;
525+
color: var(--text-muted);
526+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
527+
font-size: 10px;
528+
text-overflow: ellipsis;
529+
white-space: nowrap;
530+
}
531+
522532
.hierarchy-node.selected .hierarchy-node-kind {
523533
color: color-mix(in srgb, var(--accent-text) 82%, var(--success));
524534
}

0 commit comments

Comments
 (0)