@qontinui/ui-bridge-native 0.4.0 → 0.6.1
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/dist/core/index.js +38 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +38 -4
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/registry.d.ts +30 -1
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/types.d.ts +18 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/debug/index.js.map +1 -1
- package/dist/debug/index.mjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +232 -30
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +232 -30
- package/dist/index.mjs.map +1 -1
- package/dist/react/UIBridgeNativeProvider.d.ts +9 -2
- package/dist/react/UIBridgeNativeProvider.d.ts.map +1 -1
- package/dist/react/index.js +207 -30
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +207 -30
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/useUIElement.d.ts +73 -35
- package/dist/react/useUIElement.d.ts.map +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/http-server.d.ts +8 -1
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/index.js +135 -4
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +135 -4
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/types.d.ts +13 -0
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/react/index.mjs
CHANGED
|
@@ -254,7 +254,18 @@ var NativeUIBridgeRegistry = class {
|
|
|
254
254
|
return Array.from(this.elements.values());
|
|
255
255
|
}
|
|
256
256
|
/**
|
|
257
|
-
* Get all registered elements that are visible
|
|
257
|
+
* Get all registered elements that are visible AND have a measured layout.
|
|
258
|
+
*
|
|
259
|
+
* This is the *strict* visibility filter: requires `state.visible === true`
|
|
260
|
+
* AND `state.layout !== null`. Use this when you need both signals
|
|
261
|
+
* (e.g. coord-based tap, where you need an actual rect to hit-test against).
|
|
262
|
+
*
|
|
263
|
+
* Callers driving an LLM agent typically want
|
|
264
|
+
* {@link getMountedVisibleElements} instead — that variant accepts
|
|
265
|
+
* elements that *should* be on screen (visible:true) but whose first
|
|
266
|
+
* `onLayout` has not yet fired. Without it, a snapshot taken in the gap
|
|
267
|
+
* between mount and the first layout pass returns 0 elements on a fully
|
|
268
|
+
* populated screen.
|
|
258
269
|
*/
|
|
259
270
|
getVisibleElements() {
|
|
260
271
|
return this.getAllElements().filter((e) => {
|
|
@@ -262,6 +273,26 @@ var NativeUIBridgeRegistry = class {
|
|
|
262
273
|
return state.visible && state.layout !== null;
|
|
263
274
|
});
|
|
264
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Get all registered elements marked as visible (regardless of layout).
|
|
278
|
+
*
|
|
279
|
+
* Looser than {@link getVisibleElements}: includes elements whose
|
|
280
|
+
* `onLayout` callback has not yet fired (so `layout === null`). React
|
|
281
|
+
* Native's `onLayout` is async — there's a one-tick window after mount
|
|
282
|
+
* where elements are in the tree, the user *sees* them, but the
|
|
283
|
+
* registry hasn't received their measurements yet. Excluding those
|
|
284
|
+
* elements makes `getSnapshot?visibleOnly=true` return 0 on a populated
|
|
285
|
+
* screen, which is a worse failure mode than including a few elements
|
|
286
|
+
* we can't yet locate spatially.
|
|
287
|
+
*
|
|
288
|
+
* Used by `createSnapshot`'s `visibleOnly` filter and by the
|
|
289
|
+
* `getSnapshot` / `getElements` handlers. Each emitted element carries a
|
|
290
|
+
* `visibility` discriminator (`"visible"` / `"likely-visible"`) so
|
|
291
|
+
* agents that *do* require a known rect can branch downstream.
|
|
292
|
+
*/
|
|
293
|
+
getMountedVisibleElements() {
|
|
294
|
+
return this.getAllElements().filter((e) => e.getState().visible);
|
|
295
|
+
}
|
|
265
296
|
/**
|
|
266
297
|
* Get elements registered on a specific route (for page-scoped filtering)
|
|
267
298
|
*/
|
|
@@ -536,7 +567,7 @@ var NativeUIBridgeRegistry = class {
|
|
|
536
567
|
}
|
|
537
568
|
return { currentRoute: null, segments: void 0 };
|
|
538
569
|
})();
|
|
539
|
-
let elements = options?.visibleOnly ? this.
|
|
570
|
+
let elements = options?.visibleOnly ? this.getMountedVisibleElements() : this.getAllElements();
|
|
540
571
|
if (options?.currentRouteOnly && resolvedRoute.currentRoute) {
|
|
541
572
|
const currentRoute = resolvedRoute.currentRoute;
|
|
542
573
|
elements = elements.filter((e) => e.registrationRoute === currentRoute);
|
|
@@ -545,16 +576,19 @@ var NativeUIBridgeRegistry = class {
|
|
|
545
576
|
timestamp: Date.now(),
|
|
546
577
|
elements: elements.map((e) => {
|
|
547
578
|
const handlers = extractHandlerNames(e.props);
|
|
579
|
+
const state = e.getState();
|
|
580
|
+
const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
|
|
548
581
|
return {
|
|
549
582
|
id: e.id,
|
|
550
583
|
type: e.type,
|
|
551
584
|
label: e.label,
|
|
552
585
|
identifier: e.getIdentifier(),
|
|
553
|
-
state
|
|
586
|
+
state,
|
|
554
587
|
actions: e.actions,
|
|
555
588
|
customActions: e.customActions ? Object.keys(e.customActions) : void 0,
|
|
556
589
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
557
|
-
registrationRoute: e.registrationRoute
|
|
590
|
+
registrationRoute: e.registrationRoute,
|
|
591
|
+
visibility
|
|
558
592
|
};
|
|
559
593
|
}),
|
|
560
594
|
components: this.getAllComponents().map((c) => ({
|
|
@@ -1896,22 +1930,25 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1896
1930
|
getElements: async (ctx) => {
|
|
1897
1931
|
const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
|
|
1898
1932
|
const forRoute = ctx.query?.route || ctx.body?.route;
|
|
1899
|
-
let allElements = visibleOnly ? registry.
|
|
1933
|
+
let allElements = visibleOnly ? registry.getMountedVisibleElements() : registry.getAllElements();
|
|
1900
1934
|
if (forRoute) {
|
|
1901
1935
|
allElements = allElements.filter((e) => e.registrationRoute === forRoute);
|
|
1902
1936
|
}
|
|
1903
1937
|
const elements = allElements.map((e) => {
|
|
1904
1938
|
const handlers = extractHandlerNames(e.props);
|
|
1939
|
+
const state = e.getState();
|
|
1940
|
+
const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
|
|
1905
1941
|
return {
|
|
1906
1942
|
id: e.id,
|
|
1907
1943
|
type: e.type,
|
|
1908
1944
|
label: e.label,
|
|
1909
1945
|
identifier: e.getIdentifier(),
|
|
1910
|
-
state
|
|
1946
|
+
state,
|
|
1911
1947
|
actions: e.actions,
|
|
1912
1948
|
customActions: e.customActions ? Object.keys(e.customActions) : void 0,
|
|
1913
1949
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
1914
|
-
registrationRoute: e.registrationRoute
|
|
1950
|
+
registrationRoute: e.registrationRoute,
|
|
1951
|
+
visibility
|
|
1915
1952
|
};
|
|
1916
1953
|
});
|
|
1917
1954
|
return success2({ elements });
|
|
@@ -2166,6 +2203,45 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2166
2203
|
"NOT_SUPPORTED"
|
|
2167
2204
|
);
|
|
2168
2205
|
},
|
|
2206
|
+
// Keep-awake (stub — apps should override via setKeepAwakeProvider, which
|
|
2207
|
+
// the provider wires when a keepAwakeProvider prop is supplied)
|
|
2208
|
+
keepAwake: async () => {
|
|
2209
|
+
return error2(
|
|
2210
|
+
"keep-awake not configured. Provide a keepAwakeProvider to UIBridgeNativeProvider.",
|
|
2211
|
+
"NOT_SUPPORTED"
|
|
2212
|
+
);
|
|
2213
|
+
},
|
|
2214
|
+
// AI helpers — runner-only endpoints that have no React Native analog.
|
|
2215
|
+
//
|
|
2216
|
+
// The mobile bridge mounts explicit NOT_SUPPORTED stubs at the runner's
|
|
2217
|
+
// cheatsheet paths so callers (and the /manual-test skill in particular)
|
|
2218
|
+
// get a structured envelope explaining the gap rather than a confusing
|
|
2219
|
+
// HTTP 404. Each error message names an in-tree mobile replacement so
|
|
2220
|
+
// operators aren't left guessing what to use instead.
|
|
2221
|
+
aiForms: async () => {
|
|
2222
|
+
return error2(
|
|
2223
|
+
'ai/forms (form discovery) is runner-only; mobile React Native has no DOM to walk. Use GET /ui-bridge/control/snapshot and filter for type === "input" instead.',
|
|
2224
|
+
"NOT_SUPPORTED"
|
|
2225
|
+
);
|
|
2226
|
+
},
|
|
2227
|
+
aiIdleStatus: async () => {
|
|
2228
|
+
return error2(
|
|
2229
|
+
'ai/idle-status (page-idle signal) is runner-only; mobile React Native has no DOM-mutation / network-idle signals. Use the registry event stream (WS subscribe to "element:registered"/"element:stateChanged") or poll /control/snapshot for stability.',
|
|
2230
|
+
"NOT_SUPPORTED"
|
|
2231
|
+
);
|
|
2232
|
+
},
|
|
2233
|
+
aiChangeBufferEnable: async () => {
|
|
2234
|
+
return error2(
|
|
2235
|
+
'ai/change-buffer (DOM mutation diff buffer) is runner-only; mobile React Native has no DOM. Use the WS event bridge (subscribe to "element:registered"/"element:unregistered"/"element:stateChanged") for change notifications.',
|
|
2236
|
+
"NOT_SUPPORTED"
|
|
2237
|
+
);
|
|
2238
|
+
},
|
|
2239
|
+
aiWaitForElement: async () => {
|
|
2240
|
+
return error2(
|
|
2241
|
+
'POST /ai/wait-for-element is runner-only on mobile. Use the WS "waitForElement" JSON-RPC method or "waitForCondition" instead \u2014 same semantics, no HTTP polling overhead.',
|
|
2242
|
+
"NOT_SUPPORTED"
|
|
2243
|
+
);
|
|
2244
|
+
},
|
|
2169
2245
|
// Meta / Introspection (stub — server constructor overrides with real route table)
|
|
2170
2246
|
getMethods: async () => {
|
|
2171
2247
|
return error2("Methods introspection not configured", "NOT_SUPPORTED");
|
|
@@ -2821,6 +2897,24 @@ var WS_ROUTES = [
|
|
|
2821
2897
|
{ pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] },
|
|
2822
2898
|
// AI helpers — mobile parity with the runner's `/ui-bridge/ai/*` family.
|
|
2823
2899
|
{ pattern: "ai/fill-form", handler: "fillForm", httpMethods: ["POST"] },
|
|
2900
|
+
// AI helpers — NOT_SUPPORTED stubs. These endpoints exist on the runner-side
|
|
2901
|
+
// UI Bridge surface (and the /manual-test skill's cheatsheet documents them
|
|
2902
|
+
// as if they're universally available). Mobile React Native has no DOM, so
|
|
2903
|
+
// the mobile bridge mounts explicit stubs so callers get a structured
|
|
2904
|
+
// NOT_SUPPORTED envelope (with a hint pointing at the mobile-native
|
|
2905
|
+
// replacement) instead of a confusing HTTP 404.
|
|
2906
|
+
{ pattern: "ai/forms", handler: "aiForms", httpMethods: ["GET"] },
|
|
2907
|
+
{ pattern: "ai/idle-status", handler: "aiIdleStatus", httpMethods: ["GET"] },
|
|
2908
|
+
{
|
|
2909
|
+
pattern: "ai/change-buffer/enable",
|
|
2910
|
+
handler: "aiChangeBufferEnable",
|
|
2911
|
+
httpMethods: ["POST"]
|
|
2912
|
+
},
|
|
2913
|
+
{
|
|
2914
|
+
pattern: "ai/wait-for-element",
|
|
2915
|
+
handler: "aiWaitForElement",
|
|
2916
|
+
httpMethods: ["POST"]
|
|
2917
|
+
},
|
|
2824
2918
|
// Coord-based tap — synthesizes a press at the given screen coords by
|
|
2825
2919
|
// matching against registered elements' layout rects.
|
|
2826
2920
|
{ pattern: "control/tap", handler: "tapAt", httpMethods: ["POST"] },
|
|
@@ -2839,6 +2933,12 @@ var WS_ROUTES = [
|
|
|
2839
2933
|
httpMethods: ["POST"],
|
|
2840
2934
|
requiresTestHooks: true
|
|
2841
2935
|
},
|
|
2936
|
+
{
|
|
2937
|
+
pattern: "control/keep-awake",
|
|
2938
|
+
handler: "keepAwake",
|
|
2939
|
+
httpMethods: ["POST"],
|
|
2940
|
+
requiresTestHooks: true
|
|
2941
|
+
},
|
|
2842
2942
|
// Observability — last-N console errors + network requests. testHooks-gated
|
|
2843
2943
|
// because patching `console.error` / `fetch` is invasive in production.
|
|
2844
2944
|
{
|
|
@@ -2887,11 +2987,16 @@ var HANDLER_DESCRIPTIONS = {
|
|
|
2887
2987
|
diffBaseline: "Diff current UI against a saved baseline",
|
|
2888
2988
|
getMethods: "List every handler name (legacy introspection)",
|
|
2889
2989
|
fillForm: "Fill multiple inputs in one call: body { fields: [{elementId, value}] } \u2014 each field dispatches `type` + `clear:true`",
|
|
2990
|
+
aiForms: 'STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use /control/snapshot + filter type==="input" instead.',
|
|
2991
|
+
aiIdleStatus: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Subscribe to registry events over WS for change signals.",
|
|
2992
|
+
aiChangeBufferEnable: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use WS event subscriptions for change notifications.",
|
|
2993
|
+
aiWaitForElement: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use the WS waitForElement / waitForCondition JSON-RPC methods.",
|
|
2890
2994
|
tapAt: "Synthesize a press at given screen coords by matching registered element layout rects: body { x, y, action? }",
|
|
2891
2995
|
getConsoleErrors: "TEST HOOK \u2014 last-N console.error/console.warn entries; ?since=<ms>&limit=<n>",
|
|
2892
2996
|
getNetworkRequests: "TEST HOOK \u2014 last-N fetch/XHR entries with status + duration; ?since=<ms>&limit=<n>",
|
|
2893
2997
|
pushModal: "TEST HOOK \u2014 push a modal onto the snapshot.modalStack via the registry ModalDetector",
|
|
2894
|
-
dismissModal: "TEST HOOK \u2014 dismiss a modal by id from the snapshot.modalStack via the registry ModalDetector"
|
|
2998
|
+
dismissModal: "TEST HOOK \u2014 dismiss a modal by id from the snapshot.modalStack via the registry ModalDetector",
|
|
2999
|
+
keepAwake: "TEST HOOK \u2014 keep the device screen awake via the injected keepAwakeProvider: body { enabled: boolean, durationMs?: number }"
|
|
2895
3000
|
};
|
|
2896
3001
|
var ACTION_REFERENCE = [
|
|
2897
3002
|
{ name: "press", aliases: ["click"], params: "PressActionParams (optional position)" },
|
|
@@ -3159,6 +3264,35 @@ var NativeUIBridgeServer = class {
|
|
|
3159
3264
|
}
|
|
3160
3265
|
};
|
|
3161
3266
|
}
|
|
3267
|
+
/**
|
|
3268
|
+
* Set a keep-awake provider for native screen-wake control.
|
|
3269
|
+
* This enables `control/keep-awake` and arms the auto-arm hook in
|
|
3270
|
+
* handleRequest (every request keeps the screen alive for 60s).
|
|
3271
|
+
*/
|
|
3272
|
+
setKeepAwakeProvider(provider) {
|
|
3273
|
+
this.keepAwakeProvider = provider;
|
|
3274
|
+
this.handlers.keepAwake = async (ctx) => {
|
|
3275
|
+
const { enabled, durationMs } = ctx.body ?? {};
|
|
3276
|
+
if (typeof enabled !== "boolean") {
|
|
3277
|
+
return {
|
|
3278
|
+
success: false,
|
|
3279
|
+
error: "enabled must be boolean",
|
|
3280
|
+
code: "INVALID_REQUEST",
|
|
3281
|
+
timestamp: Date.now()
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
if (enabled) {
|
|
3285
|
+
provider.request("ui-bridge", typeof durationMs === "number" ? durationMs : void 0);
|
|
3286
|
+
} else {
|
|
3287
|
+
provider.release("ui-bridge");
|
|
3288
|
+
}
|
|
3289
|
+
return {
|
|
3290
|
+
success: true,
|
|
3291
|
+
data: { enabled, durationMs: typeof durationMs === "number" ? durationMs : null },
|
|
3292
|
+
timestamp: Date.now()
|
|
3293
|
+
};
|
|
3294
|
+
};
|
|
3295
|
+
}
|
|
3162
3296
|
/**
|
|
3163
3297
|
* Start the HTTP server
|
|
3164
3298
|
*/
|
|
@@ -3751,6 +3885,12 @@ var NativeUIBridgeServer = class {
|
|
|
3751
3885
|
if (request.method === "OPTIONS") {
|
|
3752
3886
|
return { status: 204, headers, body: "" };
|
|
3753
3887
|
}
|
|
3888
|
+
if (this.config.testHooks === true && this.keepAwakeProvider) {
|
|
3889
|
+
try {
|
|
3890
|
+
this.keepAwakeProvider.request("auto-arm", 6e4);
|
|
3891
|
+
} catch {
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3754
3894
|
try {
|
|
3755
3895
|
const response = await this.routeRequest(request);
|
|
3756
3896
|
return {
|
|
@@ -4651,6 +4791,7 @@ function UIBridgeNativeProvider({
|
|
|
4651
4791
|
serverAdapter,
|
|
4652
4792
|
navigationProvider,
|
|
4653
4793
|
screenshotProvider,
|
|
4794
|
+
keepAwakeProvider,
|
|
4654
4795
|
routeProvider,
|
|
4655
4796
|
cloudRelayConfig,
|
|
4656
4797
|
enableMdnsAnnounce,
|
|
@@ -4705,6 +4846,9 @@ function UIBridgeNativeProvider({
|
|
|
4705
4846
|
if (screenshotProvider) {
|
|
4706
4847
|
server.setScreenshotProvider(screenshotProvider);
|
|
4707
4848
|
}
|
|
4849
|
+
if (keepAwakeProvider) {
|
|
4850
|
+
server.setKeepAwakeProvider(keepAwakeProvider);
|
|
4851
|
+
}
|
|
4708
4852
|
if (routeProvider) {
|
|
4709
4853
|
server.setRouteProvider(routeProvider);
|
|
4710
4854
|
}
|
|
@@ -4758,6 +4902,7 @@ function UIBridgeNativeProvider({
|
|
|
4758
4902
|
serverAdapter,
|
|
4759
4903
|
navigationProvider,
|
|
4760
4904
|
screenshotProvider,
|
|
4905
|
+
keepAwakeProvider,
|
|
4761
4906
|
routeProvider
|
|
4762
4907
|
]);
|
|
4763
4908
|
const stopServer = useCallback(() => {
|
|
@@ -4791,6 +4936,7 @@ function UIBridgeNativeProvider({
|
|
|
4791
4936
|
});
|
|
4792
4937
|
if (navigationProvider) bareServer.setNavigationProvider(navigationProvider);
|
|
4793
4938
|
if (screenshotProvider) bareServer.setScreenshotProvider(screenshotProvider);
|
|
4939
|
+
if (keepAwakeProvider) bareServer.setKeepAwakeProvider(keepAwakeProvider);
|
|
4794
4940
|
if (routeProvider) bareServer.setRouteProvider(routeProvider);
|
|
4795
4941
|
serverRef.current = bareServer;
|
|
4796
4942
|
} catch (err) {
|
|
@@ -4965,6 +5111,24 @@ function flattenStateStyles(stateStyles) {
|
|
|
4965
5111
|
if (stateStyles.disabled != null) result.disabled = flattenStyle(stateStyles.disabled);
|
|
4966
5112
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
4967
5113
|
}
|
|
5114
|
+
var warnedDeadButtonIds = /* @__PURE__ */ new Set();
|
|
5115
|
+
function scheduleDeadButtonWarning(bridge, id, type) {
|
|
5116
|
+
if (type !== "button" && type !== "pressable") return;
|
|
5117
|
+
if (warnedDeadButtonIds.has(id)) return;
|
|
5118
|
+
if (!bridge) return;
|
|
5119
|
+
setTimeout(() => {
|
|
5120
|
+
if (warnedDeadButtonIds.has(id)) return;
|
|
5121
|
+
const element = bridge.registry.getElement(id);
|
|
5122
|
+
if (!element) return;
|
|
5123
|
+
const props = element.props ?? {};
|
|
5124
|
+
const hasPress = typeof props.onPress === "function" || typeof props.onPressIn === "function" || typeof props.onResponderRelease === "function";
|
|
5125
|
+
if (hasPress) return;
|
|
5126
|
+
warnedDeadButtonIds.add(id);
|
|
5127
|
+
console.warn(
|
|
5128
|
+
`[UI Bridge] Registered \`${type}\` element "${id}" without an \`onPress\` handler \u2014 UI Bridge \`press\` actions on this element will fail with "No press handler found on element". Use \`useUIElementWithProps\` and \`captureProps({onPress})\` instead.`
|
|
5129
|
+
);
|
|
5130
|
+
}, 0);
|
|
5131
|
+
}
|
|
4968
5132
|
function useUIElement(options) {
|
|
4969
5133
|
const bridge = useUIBridgeNativeOptional();
|
|
4970
5134
|
const ref = useRef(null);
|
|
@@ -5011,6 +5175,7 @@ function useUIElement(options) {
|
|
|
5011
5175
|
registeredRef.current = true;
|
|
5012
5176
|
registeredIdRef.current = id;
|
|
5013
5177
|
setRegistered(true);
|
|
5178
|
+
scheduleDeadButtonWarning(bridge, id, type);
|
|
5014
5179
|
}, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp]);
|
|
5015
5180
|
const unregister = useCallback(() => {
|
|
5016
5181
|
if (!bridge || !registeredRef.current) return;
|
|
@@ -5070,30 +5235,42 @@ function useUIElement(options) {
|
|
|
5070
5235
|
const node = ref.current;
|
|
5071
5236
|
if (!node || typeof node.measureInWindow !== "function") return;
|
|
5072
5237
|
let cancelled = false;
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5238
|
+
let pendingTimer = null;
|
|
5239
|
+
const backoffMs = [50, 200, 500, 1e3, 2500];
|
|
5240
|
+
let attempt = 0;
|
|
5241
|
+
const tryMeasure = () => {
|
|
5242
|
+
if (cancelled || !node.measureInWindow) return;
|
|
5243
|
+
node.measureInWindow((pageX, pageY, w, h) => {
|
|
5244
|
+
if (cancelled) return;
|
|
5245
|
+
if (w > 0 && h > 0) {
|
|
5246
|
+
const newLayout = {
|
|
5247
|
+
x: pageX,
|
|
5248
|
+
y: pageY,
|
|
5249
|
+
width: w,
|
|
5250
|
+
height: h,
|
|
5251
|
+
pageX,
|
|
5252
|
+
pageY
|
|
5253
|
+
};
|
|
5254
|
+
setLayout(newLayout);
|
|
5255
|
+
const newState = {
|
|
5256
|
+
mounted: true,
|
|
5257
|
+
visible: true,
|
|
5258
|
+
enabled: true,
|
|
5259
|
+
focused: false,
|
|
5260
|
+
layout: newLayout
|
|
5261
|
+
};
|
|
5262
|
+
bridge.registry.updateElementState(id, newState);
|
|
5263
|
+
onStateChangeRef.current?.(newState);
|
|
5264
|
+
return;
|
|
5265
|
+
}
|
|
5266
|
+
if (attempt >= backoffMs.length) return;
|
|
5267
|
+
pendingTimer = setTimeout(tryMeasure, backoffMs[attempt++]);
|
|
5268
|
+
});
|
|
5269
|
+
};
|
|
5270
|
+
tryMeasure();
|
|
5095
5271
|
return () => {
|
|
5096
5272
|
cancelled = true;
|
|
5273
|
+
if (pendingTimer != null) clearTimeout(pendingTimer);
|
|
5097
5274
|
};
|
|
5098
5275
|
}, [bridge, registered, id]);
|
|
5099
5276
|
const registeredHandlerKeysRef = useRef([]);
|