@buoy-gg/focus-inspector 7.0.36 → 7.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -122
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,136 +1,61 @@
|
|
|
1
1
|
# @buoy-gg/focus-inspector
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Inspect observed focus transitions and the discovered focusable inventory in a React Native TV app. Buoy Desktop shows current observations, directional history, and possible focus problems.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @buoy-gg/core @buoy-gg/external-sync @buoy-gg/focus-inspector
|
|
9
|
+
```
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
## Account setup
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
Start in a development build with a Free or Pro Buoy account key. From the app directory:
|
|
10
14
|
|
|
11
15
|
```bash
|
|
12
|
-
|
|
16
|
+
npx --package=@buoy-gg/core buoy login
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
For Expo, initialize Buoy with the key written to `.env.local`:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { Buoy } from "@buoy-gg/core";
|
|
23
|
+
|
|
24
|
+
Buoy.init({ licenseKey: process.env.EXPO_PUBLIC_BUOY_KEY });
|
|
13
25
|
```
|
|
14
26
|
|
|
27
|
+
For React Native CLI, pass the key through your app's environment configuration; `.env.local` is not loaded automatically. Keep the menu inside the providers used by your app. Restart the development server after installing packages. See the [Quick Start](https://buoy.gg/buoy/latest/docs/quick-start) for the complete root component.
|
|
28
|
+
|
|
29
|
+
## Connect a TV app
|
|
30
|
+
|
|
31
|
+
Use a compatible react-native-tvos app and follow the [TV installation guide](https://buoy.gg/buoy/latest/docs/tv/installation). Mount the core integration after account initialization:
|
|
32
|
+
|
|
15
33
|
```tsx
|
|
16
34
|
import { FloatingDevTools } from "@buoy-gg/core";
|
|
17
35
|
|
|
18
|
-
|
|
19
|
-
return (
|
|
20
|
-
<>
|
|
21
|
-
{/* your app */}
|
|
22
|
-
<FloatingDevTools headless /> {/* TV apps run headless — no bubble */}
|
|
23
|
-
</>
|
|
24
|
-
);
|
|
25
|
-
}
|
|
36
|
+
<FloatingDevTools headless />;
|
|
26
37
|
```
|
|
27
38
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
##
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
## What this package does — and deliberately does not
|
|
53
|
-
|
|
54
|
-
It **renders nothing**, and that is a hard constraint rather than a preference. On Android TV any
|
|
55
|
-
focusable view in an overlay becomes a D-pad stop in the host app's focus order
|
|
56
|
-
(`pointerEvents="box-none"` gates touch, not focus), so an on-device panel would change the focus
|
|
57
|
-
order it is measuring. Buoy Desktop is the surface.
|
|
58
|
-
|
|
59
|
-
It ships **no native code** — no podspec, no gradle. The focus stream is JS-only.
|
|
60
|
-
|
|
61
|
-
It is **inert**. The only thing it ever writes is `focusElement`, which you have to click. The
|
|
62
|
-
inventory is a read-only fiber walk (`memoizedProps` + `measureInWindow`); nothing is mounted and
|
|
63
|
-
no prop is set. Verified empirically: the same 12-press script over the TV example produces a
|
|
64
|
-
byte-identical focus sequence with the tool active and with it disabled (see the matrix below).
|
|
65
|
-
|
|
66
|
-
## How it knows
|
|
67
|
-
|
|
68
|
-
**Focus** comes from `react-native/Libraries/Core/RawEventEmitter`, which mirrors every
|
|
69
|
-
Fabric-dispatched event before React's plugins see it. On both platforms every focus change
|
|
70
|
-
arrives there as `topFocus` / `topBlur` carrying the focused view's native tag — no native code,
|
|
71
|
-
no app instrumentation, and crucially it sees focus landing on views with **no** `onFocus`
|
|
72
|
-
handler. That last part is the whole point: a `TVFocusGuideView` with `destinations` is a real
|
|
73
|
-
focus stop with no highlight, and a per-component observer would report "focus is nowhere" for
|
|
74
|
-
exactly the bug you are chasing. The deep import is guarded; if React Native ever moves it, the
|
|
75
|
-
snapshot degrades to `observer: "unavailable"` and the panel says so.
|
|
76
|
-
|
|
77
|
-
`TVEventHandler`'s `focus`/`blur` HWEvents are *not* the source: 0.85 deleted the Fabric calls
|
|
78
|
-
that emitted them ([fork #1037](https://github.com/react-native-tvos/react-native-tvos/issues/1037)).
|
|
79
|
-
The 0.83 line still sends them with a tag, and they are folded in as optional enrichment only.
|
|
80
|
-
|
|
81
|
-
**Direction** comes from `TVEventHandler`'s D-pad events, which carry no tag — so the two streams
|
|
82
|
-
are joined on time, in both directions, because their delivery order is not deterministic. Key
|
|
83
|
-
down is ignored, never key up (tvOS emits only key-up for button presses).
|
|
84
|
-
|
|
85
|
-
**Nothing is computed.** There is no JS API for "which view has focus" and none for "if focus is
|
|
86
|
-
here and the user presses RIGHT, where does it go" — Android has
|
|
87
|
-
`FabricUIManager.findNextFocusableElement` natively but never exposes it to JS, and tvOS's UIKit
|
|
88
|
-
engine decides at runtime and answers nothing. So every edge in this tool is one your app
|
|
89
|
-
actually took. Geometry is used only to *downgrade* a flag ("nothing exists past this, so the
|
|
90
|
-
screen just ends here") or to raise a *suspicion*, never to assert where the engine would go. The
|
|
91
|
-
panel says "unvisited" and "suspected unreachable", never "unreachable".
|
|
92
|
-
|
|
93
|
-
## Detectors
|
|
94
|
-
|
|
95
|
-
| Flag | Rule |
|
|
96
|
-
|---|---|
|
|
97
|
-
| **Dead end** | 3+ presses of one direction in a row from the same element moved nothing, and the inventory knows of something focusable further along in that direction. One successful move clears the run. |
|
|
98
|
-
| **Trap** | A region — an element, or everything inside one `TVFocusGuideView` — that focus was never observed escaping in *any* direction where something focusable exists outside it. Annotated when the enclosing guide sets `trapFocus*`, which suggests it was asked for. |
|
|
99
|
-
| **Invisible stop** | Focus landed on a `TVFocusGuideView` (a stop with no highlight) or on a tag the inventory has never seen. This is what "focus vanished" looks like. |
|
|
100
|
-
| **Focus lost** | A view blurred and nothing took focus within 400ms — focus left the React Native tree. |
|
|
101
|
-
| **Coverage** | Visited vs. inventory, with *suspected unreachable* for elements that are unvisited, have no observed edge pointing at them, and sit right beside a visited neighbour. |
|
|
102
|
-
|
|
103
|
-
Flags are judged per screen: anything recorded before the current scan belonged to a screen that
|
|
104
|
-
no longer exists and is not judged at all.
|
|
105
|
-
|
|
106
|
-
## Platform notes
|
|
107
|
-
|
|
108
|
-
- **`trapFocus*` holds on tvOS and silently does not hold on Android TV.** An observed trap on
|
|
109
|
-
Android is therefore always a real bug, whatever the props say. Do not "fix" the detector to
|
|
110
|
-
make the platforms agree.
|
|
111
|
-
- **Focus/blur ordering differs by platform.** tvOS Fabric delivers `topFocus(new)` and *then*
|
|
112
|
-
`topBlur(old)`; Android delivers blur first. Both are handled — a blur for an element that no
|
|
113
|
-
longer holds focus is a late echo, not the start of a loss.
|
|
114
|
-
- `Platform.isTVOS` is `undefined` on Android TV. Only ever test `=== true`.
|
|
115
|
-
|
|
116
|
-
## Verified on device
|
|
117
|
-
|
|
118
|
-
Apple TV 4K simulator (tvOS 26.2) and Android TV emulator (API 34), both Fabric,
|
|
119
|
-
react-native-tvos 0.85.3-3, driven with `idb ui key` / `adb shell input keyevent` over the TV
|
|
120
|
-
example app's focus fixtures.
|
|
121
|
-
|
|
122
|
-
| Case | Apple TV | Android TV |
|
|
123
|
-
|---|---|---|
|
|
124
|
-
| Observer + D-pad stream attach; inventory scans | ✅ 63 focusables on the home screen | ✅ 63 focusables |
|
|
125
|
-
| Clean traversal: correct `from → direction → to`, coverage climbs, no false flags | ✅ | ✅ |
|
|
126
|
-
| Direction credited in both delivery orders (key before / after the focus pair) | ✅ | ✅ |
|
|
127
|
-
| Screen edge downgraded, not flagged (LEFT from the leftmost card) | ✅ | ✅ |
|
|
128
|
-
| Armed 4-way `trapFocus` guide → trap flagged, annotated "guide asked for this" | ✅ | ✅ correctly **not** flagged — `trapFocus` does not hold there, and focus escaped |
|
|
129
|
-
| Trap released → flag clears on the next traversal | ✅ | n/a |
|
|
130
|
-
| Focus parked on a `destinations` guide → invisible-stop flag | ✅ | not reproduced — the engine returned a real dead end instead, which was flagged as one |
|
|
131
|
-
| `focusElement` moves focus (`requestTVFocus`) | ✅ | ✅ |
|
|
132
|
-
| Inertness: identical focus sequence with the tool active vs. disabled | ✅ | — |
|
|
133
|
-
|
|
134
|
-
## License
|
|
135
|
-
|
|
136
|
-
Proprietary. See the [Buoy](https://github.com/Buoy-gg/buoy) repository.
|
|
39
|
+
Keep the TV app headless so a focusable developer overlay does not interfere with its navigation. Open Desktop, sign in, select the device, and open the tool's panel. A phone build is not a substitute for testing TV behavior.
|
|
40
|
+
|
|
41
|
+
## Observe a known focus path
|
|
42
|
+
|
|
43
|
+
Focus Inspector requires the TV fork and New Architecture event support. Move through a short known path and compare the highlighted element with the panel's transitions. A direction remains unknown until it has been observed. Scan the screen and inspect discovered elements that remain unvisited.
|
|
44
|
+
|
|
45
|
+
Listeners observe focus events; inventory scanning depends on dashboard watch state. Internal React Native event APIs may change between versions. If the observer is unavailable, the panel should report that state rather than claim complete capture.
|
|
46
|
+
|
|
47
|
+
## Interpret findings
|
|
48
|
+
|
|
49
|
+
Flags identify possible dead ends, traps, invisible stops, focus loss, and unvisited elements. They are diagnostic heuristics, not proof that an element is unreachable. Intentional focus guides can constrain navigation on both tvOS and Android TV; evaluate trapFocus behavior against the TV fork version your app uses.
|
|
50
|
+
|
|
51
|
+
Actions include `rescan`, `clearHistory`, `setTracking`, and `focusElement({ nativeTag })`. The last action changes focus; it is not equivalent to traversing the same route with D-pad presses.
|
|
52
|
+
|
|
53
|
+
Verify your routes, layouts, and TV fork version, including a comparison with capture disabled if you suspect measurement interference.
|
|
54
|
+
|
|
55
|
+
[Full Focus Inspector guide](https://buoy.gg/buoy/latest/docs/tools/focus-inspector)
|
|
56
|
+
|
|
57
|
+
## Desktop and MCP
|
|
58
|
+
|
|
59
|
+
React Native connections require `@buoy-gg/external-sync`. Follow the [Desktop connection guide](https://buoy.gg/buoy/latest/docs/desktop), sign in to Desktop separately, and confirm the selected device. [MCP](https://buoy.gg/buoy/latest/docs/mcp) also needs a process account and Pro access.
|
|
60
|
+
|
|
61
|
+
Device data can travel over your LAN to the configured broker. Account validation makes network requests. See [Telemetry](https://buoy.gg/buoy/latest/docs/telemetry) for data-flow details.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buoy-gg/focus-inspector",
|
|
3
|
-
"version": "7.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "7.0.39",
|
|
4
|
+
"description": "Observe focus transitions and discovered focusable elements in React Native TV apps. Review diagnostics in Buoy Desktop.",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
7
7
|
"types": "lib/typescript/index.d.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
],
|
|
13
13
|
"sideEffects": false,
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@buoy-gg/shared-ui": "^7.0.
|
|
15
|
+
"@buoy-gg/shared-ui": "^7.0.39"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
18
|
"react": "*",
|