@buoy-gg/focus-inspector 7.0.38 → 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.
Files changed (41) hide show
  1. package/lib/commonjs/detect/detectors.js +351 -1
  2. package/lib/commonjs/detect/detectors.js.map +1 -0
  3. package/lib/commonjs/index.js +106 -1
  4. package/lib/commonjs/index.js.map +1 -0
  5. package/lib/commonjs/observe/focusEventObserver.js +105 -1
  6. package/lib/commonjs/observe/focusEventObserver.js.map +1 -0
  7. package/lib/commonjs/observe/keyEventObserver.js +94 -1
  8. package/lib/commonjs/observe/keyEventObserver.js.map +1 -0
  9. package/lib/commonjs/observe/rawEventEmitter.js +44 -1
  10. package/lib/commonjs/observe/rawEventEmitter.js.map +1 -0
  11. package/lib/commonjs/scan/focusableScanner.js +497 -1
  12. package/lib/commonjs/scan/focusableScanner.js.map +1 -0
  13. package/lib/commonjs/store/focusStore.js +325 -1
  14. package/lib/commonjs/store/focusStore.js.map +1 -0
  15. package/lib/commonjs/sync/focusInspectorSyncAdapter.js +247 -1
  16. package/lib/commonjs/sync/focusInspectorSyncAdapter.js.map +1 -0
  17. package/lib/module/detect/detectors.js +341 -1
  18. package/lib/module/detect/detectors.js.map +1 -0
  19. package/lib/module/index.js +36 -1
  20. package/lib/module/index.js.map +1 -0
  21. package/lib/module/observe/focusEventObserver.js +99 -1
  22. package/lib/module/observe/focusEventObserver.js.map +1 -0
  23. package/lib/module/observe/keyEventObserver.js +86 -1
  24. package/lib/module/observe/keyEventObserver.js.map +1 -0
  25. package/lib/module/observe/rawEventEmitter.js +40 -1
  26. package/lib/module/observe/rawEventEmitter.js.map +1 -0
  27. package/lib/module/scan/focusableScanner.js +492 -1
  28. package/lib/module/scan/focusableScanner.js.map +1 -0
  29. package/lib/module/store/focusStore.js +312 -1
  30. package/lib/module/store/focusStore.js.map +1 -0
  31. package/lib/module/sync/focusInspectorSyncAdapter.js +244 -1
  32. package/lib/module/sync/focusInspectorSyncAdapter.js.map +1 -0
  33. package/lib/typescript/detect/detectors.d.ts.map +1 -0
  34. package/lib/typescript/index.d.ts.map +1 -0
  35. package/lib/typescript/observe/focusEventObserver.d.ts.map +1 -0
  36. package/lib/typescript/observe/keyEventObserver.d.ts.map +1 -0
  37. package/lib/typescript/observe/rawEventEmitter.d.ts.map +1 -0
  38. package/lib/typescript/scan/focusableScanner.d.ts.map +1 -0
  39. package/lib/typescript/store/focusStore.d.ts.map +1 -0
  40. package/lib/typescript/sync/focusInspectorSyncAdapter.d.ts.map +1 -0
  41. package/package.json +2 -2
@@ -1 +1,94 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.attachKeyObserver=attachKeyObserver,exports.detachKeyObserver=detachKeyObserver,exports.isKeyObserverListening=isKeyObserverListening,exports.platformOS=exports.isTV=void 0;var ReactNative=_interopRequireWildcard(require("react-native")),_focusStore=require("../store/focusStore");function _interopRequireWildcard(e,t){if("function"==typeof WeakMap)var r=new WeakMap,n=new WeakMap;return(_interopRequireWildcard=function(e,t){if(!t&&e&&e.__esModule)return e;var o,i,s={__proto__:null,default:e};if(null===e||"object"!=typeof e&&"function"!=typeof e)return s;if(o=t?n:r){if(o.has(e))return o.get(e);o.set(e,s)}for(const t in e)"default"!==t&&{}.hasOwnProperty.call(e,t)&&((i=(o=Object.defineProperty)&&Object.getOwnPropertyDescriptor(e,t))&&(i.get||i.set)?o(s,t,i):s[t]=e[t]);return s})(e,t)}const RN=ReactNative,isTV=exports.isTV=!0===RN.Platform?.isTV,platformOS=exports.platformOS=RN.Platform?.OS??"unknown",DIRECTION_BY_EVENT_TYPE={up:"up",down:"down",left:"left",right:"right",longUp:"up",longDown:"down",longLeft:"left",longRight:"right"};let subscription=null;function handleEvent(e){const t=e,r=t?.eventType;if("string"!=typeof r)return;if(0===t?.eventKeyAction)return;if("focus"===r||"blur"===r){if("number"!=typeof t?.tag)return;return void("focus"===r?(0,_focusStore.recordFocus)(t.tag):(0,_focusStore.recordBlur)(t.tag))}const n=DIRECTION_BY_EVENT_TYPE[r];n&&(0,_focusStore.recordKey)(n)}function attachKeyObserver(){!subscription&&isTV&&RN.TVEventHandler&&(subscription=RN.TVEventHandler.addListener(handleEvent))}function detachKeyObserver(){if(subscription){try{subscription.remove()}catch{}subscription=null}}function isKeyObserverListening(){return null!==subscription}
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.attachKeyObserver = attachKeyObserver;
7
+ exports.detachKeyObserver = detachKeyObserver;
8
+ exports.isKeyObserverListening = isKeyObserverListening;
9
+ exports.platformOS = exports.isTV = void 0;
10
+ var ReactNative = _interopRequireWildcard(require("react-native"));
11
+ var _focusStore = require("../store/focusStore");
12
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
13
+ /**
14
+ * keyEventObserver — the direction half of the stream.
15
+ *
16
+ * D-pad HWEvents carry NO tag on either platform (an Android payload is exactly
17
+ * `{"eventKeyAction":1,"eventType":"right"}`), and focus events carry no
18
+ * direction. So key events tell you *which way the user pushed* and focus
19
+ * events tell you *where focus went*; the store joins them on time.
20
+ *
21
+ * `TVEventHandler.addListener` (not the `useTVEventHandler` hook) because sync
22
+ * adapters are not React components — nothing here mounts.
23
+ *
24
+ * On the 0.83 fork line iOS Fabric ALSO still emits tagged `focus`/`blur`
25
+ * HWEvents here. Those are folded into the same store as enrichment so the
26
+ * older line gets a focus source even if the RawEventEmitter path ever fails —
27
+ * never depended on, because 0.85 deleted them.
28
+ */
29
+
30
+ /**
31
+ * `TVEventHandler` only exists on the react-native-tvos fork. A namespace read
32
+ * (rather than a named import) keeps this module importable on stock RN.
33
+ */
34
+ const RN = ReactNative;
35
+ const isTV = exports.isTV = RN.Platform?.isTV === true;
36
+ const platformOS = exports.platformOS = RN.Platform?.OS ?? "unknown";
37
+
38
+ /**
39
+ * Long-press variants move focus the same way a tap does, so they map to the
40
+ * same direction. Everything else (select, menu, playPause, pans, swipes) never
41
+ * moves focus and is dropped rather than logged as a probe that can't resolve.
42
+ */
43
+ const DIRECTION_BY_EVENT_TYPE = {
44
+ up: "up",
45
+ down: "down",
46
+ left: "left",
47
+ right: "right",
48
+ longUp: "up",
49
+ longDown: "down",
50
+ longLeft: "left",
51
+ longRight: "right"
52
+ };
53
+ let subscription = null;
54
+ function handleEvent(raw) {
55
+ const event = raw;
56
+ const eventType = event?.eventType;
57
+ if (typeof eventType !== "string") return;
58
+
59
+ /**
60
+ * Dedupe by ignoring key DOWN, never key UP. tvOS emits keyAction 1 (up)
61
+ * ONLY for button presses — filtering to "down" would swallow every tvOS
62
+ * event — while Android's key-down dispatch sits behind a feature flag that
63
+ * is off by default. One press must open exactly one probe.
64
+ */
65
+ if (event?.eventKeyAction === 0) return;
66
+
67
+ // 0.83-line iOS bonus: tagged focus/blur HWEvents. Guarded on the tag being
68
+ // present so 0.85's tag-less variants (if any ever return) can't record a
69
+ // transition to nowhere.
70
+ if (eventType === "focus" || eventType === "blur") {
71
+ if (typeof event?.tag !== "number") return;
72
+ if (eventType === "focus") (0, _focusStore.recordFocus)(event.tag);else (0, _focusStore.recordBlur)(event.tag);
73
+ return;
74
+ }
75
+ const direction = DIRECTION_BY_EVENT_TYPE[eventType];
76
+ if (direction) (0, _focusStore.recordKey)(direction);
77
+ }
78
+ function attachKeyObserver() {
79
+ if (subscription || !isTV || !RN.TVEventHandler) return;
80
+ subscription = RN.TVEventHandler.addListener(handleEvent);
81
+ }
82
+ function detachKeyObserver() {
83
+ if (!subscription) return;
84
+ try {
85
+ subscription.remove();
86
+ } catch {
87
+ /* already gone */
88
+ }
89
+ subscription = null;
90
+ }
91
+ function isKeyObserverListening() {
92
+ return subscription !== null;
93
+ }
94
+ //# sourceMappingURL=keyEventObserver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["ReactNative","_interopRequireWildcard","require","_focusStore","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","RN","isTV","exports","Platform","platformOS","OS","DIRECTION_BY_EVENT_TYPE","up","down","left","right","longUp","longDown","longLeft","longRight","subscription","handleEvent","raw","event","eventType","eventKeyAction","tag","recordFocus","recordBlur","direction","recordKey","attachKeyObserver","TVEventHandler","addListener","detachKeyObserver","remove","isKeyObserverListening"],"sourceRoot":"../../../src","sources":["observe/keyEventObserver.ts"],"mappings":"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","ignoreList":[]}
@@ -1 +1,44 @@
1
- "use strict";function resolveRawEventEmitter(){let e;try{e=require("react-native/Libraries/Core/RawEventEmitter")}catch(e){return{emitter:null,reason:e?.message??"RawEventEmitter is unavailable"}}const t=e?.default??e;return"function"!=typeof t?.addListener?{emitter:null,reason:"RawEventEmitter has no addListener"}:{emitter:t,reason:null}}Object.defineProperty(exports,"__esModule",{value:!0}),exports.resolveRawEventEmitter=resolveRawEventEmitter;
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.resolveRawEventEmitter = resolveRawEventEmitter;
7
+ /**
8
+ * The one guarded require for React Native's RawEventEmitter, kept in its own
9
+ * module for two reasons.
10
+ *
11
+ * 1. `react-native/Libraries/Core/RawEventEmitter` is a DEEP import. It warns
12
+ * under 0.85's export map and could move in any release, so the failure has
13
+ * to degrade to "observer unavailable" in the snapshot rather than taking
14
+ * the host app's bundle down. Metro only treats a require as optional when
15
+ * it sits LEXICALLY inside a try statement — wrapping it in a helper or an
16
+ * arrow makes Metro hard-fail the bundle instead.
17
+ * 2. It gives the unit suite a seam. Node's `require` resolves through the
18
+ * filesystem and ignores Vitest's module mocks, so the emitter can only be
19
+ * faked by mocking a module that is reached with a real `import`.
20
+ */
21
+
22
+ function resolveRawEventEmitter() {
23
+ let mod;
24
+ try {
25
+ mod = require("react-native/Libraries/Core/RawEventEmitter");
26
+ } catch (e) {
27
+ return {
28
+ emitter: null,
29
+ reason: e?.message ?? "RawEventEmitter is unavailable"
30
+ };
31
+ }
32
+ const candidate = mod?.default ?? mod;
33
+ if (typeof candidate?.addListener !== "function") {
34
+ return {
35
+ emitter: null,
36
+ reason: "RawEventEmitter has no addListener"
37
+ };
38
+ }
39
+ return {
40
+ emitter: candidate,
41
+ reason: null
42
+ };
43
+ }
44
+ //# sourceMappingURL=rawEventEmitter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["resolveRawEventEmitter","mod","require","e","emitter","reason","message","candidate","default","addListener"],"sourceRoot":"../../../src","sources":["observe/rawEventEmitter.ts"],"mappings":";;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAkBO,SAASA,sBAAsBA,CAAA,EAAyB;EAC7D,IAAIC,GAAY;EAChB,IAAI;IACFA,GAAG,GAAGC,OAAO,CAAC,6CAA6C,CAAC;EAC9D,CAAC,CAAC,OAAOC,CAAC,EAAE;IACV,OAAO;MAAEC,OAAO,EAAE,IAAI;MAAEC,MAAM,EAAGF,CAAC,EAAYG,OAAO,IAAI;IAAiC,CAAC;EAC7F;EACA,MAAMC,SAAS,GAAKN,GAAG,EAA4BO,OAAO,IAAIP,GAEjD;EACb,IAAI,OAAOM,SAAS,EAAEE,WAAW,KAAK,UAAU,EAAE;IAChD,OAAO;MAAEL,OAAO,EAAE,IAAI;MAAEC,MAAM,EAAE;IAAqC,CAAC;EACxE;EACA,OAAO;IAAED,OAAO,EAAEG,SAAS;IAAEF,MAAM,EAAE;EAAK,CAAC;AAC7C","ignoreList":[]}
@@ -1 +1,497 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.requestFocusForTag=requestFocusForTag,exports.scanFocusables=scanFocusables;var _reactNative=require("react-native");const OffscreenComponent=22,INTERNAL_NAMES=new Set(["View","RCTView","ScrollView","RCTScrollView","RCTScrollContentView","SafeAreaView","RCTSafeAreaView","AndroidHorizontalScrollContentView","KeyboardAvoidingView","Pressable","PlatformPressable","TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","TouchableNativeFeedback","AnimatedComponent","Wrapper"]),GUIDE_NAME="TVFocusGuideView";function getReactDevToolsHook(){return"undefined"==typeof global?null:global.__REACT_DEVTOOLS_GLOBAL_HOOK__??null}function getAllFiberRoots(){const e=getReactDevToolsHook();if(!e)return[];const n=[];try{if(e.renderers&&"function"==typeof e.renderers.forEach&&e.renderers.forEach((t,o)=>{try{const t=e.getFiberRoots?.(o);t&&t.forEach(e=>n.push(e))}catch{}}),0===n.length&&e.getFiberRoots){const t=e.getFiberRoots(1);t&&t.forEach(e=>n.push(e))}}catch{}return n}function isHiddenOffscreen(e){return 22===e.tag&&null!==e.memoizedState}function isInactiveScreen(e){const n=e.memoizedProps?.activityState;return 0===n||1===n||!1===e.memoizedProps?.isPageFocused}function getProps(e){return e?.memoizedProps??e?.pendingProps??null}function getComponentName(e){const n=e?.type;return n?"string"==typeof n?n:n.displayName||n.name||null:null}function getOwningComponentName(e){let n=e?._debugOwner||e?.return,t=0;for(;n&&t<30;){t++;const e=getComponentName(n);if(e&&!INTERNAL_NAMES.has(e)&&"string"!=typeof n.type)return e;n=n.return}return null}function getPublicInstance(e){if(!e)return null;const n=e;return n.canonical?.publicInstance?n.canonical.publicInstance:n.canonical&&"function"==typeof n.canonical.measure?n.canonical:"function"==typeof n.measure||"function"==typeof n.measureInWindow?n:null}function getNativeTag(e){if(null==e)return null;const n=e;if("number"==typeof n.__nativeTag)return n.__nativeTag;if("number"==typeof n._nativeTag)return n._nativeTag;if("number"==typeof n.nativeTag)return n.nativeTag;if(n.canonical){if("number"==typeof n.canonical.__nativeTag)return n.canonical.__nativeTag;if("number"==typeof n.canonical._nativeTag)return n.canonical._nativeTag;if("number"==typeof n.canonical.nativeTag)return n.canonical.nativeTag}return null}function findHostInstance(e){const n=getPublicInstance(e.stateNode);if(n)return{instance:n,nativeTag:getNativeTag(e.stateNode)};const t=[];e.child&&t.push(e.child);let o=0;for(;t.length>0&&o<4e3;){o++;const e=t.pop();if(!e)continue;const n=getPublicInstance(e.stateNode);if(n)return{instance:n,nativeTag:getNativeTag(e.stateNode)};e.child&&t.push(e.child),e.sibling&&t.push(e.sibling)}return null}function focusableFlag(e){return"boolean"==typeof e.isTVSelectable?e.isTVSelectable:"boolean"==typeof e.focusable?e.focusable:"boolean"==typeof e.tvFocusable?e.tvFocusable:void 0}function nextFocusFlags(e){const n="number"==typeof e.nextFocusUp,t="number"==typeof e.nextFocusDown,o="number"==typeof e.nextFocusLeft,i="number"==typeof e.nextFocusRight;if(n||t||o||i)return{up:n||void 0,down:t||void 0,left:o||void 0,right:i||void 0}}function getVisibleText(e){if(!e)return;const n=e=>"string"==typeof e?e.trim()?e.trim().slice(0,60):void 0:"number"==typeof e&&Number.isFinite(e)?String(e):void 0;return n(e.children)??n(e.title)??n(e.label)??void 0}function collectDescendantText(e,n){const t=[],o=(i,s)=>{if(!i||s>n||t.length>=3)return;if(i!==e){const e=getProps(i);if(e&&"function"==typeof e.onPress)return}const r=getVisibleText(getProps(i));r&&!t.includes(r)&&t.push(r),i.child&&o(i.child,s+1),i!==e&&i.sibling&&o(i.sibling,s)};e.child&&o(e.child,1);const i=t.join(" ").trim();return i?i.slice(0,60):void 0}function measureInstance(e){return new Promise(n=>{let t=!1;const o=e=>{t||(t=!0,n(e))};setTimeout(()=>o(null),1e3);try{"function"==typeof e.measureInWindow?e.measureInWindow((e,n,t,i)=>{o(t>0&&i>0?{x:e,y:n,width:t,height:i}:null)}):"function"==typeof e.measure?e.measure((e,n,t,i,s,r)=>{o(t>0&&i>0&&null!=s&&null!=r?{x:s,y:r,width:t,height:i}:null)}):o(null)}catch{o(null)}})}const instancesByTag=new Map;function walk(e,n,t){const o=[{fiber:e,context:n,depth:0}],i=new Set;for(;o.length>0;){const e=o.pop();if(!e)continue;const{fiber:n,depth:s}=e;if(!n||i.has(n)||s>500)continue;if(i.add(n),isHiddenOffscreen(n)||isInactiveScreen(n)){n.sibling&&o.push({fiber:n.sibling,context:e.context,depth:s});continue}const r=t(n,e.context);n.sibling&&o.push({fiber:n.sibling,context:e.context,depth:s}),n.child&&o.push({fiber:n.child,context:r,depth:s+1})}}function collectCandidates(){const e=getAllFiberRoots();if(0===e.length)return[];const n=[],t=new Set;for(const o of e)walk(o.current,{guideTag:null},(e,o)=>{const i=getProps(e);if(!i)return o;const s=getComponentName(e),r=s===GUIDE_NAME,a=focusableFlag(i),c="function"==typeof i.onPress||"function"==typeof i.onLongPress,u=!0===i.hasTVPreferredFocus,l=nextFocusFlags(i);if(!(r||!0===a||u||l||c&&!1!==a))return o;const f=findHostInstance(e),d=f?.nativeTag??null;if(null===d)return o;const g=r?{guideTag:d}:o;if(t.has(d)){const e=n.find(e=>e.node.nativeTag===d);return e&&(void 0!==a&&void 0===e.node.focusProps.focusable&&(e.node.focusProps.focusable=a),u&&(e.node.focusProps.hasTVPreferredFocus=!0),l&&!e.node.focusProps.nextFocus&&(e.node.focusProps.nextFocus=l),c&&(e.node.pressable=!0),!e.node.testID&&i.testID&&(e.node.testID=i.testID),r&&!e.node.guide&&(e.node.guide=readGuide(i),e.node.name=GUIDE_NAME)),g}t.add(d);let p=s??"Unknown";s&&!INTERNAL_NAMES.has(s)&&"string"!=typeof e.type||(p=getOwningComponentName(e)??s??"Unknown");const h=i.accessibilityLabel||void 0,b=getVisibleText(i)??(c?collectDescendantText(e,6):void 0),m={nativeTag:d,name:r?GUIDE_NAME:p,owner:getOwningComponentName(e)??void 0,testID:i.testID||void 0,label:h,text:b,focusProps:{focusable:a,hasTVPreferredFocus:u||void 0,nextFocus:l},guide:r?readGuide(i):void 0,guideAncestorTag:o.guideTag??void 0,pressable:c||void 0};return f?.instance&&instancesByTag.set(d,f.instance),n.push({node:m,instance:f?.instance??null}),g});return n}function readGuide(e){return{autoFocus:!0===e.autoFocus,hasDestinations:Array.isArray(e.destinations)&&e.destinations.length>0,enabled:!1!==e.enabled,trap:{up:!0===e.trapFocusUp,down:!0===e.trapFocusDown,left:!0===e.trapFocusLeft,right:!0===e.trapFocusRight}}}function round3(e){return Math.round(1e3*e)/1e3}const MAX_NODES=400;async function scanFocusables(){instancesByTag.clear();const e=collectCandidates();await Promise.all(e.map(async e=>{if(!e.instance)return;const n=await measureInstance(e.instance);n&&(e.node.frame=n)}));const n=_reactNative.Dimensions.get("window"),t=Math.round(n.width)||1,o=Math.round(n.height)||1,i=[];for(const n of e){const e=n.node.frame;if(e){const i=e.x+e.width/2,s=e.y+e.height/2;n.node.onScreen=s>=0&&s<=o&&i>=0&&i<=t,n.node.frame={x:round3(e.x/t),y:round3(e.y/o),width:round3(e.width/t),height:round3(e.height/o)}}if(i.push(n.node),i.length>=MAX_NODES)break}return i.sort((e,n)=>{const t=e.frame?.y??2,o=n.frame?.y??2;return Math.abs(t-o)>.01?t-o:(e.frame?.x??2)-(n.frame?.x??2)}),{screen:{width:t,height:o},nodes:i,scannedAt:Date.now()}}function requestFocusForTag(e){const n=instancesByTag.get(e);if(!n)return{ok:!1,reason:"Unknown tag — rescan and try again."};if("function"!=typeof n.requestTVFocus)return{ok:!1,reason:"This element exposes no requestTVFocus()."};try{return n.requestTVFocus(),{ok:!0}}catch(e){return{ok:!1,reason:e?.message??"requestTVFocus() threw."}}}
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.requestFocusForTag = requestFocusForTag;
7
+ exports.scanFocusables = scanFocusables;
8
+ var _reactNative = require("react-native");
9
+ /**
10
+ * focusableScanner — a read-only fiber walk that inventories everything on
11
+ * screen the TV focus engine could land on.
12
+ *
13
+ * Provably inert: it reads `memoizedProps` and calls `measureInWindow`. It adds
14
+ * no views, sets no props, and calls no focus command — which matters more here
15
+ * than anywhere else in Buoy, because on Android TV a single focusable view we
16
+ * rendered would insert itself into the host app's focus order and change the
17
+ * very navigation this tool measures.
18
+ *
19
+ * Traversal (multi-renderer roots, the Fabric/Paper/CLI-dev instance triad,
20
+ * Offscreen + inactive-`react-native-screens` pruning, measure-with-a-hang-
21
+ * guard) follows the house pattern from
22
+ * `packages/highlight-updates/src/highlight-updates/utils/screenElements.ts`.
23
+ * The repo convention is a per-package copy trimmed to the tool's needs rather
24
+ * than a shared walker library — see packages/CLAUDE.md.
25
+ *
26
+ * There is no JS API for "which view has focus" and none for "if focus is here
27
+ * and the user presses RIGHT, where does it go" (Android has
28
+ * `FabricUIManager.findNextFocusableElement` natively but never exposes it to
29
+ * JS; tvOS's UIKit engine decides at runtime and answers nothing). So this
30
+ * inventory is the *candidate set* — the observed transition stream is what
31
+ * says which of them focus actually reaches.
32
+ */
33
+
34
+ const OffscreenComponent = 22;
35
+
36
+ /** Host wrappers whose own name says nothing — fall through to the owner. */
37
+ const INTERNAL_NAMES = new Set(["View", "RCTView", "ScrollView", "RCTScrollView", "RCTScrollContentView", "SafeAreaView", "RCTSafeAreaView", "AndroidHorizontalScrollContentView", "KeyboardAvoidingView", "Pressable", "PlatformPressable", "TouchableOpacity", "TouchableHighlight", "TouchableWithoutFeedback", "TouchableNativeFeedback", "AnimatedComponent", "Wrapper"]);
38
+
39
+ /** The composite that renders an invisible focus stop. */
40
+ const GUIDE_NAME = "TVFocusGuideView";
41
+ // --- fiber plumbing -------------------------------------------------------
42
+
43
+ function getReactDevToolsHook() {
44
+ if (typeof global === "undefined") return null;
45
+ return global.__REACT_DEVTOOLS_GLOBAL_HOOK__ ?? null;
46
+ }
47
+
48
+ /**
49
+ * Roots across ALL renderers. A single renderer id is not safe to assume:
50
+ * secondary reconcilers (skia, svg) register their own, sometimes first.
51
+ */
52
+ function getAllFiberRoots() {
53
+ const hook = getReactDevToolsHook();
54
+ if (!hook) return [];
55
+ const roots = [];
56
+ try {
57
+ if (hook.renderers && typeof hook.renderers.forEach === "function") {
58
+ hook.renderers.forEach((_r, rendererId) => {
59
+ try {
60
+ const set = hook.getFiberRoots?.(rendererId);
61
+ if (set) set.forEach(root => roots.push(root));
62
+ } catch {
63
+ /* one renderer failing must not lose the others */
64
+ }
65
+ });
66
+ }
67
+ if (roots.length === 0 && hook.getFiberRoots) {
68
+ const legacy = hook.getFiberRoots(1);
69
+ if (legacy) legacy.forEach(root => roots.push(root));
70
+ }
71
+ } catch {
72
+ /* ignore */
73
+ }
74
+ return roots;
75
+ }
76
+ function isHiddenOffscreen(fiber) {
77
+ return fiber.tag === OffscreenComponent && fiber.memoizedState !== null;
78
+ }
79
+ function isInactiveScreen(fiber) {
80
+ // react-native-screens: activityState 0/1 = inactive/transitioning. A card
81
+ // sitting behind the current screen still has focusable props but is not a
82
+ // focus target — counting it would tank the coverage number forever.
83
+ const activityState = fiber.memoizedProps?.activityState;
84
+ if (activityState === 0 || activityState === 1) return true;
85
+ if (fiber.memoizedProps?.isPageFocused === false) return true;
86
+ return false;
87
+ }
88
+ function getProps(fiber) {
89
+ return fiber?.memoizedProps ?? fiber?.pendingProps ?? null;
90
+ }
91
+ function getComponentName(fiber) {
92
+ const type = fiber?.type;
93
+ if (!type) return null;
94
+ if (typeof type === "string") return type;
95
+ return type.displayName || type.name || null;
96
+ }
97
+ function getOwningComponentName(fiber) {
98
+ let cur = fiber?._debugOwner || fiber?.return;
99
+ let depth = 0;
100
+ while (cur && depth < 30) {
101
+ depth++;
102
+ const name = getComponentName(cur);
103
+ if (name && !INTERNAL_NAMES.has(name) && typeof cur.type !== "string") return name;
104
+ cur = cur.return;
105
+ }
106
+ return null;
107
+ }
108
+ /** The Fabric / Paper / RN-CLI-dev instance triad — always check all three. */
109
+ function getPublicInstance(stateNode) {
110
+ if (!stateNode) return null;
111
+ const node = stateNode;
112
+ if (node.canonical?.publicInstance) return node.canonical.publicInstance;
113
+ if (node.canonical && typeof node.canonical.measure === "function") return node.canonical;
114
+ if (typeof node.measure === "function" || typeof node.measureInWindow === "function") {
115
+ return node;
116
+ }
117
+ return null;
118
+ }
119
+ function getNativeTag(stateNode) {
120
+ if (stateNode == null) return null;
121
+ const n = stateNode;
122
+ if (typeof n.__nativeTag === "number") return n.__nativeTag;
123
+ if (typeof n._nativeTag === "number") return n._nativeTag;
124
+ if (typeof n.nativeTag === "number") return n.nativeTag;
125
+ if (n.canonical) {
126
+ if (typeof n.canonical.__nativeTag === "number") return n.canonical.__nativeTag;
127
+ if (typeof n.canonical._nativeTag === "number") return n.canonical._nativeTag;
128
+ if (typeof n.canonical.nativeTag === "number") return n.canonical.nativeTag;
129
+ }
130
+ return null;
131
+ }
132
+
133
+ /** First host node at or below a fiber (self first, then a bounded subtree DFS). */
134
+ function findHostInstance(fiber) {
135
+ const selfInstance = getPublicInstance(fiber.stateNode);
136
+ if (selfInstance) {
137
+ return {
138
+ instance: selfInstance,
139
+ nativeTag: getNativeTag(fiber.stateNode)
140
+ };
141
+ }
142
+ const stack = [];
143
+ if (fiber.child) stack.push(fiber.child);
144
+ let guard = 0;
145
+ while (stack.length > 0 && guard < 4000) {
146
+ guard++;
147
+ const f = stack.pop();
148
+ if (!f) continue;
149
+ const instance = getPublicInstance(f.stateNode);
150
+ if (instance) return {
151
+ instance,
152
+ nativeTag: getNativeTag(f.stateNode)
153
+ };
154
+ if (f.child) stack.push(f.child);
155
+ if (f.sibling) stack.push(f.sibling);
156
+ }
157
+ return null;
158
+ }
159
+
160
+ // --- candidate predicate --------------------------------------------------
161
+
162
+ /**
163
+ * The props that make a view a focus candidate, per the fork's own mapping:
164
+ * on iOS `View` writes `focusable ?? isTVSelectable` into `isTVSelectable` and
165
+ * DELETES `focusable`; on Android it writes `focusable` and deletes
166
+ * `isTVSelectable`. So both names have to be accepted — the host fiber only
167
+ * ever carries one of them, and which one depends on the platform.
168
+ */
169
+ function focusableFlag(props) {
170
+ if (typeof props.isTVSelectable === "boolean") return props.isTVSelectable;
171
+ if (typeof props.focusable === "boolean") return props.focusable;
172
+ if (typeof props.tvFocusable === "boolean") return props.tvFocusable;
173
+ return undefined;
174
+ }
175
+ function nextFocusFlags(props) {
176
+ const up = typeof props.nextFocusUp === "number";
177
+ const down = typeof props.nextFocusDown === "number";
178
+ const left = typeof props.nextFocusLeft === "number";
179
+ const right = typeof props.nextFocusRight === "number";
180
+ if (!up && !down && !left && !right) return undefined;
181
+ return {
182
+ up: up || undefined,
183
+ down: down || undefined,
184
+ left: left || undefined,
185
+ right: right || undefined
186
+ };
187
+ }
188
+ function getVisibleText(props) {
189
+ if (!props) return undefined;
190
+ const pick = value => {
191
+ if (typeof value === "string") return value.trim() ? value.trim().slice(0, 60) : undefined;
192
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
193
+ return undefined;
194
+ };
195
+ return pick(props.children) ?? pick(props.title) ?? pick(props.label) ?? undefined;
196
+ }
197
+
198
+ /** A focusable's own label, gathered from descendants without crossing into another stop. */
199
+ function collectDescendantText(root, maxDepth) {
200
+ const parts = [];
201
+ const visit = (fiber, depth) => {
202
+ if (!fiber || depth > maxDepth || parts.length >= 3) return;
203
+ if (fiber !== root) {
204
+ const props = getProps(fiber);
205
+ if (props && typeof props.onPress === "function") return;
206
+ }
207
+ const text = getVisibleText(getProps(fiber));
208
+ if (text && !parts.includes(text)) parts.push(text);
209
+ if (fiber.child) visit(fiber.child, depth + 1);
210
+ if (fiber !== root && fiber.sibling) visit(fiber.sibling, depth);
211
+ };
212
+ if (root.child) visit(root.child, 1);
213
+ const joined = parts.join(" ").trim();
214
+ return joined ? joined.slice(0, 60) : undefined;
215
+ }
216
+
217
+ // --- measurement ----------------------------------------------------------
218
+
219
+ function measureInstance(instance) {
220
+ return new Promise(resolve => {
221
+ let settled = false;
222
+ const done = rect => {
223
+ if (settled) return;
224
+ settled = true;
225
+ resolve(rect);
226
+ };
227
+ // Detached / off-screen nodes can fire NEITHER callback — without this the
228
+ // whole scan would hang on one stale node.
229
+ setTimeout(() => done(null), 1000);
230
+ try {
231
+ if (typeof instance.measureInWindow === "function") {
232
+ instance.measureInWindow((x, y, width, height) => {
233
+ done(width > 0 && height > 0 ? {
234
+ x,
235
+ y,
236
+ width,
237
+ height
238
+ } : null);
239
+ });
240
+ } else if (typeof instance.measure === "function") {
241
+ instance.measure((_x, _y, width, height, pageX, pageY) => {
242
+ done(width > 0 && height > 0 && pageX != null && pageY != null ? {
243
+ x: pageX,
244
+ y: pageY,
245
+ width,
246
+ height
247
+ } : null);
248
+ });
249
+ } else {
250
+ done(null);
251
+ }
252
+ } catch {
253
+ done(null);
254
+ }
255
+ });
256
+ }
257
+
258
+ // --- collection -----------------------------------------------------------
259
+
260
+ /**
261
+ * A live handle on the element behind a native tag, kept out of the wire
262
+ * snapshot. `focusElement` needs the public INSTANCE (Commands take instance
263
+ * refs, never tags), so the last scan's instances are the lookup table.
264
+ */
265
+ const instancesByTag = new Map();
266
+ function walk(fiber, context, visit) {
267
+ const stack = [{
268
+ fiber,
269
+ context,
270
+ depth: 0
271
+ }];
272
+ const seen = new Set();
273
+ while (stack.length > 0) {
274
+ const entry = stack.pop();
275
+ if (!entry) continue;
276
+ const {
277
+ fiber: current,
278
+ depth
279
+ } = entry;
280
+ if (!current || seen.has(current) || depth > 500) continue;
281
+ seen.add(current);
282
+ if (isHiddenOffscreen(current) || isInactiveScreen(current)) {
283
+ // Prune the subtree, keep walking siblings.
284
+ if (current.sibling) {
285
+ stack.push({
286
+ fiber: current.sibling,
287
+ context: entry.context,
288
+ depth
289
+ });
290
+ }
291
+ continue;
292
+ }
293
+ const childContext = visit(current, entry.context);
294
+ if (current.sibling) stack.push({
295
+ fiber: current.sibling,
296
+ context: entry.context,
297
+ depth
298
+ });
299
+ if (current.child) stack.push({
300
+ fiber: current.child,
301
+ context: childContext,
302
+ depth: depth + 1
303
+ });
304
+ }
305
+ }
306
+ function collectCandidates() {
307
+ const roots = getAllFiberRoots();
308
+ if (roots.length === 0) return [];
309
+ const out = [];
310
+ const seenTags = new Set();
311
+ for (const root of roots) {
312
+ walk(root.current, {
313
+ guideTag: null
314
+ }, (fiber, context) => {
315
+ const props = getProps(fiber);
316
+ if (!props) return context;
317
+ const componentName = getComponentName(fiber);
318
+ const isGuide = componentName === GUIDE_NAME;
319
+ const flag = focusableFlag(props);
320
+ const pressable = typeof props.onPress === "function" || typeof props.onLongPress === "function";
321
+ const hasPreferred = props.hasTVPreferredFocus === true;
322
+ const nextFocus = nextFocusFlags(props);
323
+
324
+ // A composite Pressable and its host View both match; the host is the one
325
+ // that carries the native tag focus events report, so dedupe on the tag.
326
+ const isCandidate = isGuide || flag === true || hasPreferred || !!nextFocus || pressable && flag !== false;
327
+ if (!isCandidate) return context;
328
+ const host = findHostInstance(fiber);
329
+ const nativeTag = host?.nativeTag ?? null;
330
+ if (nativeTag === null) return context;
331
+ const nextContext = isGuide ? {
332
+ guideTag: nativeTag
333
+ } : context;
334
+ if (seenTags.has(nativeTag)) {
335
+ // Merge the richer signal onto the row already recorded for this tag —
336
+ // the composite knows the name, the host knows the focus props.
337
+ const existing = out.find(c => c.node.nativeTag === nativeTag);
338
+ if (existing) {
339
+ if (flag !== undefined && existing.node.focusProps.focusable === undefined) {
340
+ existing.node.focusProps.focusable = flag;
341
+ }
342
+ if (hasPreferred) existing.node.focusProps.hasTVPreferredFocus = true;
343
+ if (nextFocus && !existing.node.focusProps.nextFocus) {
344
+ existing.node.focusProps.nextFocus = nextFocus;
345
+ }
346
+ if (pressable) existing.node.pressable = true;
347
+ if (!existing.node.testID && props.testID) existing.node.testID = props.testID;
348
+ if (isGuide && !existing.node.guide) {
349
+ existing.node.guide = readGuide(props);
350
+ existing.node.name = GUIDE_NAME;
351
+ }
352
+ }
353
+ return nextContext;
354
+ }
355
+ seenTags.add(nativeTag);
356
+ let name = componentName ?? "Unknown";
357
+ if (!componentName || INTERNAL_NAMES.has(componentName) || typeof fiber.type === "string") {
358
+ name = getOwningComponentName(fiber) ?? componentName ?? "Unknown";
359
+ }
360
+ const label = props.accessibilityLabel || undefined;
361
+ const text = getVisibleText(props) ?? (pressable ? collectDescendantText(fiber, 6) : undefined);
362
+ const node = {
363
+ nativeTag,
364
+ name: isGuide ? GUIDE_NAME : name,
365
+ owner: getOwningComponentName(fiber) ?? undefined,
366
+ testID: props.testID || undefined,
367
+ label,
368
+ text,
369
+ focusProps: {
370
+ focusable: flag,
371
+ hasTVPreferredFocus: hasPreferred || undefined,
372
+ nextFocus
373
+ },
374
+ guide: isGuide ? readGuide(props) : undefined,
375
+ guideAncestorTag: context.guideTag ?? undefined,
376
+ pressable: pressable || undefined
377
+ };
378
+ if (host?.instance) instancesByTag.set(nativeTag, host.instance);
379
+ out.push({
380
+ node,
381
+ instance: host?.instance ?? null
382
+ });
383
+ return nextContext;
384
+ });
385
+ }
386
+ return out;
387
+ }
388
+ function readGuide(props) {
389
+ return {
390
+ autoFocus: props.autoFocus === true,
391
+ hasDestinations: Array.isArray(props.destinations) && props.destinations.length > 0,
392
+ enabled: props.enabled !== false,
393
+ trap: {
394
+ up: props.trapFocusUp === true,
395
+ down: props.trapFocusDown === true,
396
+ left: props.trapFocusLeft === true,
397
+ right: props.trapFocusRight === true
398
+ }
399
+ };
400
+ }
401
+ function round3(n) {
402
+ return Math.round(n * 1000) / 1000;
403
+ }
404
+
405
+ /** Cap the inventory so a pathological screen can't blow the snapshot budget. */
406
+ const MAX_NODES = 400;
407
+ async function scanFocusables() {
408
+ instancesByTag.clear();
409
+ const candidates = collectCandidates();
410
+ await Promise.all(candidates.map(async candidate => {
411
+ if (!candidate.instance) return;
412
+ const rect = await measureInstance(candidate.instance);
413
+ if (!rect) return;
414
+ candidate.node.frame = rect;
415
+ }));
416
+ const window = _reactNative.Dimensions.get("window");
417
+ const screenW = Math.round(window.width) || 1;
418
+ const screenH = Math.round(window.height) || 1;
419
+ const nodes = [];
420
+ for (const candidate of candidates) {
421
+ const rect = candidate.node.frame;
422
+ if (rect) {
423
+ // A guide's own box is often a 1x1 sliver (its StyleSheet minWidth), so
424
+ // there is no minimum-size filter here — the invisible stop IS the point.
425
+ //
426
+ // Off-window elements are KEPT, unlike in the describe-screen walker this
427
+ // is adapted from. A tile below a ScrollView's fold is still somewhere the
428
+ // D-pad goes (the engine scrolls to it), so dropping it would under-report
429
+ // the coverage denominator — the tool's headline number — and would make
430
+ // rows vanish from the inventory as the user scrolls. Previous router
431
+ // screens are already pruned by the inactive-`react-native-screens` check
432
+ // during the walk, which is the case the drop existed for.
433
+ const cx = rect.x + rect.width / 2;
434
+ const cy = rect.y + rect.height / 2;
435
+ candidate.node.onScreen = cy >= 0 && cy <= screenH && cx >= 0 && cx <= screenW;
436
+ candidate.node.frame = {
437
+ x: round3(rect.x / screenW),
438
+ y: round3(rect.y / screenH),
439
+ width: round3(rect.width / screenW),
440
+ height: round3(rect.height / screenH)
441
+ };
442
+ }
443
+ nodes.push(candidate.node);
444
+ if (nodes.length >= MAX_NODES) break;
445
+ }
446
+
447
+ // Reading order, so the inventory table matches how a person scans the screen.
448
+ nodes.sort((a, b) => {
449
+ const ay = a.frame?.y ?? 2;
450
+ const by = b.frame?.y ?? 2;
451
+ if (Math.abs(ay - by) > 0.01) return ay - by;
452
+ return (a.frame?.x ?? 2) - (b.frame?.x ?? 2);
453
+ });
454
+ return {
455
+ screen: {
456
+ width: screenW,
457
+ height: screenH
458
+ },
459
+ nodes,
460
+ scannedAt: Date.now()
461
+ };
462
+ }
463
+
464
+ /**
465
+ * Move focus to a native tag. Source-verified only until this runs on a device:
466
+ * `Commands.requestTVFocus` takes an instance ref, never a tag, so the last
467
+ * scan's instance table is the only way to get from one to the other.
468
+ */
469
+ function requestFocusForTag(nativeTag) {
470
+ const instance = instancesByTag.get(nativeTag);
471
+ if (!instance) {
472
+ return {
473
+ ok: false,
474
+ reason: "Unknown tag — rescan and try again."
475
+ };
476
+ }
477
+ if (typeof instance.requestTVFocus !== "function") {
478
+ // `requestTVFocus` is attached by RN's View to its instance; a host that
479
+ // isn't a View (Text, Image) never gets one.
480
+ return {
481
+ ok: false,
482
+ reason: "This element exposes no requestTVFocus()."
483
+ };
484
+ }
485
+ try {
486
+ instance.requestTVFocus();
487
+ return {
488
+ ok: true
489
+ };
490
+ } catch (e) {
491
+ return {
492
+ ok: false,
493
+ reason: e?.message ?? "requestTVFocus() threw."
494
+ };
495
+ }
496
+ }
497
+ //# sourceMappingURL=focusableScanner.js.map