@tamagui/use-element-layout 2.0.0-rc.4 → 2.0.0-rc.40

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamagui/use-element-layout",
3
- "version": "2.0.0-rc.4",
3
+ "version": "2.0.0-rc.40",
4
4
  "gitHead": "a49cc7ea6b93ba384e77a4880ae48ac4a5635c14",
5
5
  "files": [
6
6
  "src",
@@ -16,15 +16,12 @@
16
16
  "./package.json": "./package.json",
17
17
  ".": {
18
18
  "types": "./types/index.d.ts",
19
- "react-native": {
20
- "module": "./dist/esm/index.native.js",
21
- "import": "./dist/esm/index.native.js",
22
- "require": "./dist/cjs/index.native.js"
23
- },
19
+ "react-native": "./dist/esm/index.native.js",
20
+ "browser": "./dist/esm/index.mjs",
24
21
  "module": "./dist/esm/index.mjs",
25
22
  "import": "./dist/esm/index.mjs",
26
23
  "require": "./dist/cjs/index.cjs",
27
- "default": "./dist/cjs/index.native.js"
24
+ "default": "./dist/esm/index.mjs"
28
25
  }
29
26
  },
30
27
  "publishConfig": {
@@ -37,11 +34,11 @@
37
34
  "clean:build": "tamagui-build clean:build"
38
35
  },
39
36
  "dependencies": {
40
- "@tamagui/constants": "2.0.0-rc.4",
41
- "@tamagui/is-equal-shallow": "2.0.0-rc.4"
37
+ "@tamagui/constants": "2.0.0-rc.40",
38
+ "@tamagui/is-equal-shallow": "2.0.0-rc.40"
42
39
  },
43
40
  "devDependencies": {
44
- "@tamagui/build": "2.0.0-rc.4",
41
+ "@tamagui/build": "2.0.0-rc.40",
45
42
  "react": ">=19"
46
43
  },
47
44
  "peerDependencies": {
package/src/index.tsx CHANGED
@@ -1,5 +1,4 @@
1
1
  import { useIsomorphicLayoutEffect } from '@tamagui/constants'
2
- import { isEqualShallow } from '@tamagui/is-equal-shallow'
3
2
  import { createContext, useContext, useId, type ReactNode, type RefObject } from 'react'
4
3
 
5
4
  const LayoutHandlers = new WeakMap<HTMLElement, Function>()
@@ -7,6 +6,24 @@ const LayoutDisableKey = new WeakMap<HTMLElement, string>()
7
6
  const Nodes = new Set<HTMLElement>()
8
7
  const IntersectionState = new WeakMap<HTMLElement, boolean>()
9
8
 
9
+ // feature flag to enable pre-transform dimension reporting (matches RN behavior)
10
+ // can be set via env var at build time or runtime global for testing
11
+ // see: https://github.com/tamagui/tamagui/pull/2329
12
+ const usePretransformDimensions = () =>
13
+ (globalThis as any).__TAMAGUI_ONLAYOUT_PRETRANSFORM === true ||
14
+ process.env.TAMAGUI_ONLAYOUT_PRETRANSFORM === '1'
15
+
16
+ let _debugLayout: boolean | undefined
17
+
18
+ function isDebugLayout() {
19
+ if (_debugLayout === undefined) {
20
+ _debugLayout =
21
+ typeof window !== 'undefined' &&
22
+ new URLSearchParams(window.location.search).has('__tamaDebugLayout')
23
+ }
24
+ return _debugLayout
25
+ }
26
+
10
27
  // separating to avoid all re-rendering
11
28
  const DisableLayoutContextValues: Record<string, boolean> = {}
12
29
  const DisableLayoutContextKey = createContext<string>('')
@@ -37,7 +54,7 @@ export const LayoutMeasurementController = ({
37
54
  )
38
55
  }
39
56
 
40
- // Single persistent IntersectionObserver for all nodes
57
+ // Single persistent IntersectionObserver for visibility tracking
41
58
  let globalIntersectionObserver: IntersectionObserver | null = null
42
59
 
43
60
  type TamaguiComponentStatePartial = {
@@ -70,7 +87,6 @@ export type LayoutEvent = {
70
87
  }
71
88
 
72
89
  const NodeRectCache = new WeakMap<HTMLElement, DOMRect>()
73
- const LastChangeTime = new WeakMap<HTMLElement, number>()
74
90
 
75
91
  // prevent thrashing during first hydration (somewhat, streaming gets trickier)
76
92
  let avoidUpdates = true
@@ -91,12 +107,13 @@ function startGlobalObservers() {
91
107
 
92
108
  globalIntersectionObserver = new IntersectionObserver(
93
109
  (entries) => {
94
- entries.forEach((entry) => {
110
+ for (let i = 0; i < entries.length; i++) {
111
+ const entry = entries[i]
95
112
  const node = entry.target as HTMLElement
96
113
  if (IntersectionState.get(node) !== entry.isIntersecting) {
97
114
  IntersectionState.set(node, entry.isIntersecting)
98
115
  }
99
- })
116
+ }
100
117
  },
101
118
  {
102
119
  threshold: 0,
@@ -104,8 +121,57 @@ function startGlobalObservers() {
104
121
  )
105
122
  }
106
123
 
124
+ // optimization: inline rect comparison to avoid function call overhead on hot path
125
+ function rectsEqual(a: DOMRectReadOnly, b: DOMRectReadOnly): boolean {
126
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height
127
+ }
128
+
107
129
  if (ENABLE) {
108
- const BoundingRects = new WeakMap<any, DOMRectReadOnly | undefined>()
130
+ const BoundingRects = new WeakMap<Element, DOMRectReadOnly>()
131
+
132
+ // optimization: persistent IO for rect fetching, reused across cycles
133
+ let rectFetchObserver: IntersectionObserver | null = null
134
+ let rectFetchResolve: ((value: boolean) => void) | null = null
135
+ let rectFetchStartTime = 0
136
+ let lastCallbackDelay = 0
137
+
138
+ function ensureRectFetchObserver() {
139
+ if (rectFetchObserver) return rectFetchObserver
140
+
141
+ rectFetchObserver = new IntersectionObserver(
142
+ (entries) => {
143
+ lastCallbackDelay = Math.round(performance.now() - rectFetchStartTime)
144
+
145
+ // store all rects
146
+ for (let i = 0; i < entries.length; i++) {
147
+ BoundingRects.set(entries[i].target, entries[i].boundingClientRect)
148
+ }
149
+
150
+ if (
151
+ process.env.NODE_ENV === 'development' &&
152
+ isDebugLayout() &&
153
+ lastCallbackDelay > 50
154
+ ) {
155
+ console.warn(
156
+ '[onLayout-io-delay]',
157
+ lastCallbackDelay + 'ms',
158
+ entries.length,
159
+ 'entries'
160
+ )
161
+ }
162
+
163
+ if (rectFetchResolve) {
164
+ rectFetchResolve(true)
165
+ rectFetchResolve = null
166
+ }
167
+ },
168
+ {
169
+ threshold: 0,
170
+ }
171
+ )
172
+
173
+ return rectFetchObserver
174
+ }
109
175
 
110
176
  async function updateLayoutIfChanged(node: HTMLElement) {
111
177
  const onLayout = LayoutHandlers.get(node)
@@ -114,46 +180,44 @@ if (ENABLE) {
114
180
  const parentNode = node.parentElement
115
181
  if (!parentNode) return
116
182
 
117
- let nodeRect: DOMRectReadOnly
118
- let parentRect: DOMRectReadOnly
183
+ let nodeRect: DOMRectReadOnly | undefined
184
+ let parentRect: DOMRectReadOnly | undefined
119
185
 
186
+ // respect the strategy contract
120
187
  if (strategy === 'async') {
121
- const [nr, pr] = await Promise.all([
122
- BoundingRects.get(node),
123
- BoundingRects.get(parentNode),
124
- ])
188
+ nodeRect = BoundingRects.get(node)
189
+ parentRect = BoundingRects.get(parentNode)
125
190
 
126
- if (!nr || !pr) {
191
+ if (!nodeRect || !parentRect) {
127
192
  return
128
193
  }
129
-
130
- nodeRect = nr
131
- parentRect = pr
132
194
  } else {
133
195
  nodeRect = node.getBoundingClientRect()
134
196
  parentRect = parentNode.getBoundingClientRect()
135
197
  }
136
198
 
137
- if (!nodeRect || !parentRect) {
138
- return
139
- }
140
-
141
199
  const cachedRect = NodeRectCache.get(node)
142
200
  const cachedParentRect = NodeRectCache.get(parentNode)
143
201
 
144
- if (
145
- !cachedRect ||
146
- !cachedParentRect ||
147
- // has changed one rect
148
- // @ts-expect-error DOMRectReadOnly can go into object
149
- !isEqualShallow(cachedRect, nodeRect) ||
150
- // @ts-expect-error DOMRectReadOnly can go into object
151
- !isEqualShallow(cachedParentRect, parentRect)
152
- ) {
153
- NodeRectCache.set(node, nodeRect)
154
- NodeRectCache.set(parentNode, parentRect)
202
+ // optimization: inline comparison instead of isEqualShallow
203
+ const nodeChanged = !cachedRect || !rectsEqual(cachedRect, nodeRect)
204
+ const parentChanged = !cachedParentRect || !rectsEqual(cachedParentRect, parentRect)
155
205
 
156
- const event = getElementLayoutEvent(nodeRect, parentRect)
206
+ if (nodeChanged || parentChanged) {
207
+ NodeRectCache.set(node, nodeRect as DOMRect)
208
+ NodeRectCache.set(parentNode, parentRect as DOMRect)
209
+
210
+ const event = getElementLayoutEvent(nodeRect, parentRect, node)
211
+
212
+ if (process.env.NODE_ENV === 'development' && isDebugLayout()) {
213
+ console.log('[useElementLayout] change', {
214
+ tag: node.tagName,
215
+ id: node.id || undefined,
216
+ className: (node.className || '').slice(0, 60) || undefined,
217
+ layout: event.nativeEvent.layout,
218
+ first: !cachedRect,
219
+ })
220
+ }
157
221
 
158
222
  if (avoidUpdates) {
159
223
  queuedUpdates.set(node, () => onLayout(event))
@@ -163,58 +227,91 @@ if (ENABLE) {
163
227
  }
164
228
  }
165
229
 
166
- // note that getBoundingClientRect() does not thrash layout if its after an animation frame
167
- // ok new note: *if* it needed recalc then yea, but browsers often skip that, so it does
168
- // which is why we use async strategy in general
230
+ const rAF =
231
+ typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame : undefined
169
232
 
233
+ // adaptive frame skipping with backoff
170
234
  const userSkipVal = process.env.TAMAGUI_LAYOUT_FRAME_SKIP
171
- const RUN_EVERY_X_FRAMES = userSkipVal ? +userSkipVal : 14
235
+ const BASE_SKIP_FRAMES = userSkipVal ? +userSkipVal : 10
236
+ const MAX_SKIP_FRAMES = 20
237
+ let skipFrames = BASE_SKIP_FRAMES
238
+ let frameCount = 0
172
239
 
173
240
  async function layoutOnAnimationFrame() {
241
+ // skip frames based on adaptive rate
242
+ if (frameCount++ % skipFrames !== 0) {
243
+ rAF ? rAF(layoutOnAnimationFrame) : setTimeout(layoutOnAnimationFrame, 16)
244
+ return
245
+ }
246
+
247
+ // reset frame count to avoid overflow
248
+ if (frameCount >= Number.MAX_SAFE_INTEGER) {
249
+ frameCount = 0
250
+ }
251
+
174
252
  if (strategy !== 'off') {
175
253
  const visibleNodes: HTMLElement[] = []
254
+ // optimization: deduplicate parent observations
255
+ const parentsToObserve = new Set<HTMLElement>()
256
+
257
+ // collect visible nodes and their unique parents
258
+ for (const node of Nodes) {
259
+ const parentElement = node.parentElement
260
+ if (!(parentElement instanceof HTMLElement)) {
261
+ cleanupNode(node)
262
+ continue
263
+ }
264
+ const disableKey = LayoutDisableKey.get(node)
265
+ if (disableKey && DisableLayoutContextValues[disableKey] === true) continue
266
+ if (IntersectionState.get(node) === false) continue
176
267
 
177
- // do a 1 rather than N IntersectionObservers for performance
178
- const didRun = await new Promise<boolean>((res) => {
179
- const io = new IntersectionObserver(
180
- (entries) => {
181
- io.disconnect()
182
- for (const entry of entries) {
183
- BoundingRects.set(entry.target, entry.boundingClientRect)
184
- }
185
- res(true)
186
- },
187
- {
188
- threshold: 0,
189
- }
190
- )
268
+ visibleNodes.push(node)
269
+ parentsToObserve.add(parentElement)
270
+ }
191
271
 
192
- let didObserve = false
193
-
194
- for (const node of Nodes) {
195
- if (!(node.parentElement instanceof HTMLElement)) continue
196
- const disableKey = LayoutDisableKey.get(node)
197
- if (disableKey && DisableLayoutContextValues[disableKey] === true) continue
198
- if (IntersectionState.get(node) === false) continue
199
- didObserve = true
200
- io.observe(node)
201
- io.observe(node.parentElement)
202
- visibleNodes.push(node)
203
- }
272
+ if (visibleNodes.length > 0) {
273
+ const io = ensureRectFetchObserver()
274
+ rectFetchStartTime = performance.now()
204
275
 
205
- if (!didObserve) {
206
- res(false)
276
+ // observe all nodes
277
+ for (let i = 0; i < visibleNodes.length; i++) {
278
+ io.observe(visibleNodes[i])
279
+ }
280
+ // optimization: observe unique parents only (not N times for N children)
281
+ for (const parent of parentsToObserve) {
282
+ io.observe(parent)
207
283
  }
208
- })
209
284
 
210
- if (didRun) {
211
- visibleNodes.forEach((node) => {
212
- updateLayoutIfChanged(node)
285
+ // wait for callback
286
+ await new Promise<boolean>((res) => {
287
+ rectFetchResolve = res
213
288
  })
289
+
290
+ // unobserve all to reset for next cycle
291
+ for (let i = 0; i < visibleNodes.length; i++) {
292
+ io.unobserve(visibleNodes[i])
293
+ }
294
+ for (const parent of parentsToObserve) {
295
+ io.unobserve(parent)
296
+ }
297
+
298
+ // adaptive backoff: if IO was slow, skip more frames next cycle
299
+ if (lastCallbackDelay > 50) {
300
+ skipFrames = Math.min(skipFrames + 2, MAX_SKIP_FRAMES)
301
+ } else if (lastCallbackDelay < 20) {
302
+ // recover back to base rate when things are fast
303
+ skipFrames = Math.max(skipFrames - 1, BASE_SKIP_FRAMES)
304
+ }
305
+
306
+ // process updates
307
+ for (let i = 0; i < visibleNodes.length; i++) {
308
+ updateLayoutIfChanged(visibleNodes[i])
309
+ }
214
310
  }
215
311
  }
216
312
 
217
- setTimeout(layoutOnAnimationFrame, 16.6667 * RUN_EVERY_X_FRAMES)
313
+ // schedule next frame
314
+ rAF ? rAF(layoutOnAnimationFrame) : setTimeout(layoutOnAnimationFrame, 16)
218
315
  }
219
316
 
220
317
  layoutOnAnimationFrame()
@@ -222,37 +319,125 @@ if (ENABLE) {
222
319
 
223
320
  export const getElementLayoutEvent = (
224
321
  nodeRect: DOMRectReadOnly,
225
- parentRect: DOMRectReadOnly
322
+ parentRect: DOMRectReadOnly,
323
+ node?: HTMLElement
226
324
  ): LayoutEvent => {
227
325
  return {
228
326
  nativeEvent: {
229
- layout: getRelativeDimensions(nodeRect, parentRect),
327
+ layout: getRelativeDimensions(nodeRect, parentRect, node),
230
328
  target: nodeRect,
231
329
  },
232
330
  timeStamp: Date.now(),
233
331
  }
234
332
  }
235
333
 
236
- const getRelativeDimensions = (a: DOMRectReadOnly, b: DOMRectReadOnly) => {
237
- const { height, left, top, width } = a
334
+ /**
335
+ * get pre-transform dimensions for a node.
336
+ * uses offsetWidth/offsetHeight which report CSS layout dimensions
337
+ * unaffected by transforms - this matches React Native's onLayout behavior.
338
+ *
339
+ * see: https://github.com/tamagui/tamagui/pull/2329
340
+ */
341
+ const getPreTransformDimensions = (
342
+ node: HTMLElement
343
+ ): { width: number; height: number } => {
344
+ return {
345
+ width: node.offsetWidth,
346
+ height: node.offsetHeight,
347
+ }
348
+ }
349
+
350
+ const getRelativeDimensions = (
351
+ a: DOMRectReadOnly,
352
+ b: DOMRectReadOnly,
353
+ aNode?: HTMLElement
354
+ ) => {
355
+ const { left, top } = a
238
356
  const x = left - b.left
239
357
  const y = top - b.top
358
+
359
+ // get pre-transform dimensions when flag is enabled and node is available
360
+ const { width, height } =
361
+ usePretransformDimensions() && aNode
362
+ ? getPreTransformDimensions(aNode)
363
+ : { width: a.width, height: a.height }
364
+
240
365
  return { x, y, width, height, pageX: a.left, pageY: a.top }
241
366
  }
242
367
 
368
+ // register an arbitrary DOM element into the measurement loop without React lifecycle
369
+ export function registerLayoutNode(
370
+ node: HTMLElement,
371
+ onChange: () => void,
372
+ disableKey?: string
373
+ ): () => void {
374
+ Nodes.add(node)
375
+ LayoutHandlers.set(node, onChange)
376
+ if (disableKey) LayoutDisableKey.set(node, disableKey)
377
+ startGlobalObservers()
378
+ if (globalIntersectionObserver) {
379
+ globalIntersectionObserver.observe(node)
380
+ IntersectionState.set(node, true)
381
+ }
382
+ return () => cleanupNode(node)
383
+ }
384
+
385
+ function cleanupNode(node: HTMLElement) {
386
+ Nodes.delete(node)
387
+ LayoutHandlers.delete(node)
388
+ LayoutDisableKey.delete(node)
389
+ NodeRectCache.delete(node)
390
+ IntersectionState.delete(node)
391
+ if (globalIntersectionObserver) {
392
+ globalIntersectionObserver.unobserve(node)
393
+ }
394
+ }
395
+
396
+ const PrevHostNode = new WeakMap<object, HTMLElement | undefined>()
397
+
243
398
  export function useElementLayout(
244
399
  ref: RefObject<TamaguiComponentStatePartial>,
245
400
  onLayout?: ((e: LayoutEvent) => void) | null
246
401
  ): void {
247
402
  const disableKey = useContext(DisableLayoutContextKey)
248
403
 
249
- // ensure always up to date so we can avoid re-running effect
404
+ // keep handlers up to date so polling always calls the latest callback
250
405
  const node = ensureWebElement(ref.current?.host)
251
406
  if (node && onLayout) {
252
407
  LayoutHandlers.set(node, onLayout)
253
408
  LayoutDisableKey.set(node, disableKey)
254
409
  }
255
410
 
411
+ // detect host swaps after commit and fire immediate sync layout
412
+ useIsomorphicLayoutEffect(() => {
413
+ if (!onLayout) return
414
+ const nextNode = ensureWebElement(ref.current?.host)
415
+ const prevNode = PrevHostNode.get(ref)
416
+ if (nextNode === prevNode) return
417
+
418
+ if (prevNode) cleanupNode(prevNode)
419
+ PrevHostNode.set(ref, nextNode)
420
+ if (!nextNode) return
421
+
422
+ Nodes.add(nextNode)
423
+ startGlobalObservers()
424
+ if (globalIntersectionObserver) {
425
+ globalIntersectionObserver.observe(nextNode)
426
+ IntersectionState.set(nextNode, true)
427
+ }
428
+
429
+ const handler = LayoutHandlers.get(nextNode)
430
+ if (typeof handler !== 'function') return
431
+ const parentNode = nextNode.parentElement
432
+ if (!parentNode) return
433
+
434
+ const nodeRect = nextNode.getBoundingClientRect()
435
+ const parentRect = parentNode.getBoundingClientRect()
436
+ NodeRectCache.set(nextNode, nodeRect)
437
+ NodeRectCache.set(parentNode, parentRect)
438
+ handler(getElementLayoutEvent(nodeRect, parentRect, nextNode))
439
+ })
440
+
256
441
  useIsomorphicLayoutEffect(() => {
257
442
  if (!onLayout) return
258
443
  const node = ref.current?.host
@@ -260,36 +445,42 @@ export function useElementLayout(
260
445
 
261
446
  Nodes.add(node)
262
447
 
263
- // Add node to intersection observer
264
448
  startGlobalObservers()
265
449
  if (globalIntersectionObserver) {
266
450
  globalIntersectionObserver.observe(node)
267
- // Initialize as intersecting by default
268
451
  IntersectionState.set(node, true)
269
452
  }
270
453
 
271
- // always do one immediate sync layout event no matter the strategy for accuracy
272
- const parentNode = node.parentNode
454
+ if (process.env.NODE_ENV === 'development' && isDebugLayout()) {
455
+ console.log('[useElementLayout] register', {
456
+ tag: node.tagName,
457
+ id: node.id || undefined,
458
+ className: (node.className || '').slice(0, 60) || undefined,
459
+ totalNodes: Nodes.size,
460
+ })
461
+ }
462
+
463
+ // always do one immediate sync layout event for accuracy
464
+ const parentNode = node.parentNode as HTMLElement | null
273
465
  if (parentNode) {
274
466
  onLayout(
275
467
  getElementLayoutEvent(
276
468
  node.getBoundingClientRect(),
277
- parentNode.getBoundingClientRect()
469
+ parentNode.getBoundingClientRect(),
470
+ node
278
471
  )
279
472
  )
280
473
  }
281
474
 
282
475
  return () => {
283
- Nodes.delete(node)
284
- LayoutHandlers.delete(node)
285
- NodeRectCache.delete(node)
286
- LastChangeTime.delete(node)
287
- IntersectionState.delete(node)
288
-
289
- // Remove from intersection observer
290
- if (globalIntersectionObserver) {
291
- globalIntersectionObserver.unobserve(node)
476
+ cleanupNode(node)
477
+
478
+ // also clean up any node from a mid-lifecycle host swap
479
+ const swappedNode = PrevHostNode.get(ref)
480
+ if (swappedNode && swappedNode !== node) {
481
+ cleanupNode(swappedNode)
292
482
  }
483
+ PrevHostNode.delete(ref)
293
484
  }
294
485
  }, [ref, !!onLayout])
295
486
  }
@@ -331,7 +522,7 @@ export const measureNode = async (
331
522
  getBoundingClientRectAsync(relativeNode),
332
523
  ])
333
524
  if (relativeNodeDim && nodeDim) {
334
- return getRelativeDimensions(nodeDim, relativeNodeDim)
525
+ return getRelativeDimensions(nodeDim, relativeNodeDim, node)
335
526
  }
336
527
  }
337
528
  return null
package/types/index.d.ts CHANGED
@@ -24,7 +24,8 @@ export type LayoutEvent = {
24
24
  timeStamp: number;
25
25
  };
26
26
  export declare function enable(): void;
27
- export declare const getElementLayoutEvent: (nodeRect: DOMRectReadOnly, parentRect: DOMRectReadOnly) => LayoutEvent;
27
+ export declare const getElementLayoutEvent: (nodeRect: DOMRectReadOnly, parentRect: DOMRectReadOnly, node?: HTMLElement) => LayoutEvent;
28
+ export declare function registerLayoutNode(node: HTMLElement, onChange: () => void, disableKey?: string): () => void;
28
29
  export declare function useElementLayout(ref: RefObject<TamaguiComponentStatePartial>, onLayout?: ((e: LayoutEvent) => void) | null): void;
29
30
  export declare const getBoundingClientRectAsync: (node: HTMLElement | null) => Promise<DOMRectReadOnly | false>;
30
31
  export declare const measureNode: (node: HTMLElement, relativeTo?: HTMLElement | null) => Promise<null | LayoutValue>;
@@ -1,11 +1,11 @@
1
1
  {
2
- "mappings": "AAEA,cAAgD,gBAAgB,iBAAiB;AAiBjF,OAAO,cAAM,8BAA+B,EAC1C,SACA,YACC;CACD;CACA,UAAU;MACR;KAiBC,+BAA+B;CAClC;;KAGG,4BAA4B,QAAQ,SAAS;AAIlD,OAAO,iBAAS,oBAAoB,OAAO;AAI3C,YAAY,cAAc;CACxB;CACA;CACA;CACA;CACA;CACA;;AAGF,YAAY,cAAc;CACxB,aAAa;EACX,QAAQ;EACR;;CAEF;;AAUF,OAAO,iBAAS;AAgJhB,OAAO,cAAM,wBACX,UAAU,iBACV,YAAY,oBACX;AAiBH,OAAO,iBAAS,iBACd,KAAK,UAAU,+BACf,aAAa,GAAG;AA2DlB,OAAO,cAAM,6BACX,MAAM,uBACL,QAAQ,kBAAkB;AAiB7B,OAAO,cAAM,cACX,MAAM,aACN,aAAa,uBACZ,eAAe;KAcb,qBAAqB,WAAW,WAAW,eAAe;KAE1D,aACH,WACA,WACA,eACA,gBACA,eACA;AAGF,OAAO,cAAM,UACX,MAAM,aACN,UAAU,cACT,QAAQ;AAWX,OAAO,iBAAS,cACd,MAAM,eACJ,UAAU,cAAc,QAAQ;KAI/B,eAAe;CAAE;CAAe;CAAe;CAAe;;AAEnE,OAAO,cAAM,kBACX,MAAM,aACN,UAAU,sBACT,QAAQ;AAQX,OAAO,cAAM,wBACX,MAAM,kBACH,UAAU,sBAAsB,QAAQ;AAI7C,OAAO,cAAM,gBACX,MAAM,aACN,cAAc,aACd,UAAU,cACT,QAAQ;AAQX,OAAO,iBAAS,oBACd,MAAM,eACJ,YAAY,aAAa,UAAU,cAAc,QAAQ",
2
+ "mappings": "AACA,cAAgD,gBAAgB,iBAAiB;AAmCjF,OAAO,cAAM,8BAA+B,EAC1C,SACA,YACC;CACD;CACA,UAAU;MACR;KAiBC,+BAA+B;CAClC;;KAGG,4BAA4B,QAAQ,SAAS;AAIlD,OAAO,iBAAS,oBAAoB,OAAO;AAI3C,YAAY,cAAc;CACxB;CACA;CACA;CACA;CACA;CACA;;AAGF,YAAY,cAAc;CACxB,aAAa;EACX,QAAQ;EACR;;CAEF;;AASF,OAAO,iBAAS;AAiOhB,OAAO,cAAM,wBACX,UAAU,iBACV,YAAY,iBACZ,OAAO,gBACN;AA6CH,OAAO,iBAAS,mBACd,MAAM,aACN,sBACA;AA0BF,OAAO,iBAAS,iBACd,KAAK,UAAU,+BACf,aAAa,GAAG;AA+FlB,OAAO,cAAM,6BACX,MAAM,uBACL,QAAQ,kBAAkB;AAiB7B,OAAO,cAAM,cACX,MAAM,aACN,aAAa,uBACZ,eAAe;KAcb,qBAAqB,WAAW,WAAW,eAAe;KAE1D,aACH,WACA,WACA,eACA,gBACA,eACA;AAGF,OAAO,cAAM,UACX,MAAM,aACN,UAAU,cACT,QAAQ;AAWX,OAAO,iBAAS,cACd,MAAM,eACJ,UAAU,cAAc,QAAQ;KAI/B,eAAe;CAAE;CAAe;CAAe;CAAe;;AAEnE,OAAO,cAAM,kBACX,MAAM,aACN,UAAU,sBACT,QAAQ;AAQX,OAAO,cAAM,wBACX,MAAM,kBACH,UAAU,sBAAsB,QAAQ;AAI7C,OAAO,cAAM,gBACX,MAAM,aACN,cAAc,aACd,UAAU,cACT,QAAQ;AAQX,OAAO,iBAAS,oBACd,MAAM,eACJ,YAAY,aAAa,UAAU,cAAc,QAAQ",
3
3
  "names": [],
4
4
  "sources": [
5
5
  "src/index.tsx"
6
6
  ],
7
+ "version": 3,
7
8
  "sourcesContent": [
8
- "import { useIsomorphicLayoutEffect } from '@tamagui/constants'\nimport { isEqualShallow } from '@tamagui/is-equal-shallow'\nimport { createContext, useContext, useId, type ReactNode, type RefObject } from 'react'\n\nconst LayoutHandlers = new WeakMap<HTMLElement, Function>()\nconst LayoutDisableKey = new WeakMap<HTMLElement, string>()\nconst Nodes = new Set<HTMLElement>()\nconst IntersectionState = new WeakMap<HTMLElement, boolean>()\n\n// separating to avoid all re-rendering\nconst DisableLayoutContextValues: Record<string, boolean> = {}\nconst DisableLayoutContextKey = createContext<string>('')\n\nconst ENABLE =\n process.env.TAMAGUI_TARGET === 'web' && typeof IntersectionObserver !== 'undefined'\n\n// internal testing - advanced helper to turn off layout measurement for extra performance\n// TODO document!\n// TODO could add frame skip control here\nexport const LayoutMeasurementController = ({\n disable,\n children,\n}: {\n disable: boolean\n children: ReactNode\n}): ReactNode => {\n const id = useId()\n\n useIsomorphicLayoutEffect(() => {\n DisableLayoutContextValues[id] = disable\n }, [disable, id])\n\n return (\n <DisableLayoutContextKey.Provider value={id}>\n {children}\n </DisableLayoutContextKey.Provider>\n )\n}\n\n// Single persistent IntersectionObserver for all nodes\nlet globalIntersectionObserver: IntersectionObserver | null = null\n\ntype TamaguiComponentStatePartial = {\n host?: any\n}\n\ntype LayoutMeasurementStrategy = 'off' | 'sync' | 'async'\n\nlet strategy: LayoutMeasurementStrategy = 'async'\n\nexport function setOnLayoutStrategy(state: LayoutMeasurementStrategy): void {\n strategy = state\n}\n\nexport type LayoutValue = {\n x: number\n y: number\n width: number\n height: number\n pageX: number\n pageY: number\n}\n\nexport type LayoutEvent = {\n nativeEvent: {\n layout: LayoutValue\n target: any\n }\n timeStamp: number\n}\n\nconst NodeRectCache = new WeakMap<HTMLElement, DOMRect>()\nconst LastChangeTime = new WeakMap<HTMLElement, number>()\n\n// prevent thrashing during first hydration (somewhat, streaming gets trickier)\nlet avoidUpdates = true\nconst queuedUpdates = new Map<HTMLElement, Function>()\n\nexport function enable(): void {\n if (avoidUpdates) {\n avoidUpdates = false\n if (queuedUpdates) {\n queuedUpdates.forEach((cb) => cb())\n queuedUpdates.clear()\n }\n }\n}\n\nfunction startGlobalObservers() {\n if (!ENABLE || globalIntersectionObserver) return\n\n globalIntersectionObserver = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n const node = entry.target as HTMLElement\n if (IntersectionState.get(node) !== entry.isIntersecting) {\n IntersectionState.set(node, entry.isIntersecting)\n }\n })\n },\n {\n threshold: 0,\n }\n )\n}\n\nif (ENABLE) {\n const BoundingRects = new WeakMap<any, DOMRectReadOnly | undefined>()\n\n async function updateLayoutIfChanged(node: HTMLElement) {\n const onLayout = LayoutHandlers.get(node)\n if (typeof onLayout !== 'function') return\n\n const parentNode = node.parentElement\n if (!parentNode) return\n\n let nodeRect: DOMRectReadOnly\n let parentRect: DOMRectReadOnly\n\n if (strategy === 'async') {\n const [nr, pr] = await Promise.all([\n BoundingRects.get(node),\n BoundingRects.get(parentNode),\n ])\n\n if (!nr || !pr) {\n return\n }\n\n nodeRect = nr\n parentRect = pr\n } else {\n nodeRect = node.getBoundingClientRect()\n parentRect = parentNode.getBoundingClientRect()\n }\n\n if (!nodeRect || !parentRect) {\n return\n }\n\n const cachedRect = NodeRectCache.get(node)\n const cachedParentRect = NodeRectCache.get(parentNode)\n\n if (\n !cachedRect ||\n !cachedParentRect ||\n // has changed one rect\n // @ts-expect-error DOMRectReadOnly can go into object\n !isEqualShallow(cachedRect, nodeRect) ||\n // @ts-expect-error DOMRectReadOnly can go into object\n !isEqualShallow(cachedParentRect, parentRect)\n ) {\n NodeRectCache.set(node, nodeRect)\n NodeRectCache.set(parentNode, parentRect)\n\n const event = getElementLayoutEvent(nodeRect, parentRect)\n\n if (avoidUpdates) {\n queuedUpdates.set(node, () => onLayout(event))\n } else {\n onLayout(event)\n }\n }\n }\n\n // note that getBoundingClientRect() does not thrash layout if its after an animation frame\n // ok new note: *if* it needed recalc then yea, but browsers often skip that, so it does\n // which is why we use async strategy in general\n\n const userSkipVal = process.env.TAMAGUI_LAYOUT_FRAME_SKIP\n const RUN_EVERY_X_FRAMES = userSkipVal ? +userSkipVal : 14\n\n async function layoutOnAnimationFrame() {\n if (strategy !== 'off') {\n const visibleNodes: HTMLElement[] = []\n\n // do a 1 rather than N IntersectionObservers for performance\n const didRun = await new Promise<boolean>((res) => {\n const io = new IntersectionObserver(\n (entries) => {\n io.disconnect()\n for (const entry of entries) {\n BoundingRects.set(entry.target, entry.boundingClientRect)\n }\n res(true)\n },\n {\n threshold: 0,\n }\n )\n\n let didObserve = false\n\n for (const node of Nodes) {\n if (!(node.parentElement instanceof HTMLElement)) continue\n const disableKey = LayoutDisableKey.get(node)\n if (disableKey && DisableLayoutContextValues[disableKey] === true) continue\n if (IntersectionState.get(node) === false) continue\n didObserve = true\n io.observe(node)\n io.observe(node.parentElement)\n visibleNodes.push(node)\n }\n\n if (!didObserve) {\n res(false)\n }\n })\n\n if (didRun) {\n visibleNodes.forEach((node) => {\n updateLayoutIfChanged(node)\n })\n }\n }\n\n setTimeout(layoutOnAnimationFrame, 16.6667 * RUN_EVERY_X_FRAMES)\n }\n\n layoutOnAnimationFrame()\n}\n\nexport const getElementLayoutEvent = (\n nodeRect: DOMRectReadOnly,\n parentRect: DOMRectReadOnly\n): LayoutEvent => {\n return {\n nativeEvent: {\n layout: getRelativeDimensions(nodeRect, parentRect),\n target: nodeRect,\n },\n timeStamp: Date.now(),\n }\n}\n\nconst getRelativeDimensions = (a: DOMRectReadOnly, b: DOMRectReadOnly) => {\n const { height, left, top, width } = a\n const x = left - b.left\n const y = top - b.top\n return { x, y, width, height, pageX: a.left, pageY: a.top }\n}\n\nexport function useElementLayout(\n ref: RefObject<TamaguiComponentStatePartial>,\n onLayout?: ((e: LayoutEvent) => void) | null\n): void {\n const disableKey = useContext(DisableLayoutContextKey)\n\n // ensure always up to date so we can avoid re-running effect\n const node = ensureWebElement(ref.current?.host)\n if (node && onLayout) {\n LayoutHandlers.set(node, onLayout)\n LayoutDisableKey.set(node, disableKey)\n }\n\n useIsomorphicLayoutEffect(() => {\n if (!onLayout) return\n const node = ref.current?.host\n if (!node) return\n\n Nodes.add(node)\n\n // Add node to intersection observer\n startGlobalObservers()\n if (globalIntersectionObserver) {\n globalIntersectionObserver.observe(node)\n // Initialize as intersecting by default\n IntersectionState.set(node, true)\n }\n\n // always do one immediate sync layout event no matter the strategy for accuracy\n const parentNode = node.parentNode\n if (parentNode) {\n onLayout(\n getElementLayoutEvent(\n node.getBoundingClientRect(),\n parentNode.getBoundingClientRect()\n )\n )\n }\n\n return () => {\n Nodes.delete(node)\n LayoutHandlers.delete(node)\n NodeRectCache.delete(node)\n LastChangeTime.delete(node)\n IntersectionState.delete(node)\n\n // Remove from intersection observer\n if (globalIntersectionObserver) {\n globalIntersectionObserver.unobserve(node)\n }\n }\n }, [ref, !!onLayout])\n}\n\nfunction ensureWebElement<X>(x: X): HTMLElement | undefined {\n if (typeof HTMLElement === 'undefined') {\n return undefined\n }\n return x instanceof HTMLElement ? x : undefined\n}\n\nexport const getBoundingClientRectAsync = (\n node: HTMLElement | null\n): Promise<DOMRectReadOnly | false> => {\n return new Promise<DOMRectReadOnly | false>((res) => {\n if (!node || node.nodeType !== 1) return res(false)\n\n const io = new IntersectionObserver(\n (entries) => {\n io.disconnect()\n return res(entries[0].boundingClientRect)\n },\n {\n threshold: 0,\n }\n )\n io.observe(node)\n })\n}\n\nexport const measureNode = async (\n node: HTMLElement,\n relativeTo?: HTMLElement | null\n): Promise<null | LayoutValue> => {\n const relativeNode = relativeTo || node?.parentElement\n if (relativeNode instanceof HTMLElement) {\n const [nodeDim, relativeNodeDim] = await Promise.all([\n getBoundingClientRectAsync(node),\n getBoundingClientRectAsync(relativeNode),\n ])\n if (relativeNodeDim && nodeDim) {\n return getRelativeDimensions(nodeDim, relativeNodeDim)\n }\n }\n return null\n}\n\ntype MeasureInWindowCb = (x: number, y: number, width: number, height: number) => void\n\ntype MeasureCb = (\n x: number,\n y: number,\n width: number,\n height: number,\n pageX: number,\n pageY: number\n) => void\n\nexport const measure = async (\n node: HTMLElement,\n callback: MeasureCb\n): Promise<LayoutValue | null> => {\n const out = await measureNode(\n node,\n node.parentNode instanceof HTMLElement ? node.parentNode : null\n )\n if (out) {\n callback?.(out.x, out.y, out.width, out.height, out.pageX, out.pageY)\n }\n return out\n}\n\nexport function createMeasure(\n node: HTMLElement\n): (callback: MeasureCb) => Promise<LayoutValue | null> {\n return (callback) => measure(node, callback)\n}\n\ntype WindowLayout = { pageX: number; pageY: number; width: number; height: number }\n\nexport const measureInWindow = async (\n node: HTMLElement,\n callback: MeasureInWindowCb\n): Promise<WindowLayout | null> => {\n const out = await measureNode(node, null)\n if (out) {\n callback?.(out.pageX, out.pageY, out.width, out.height)\n }\n return out\n}\n\nexport const createMeasureInWindow = (\n node: HTMLElement\n): ((callback: MeasureInWindowCb) => Promise<WindowLayout | null>) => {\n return (callback) => measureInWindow(node, callback)\n}\n\nexport const measureLayout = async (\n node: HTMLElement,\n relativeNode: HTMLElement,\n callback: MeasureCb\n): Promise<LayoutValue | null> => {\n const out = await measureNode(node, relativeNode)\n if (out) {\n callback?.(out.x, out.y, out.width, out.height, out.pageX, out.pageY)\n }\n return out\n}\n\nexport function createMeasureLayout(\n node: HTMLElement\n): (relativeTo: HTMLElement, callback: MeasureCb) => Promise<LayoutValue | null> {\n return (relativeTo, callback) => measureLayout(node, relativeTo, callback)\n}\n"
9
- ],
10
- "version": 3
9
+ "import { useIsomorphicLayoutEffect } from '@tamagui/constants'\nimport { createContext, useContext, useId, type ReactNode, type RefObject } from 'react'\n\nconst LayoutHandlers = new WeakMap<HTMLElement, Function>()\nconst LayoutDisableKey = new WeakMap<HTMLElement, string>()\nconst Nodes = new Set<HTMLElement>()\nconst IntersectionState = new WeakMap<HTMLElement, boolean>()\n\n// feature flag to enable pre-transform dimension reporting (matches RN behavior)\n// can be set via env var at build time or runtime global for testing\n// see: https://github.com/tamagui/tamagui/pull/2329\nconst usePretransformDimensions = () =>\n (globalThis as any).__TAMAGUI_ONLAYOUT_PRETRANSFORM === true ||\n process.env.TAMAGUI_ONLAYOUT_PRETRANSFORM === '1'\n\nlet _debugLayout: boolean | undefined\n\nfunction isDebugLayout() {\n if (_debugLayout === undefined) {\n _debugLayout =\n typeof window !== 'undefined' &&\n new URLSearchParams(window.location.search).has('__tamaDebugLayout')\n }\n return _debugLayout\n}\n\n// separating to avoid all re-rendering\nconst DisableLayoutContextValues: Record<string, boolean> = {}\nconst DisableLayoutContextKey = createContext<string>('')\n\nconst ENABLE =\n process.env.TAMAGUI_TARGET === 'web' && typeof IntersectionObserver !== 'undefined'\n\n// internal testing - advanced helper to turn off layout measurement for extra performance\n// TODO document!\n// TODO could add frame skip control here\nexport const LayoutMeasurementController = ({\n disable,\n children,\n}: {\n disable: boolean\n children: ReactNode\n}): ReactNode => {\n const id = useId()\n\n useIsomorphicLayoutEffect(() => {\n DisableLayoutContextValues[id] = disable\n }, [disable, id])\n\n return (\n <DisableLayoutContextKey.Provider value={id}>\n {children}\n </DisableLayoutContextKey.Provider>\n )\n}\n\n// Single persistent IntersectionObserver for visibility tracking\nlet globalIntersectionObserver: IntersectionObserver | null = null\n\ntype TamaguiComponentStatePartial = {\n host?: any\n}\n\ntype LayoutMeasurementStrategy = 'off' | 'sync' | 'async'\n\nlet strategy: LayoutMeasurementStrategy = 'async'\n\nexport function setOnLayoutStrategy(state: LayoutMeasurementStrategy): void {\n strategy = state\n}\n\nexport type LayoutValue = {\n x: number\n y: number\n width: number\n height: number\n pageX: number\n pageY: number\n}\n\nexport type LayoutEvent = {\n nativeEvent: {\n layout: LayoutValue\n target: any\n }\n timeStamp: number\n}\n\nconst NodeRectCache = new WeakMap<HTMLElement, DOMRect>()\n\n// prevent thrashing during first hydration (somewhat, streaming gets trickier)\nlet avoidUpdates = true\nconst queuedUpdates = new Map<HTMLElement, Function>()\n\nexport function enable(): void {\n if (avoidUpdates) {\n avoidUpdates = false\n if (queuedUpdates) {\n queuedUpdates.forEach((cb) => cb())\n queuedUpdates.clear()\n }\n }\n}\n\nfunction startGlobalObservers() {\n if (!ENABLE || globalIntersectionObserver) return\n\n globalIntersectionObserver = new IntersectionObserver(\n (entries) => {\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i]\n const node = entry.target as HTMLElement\n if (IntersectionState.get(node) !== entry.isIntersecting) {\n IntersectionState.set(node, entry.isIntersecting)\n }\n }\n },\n {\n threshold: 0,\n }\n )\n}\n\n// optimization: inline rect comparison to avoid function call overhead on hot path\nfunction rectsEqual(a: DOMRectReadOnly, b: DOMRectReadOnly): boolean {\n return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height\n}\n\nif (ENABLE) {\n const BoundingRects = new WeakMap<Element, DOMRectReadOnly>()\n\n // optimization: persistent IO for rect fetching, reused across cycles\n let rectFetchObserver: IntersectionObserver | null = null\n let rectFetchResolve: ((value: boolean) => void) | null = null\n let rectFetchStartTime = 0\n let lastCallbackDelay = 0\n\n function ensureRectFetchObserver() {\n if (rectFetchObserver) return rectFetchObserver\n\n rectFetchObserver = new IntersectionObserver(\n (entries) => {\n lastCallbackDelay = Math.round(performance.now() - rectFetchStartTime)\n\n // store all rects\n for (let i = 0; i < entries.length; i++) {\n BoundingRects.set(entries[i].target, entries[i].boundingClientRect)\n }\n\n if (\n process.env.NODE_ENV === 'development' &&\n isDebugLayout() &&\n lastCallbackDelay > 50\n ) {\n console.warn(\n '[onLayout-io-delay]',\n lastCallbackDelay + 'ms',\n entries.length,\n 'entries'\n )\n }\n\n if (rectFetchResolve) {\n rectFetchResolve(true)\n rectFetchResolve = null\n }\n },\n {\n threshold: 0,\n }\n )\n\n return rectFetchObserver\n }\n\n async function updateLayoutIfChanged(node: HTMLElement) {\n const onLayout = LayoutHandlers.get(node)\n if (typeof onLayout !== 'function') return\n\n const parentNode = node.parentElement\n if (!parentNode) return\n\n let nodeRect: DOMRectReadOnly | undefined\n let parentRect: DOMRectReadOnly | undefined\n\n // respect the strategy contract\n if (strategy === 'async') {\n nodeRect = BoundingRects.get(node)\n parentRect = BoundingRects.get(parentNode)\n\n if (!nodeRect || !parentRect) {\n return\n }\n } else {\n nodeRect = node.getBoundingClientRect()\n parentRect = parentNode.getBoundingClientRect()\n }\n\n const cachedRect = NodeRectCache.get(node)\n const cachedParentRect = NodeRectCache.get(parentNode)\n\n // optimization: inline comparison instead of isEqualShallow\n const nodeChanged = !cachedRect || !rectsEqual(cachedRect, nodeRect)\n const parentChanged = !cachedParentRect || !rectsEqual(cachedParentRect, parentRect)\n\n if (nodeChanged || parentChanged) {\n NodeRectCache.set(node, nodeRect as DOMRect)\n NodeRectCache.set(parentNode, parentRect as DOMRect)\n\n const event = getElementLayoutEvent(nodeRect, parentRect, node)\n\n if (process.env.NODE_ENV === 'development' && isDebugLayout()) {\n console.log('[useElementLayout] change', {\n tag: node.tagName,\n id: node.id || undefined,\n className: (node.className || '').slice(0, 60) || undefined,\n layout: event.nativeEvent.layout,\n first: !cachedRect,\n })\n }\n\n if (avoidUpdates) {\n queuedUpdates.set(node, () => onLayout(event))\n } else {\n onLayout(event)\n }\n }\n }\n\n const rAF =\n typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame : undefined\n\n // adaptive frame skipping with backoff\n const userSkipVal = process.env.TAMAGUI_LAYOUT_FRAME_SKIP\n const BASE_SKIP_FRAMES = userSkipVal ? +userSkipVal : 10\n const MAX_SKIP_FRAMES = 20\n let skipFrames = BASE_SKIP_FRAMES\n let frameCount = 0\n\n async function layoutOnAnimationFrame() {\n // skip frames based on adaptive rate\n if (frameCount++ % skipFrames !== 0) {\n rAF ? rAF(layoutOnAnimationFrame) : setTimeout(layoutOnAnimationFrame, 16)\n return\n }\n\n // reset frame count to avoid overflow\n if (frameCount >= Number.MAX_SAFE_INTEGER) {\n frameCount = 0\n }\n\n if (strategy !== 'off') {\n const visibleNodes: HTMLElement[] = []\n // optimization: deduplicate parent observations\n const parentsToObserve = new Set<HTMLElement>()\n\n // collect visible nodes and their unique parents\n for (const node of Nodes) {\n const parentElement = node.parentElement\n if (!(parentElement instanceof HTMLElement)) {\n cleanupNode(node)\n continue\n }\n const disableKey = LayoutDisableKey.get(node)\n if (disableKey && DisableLayoutContextValues[disableKey] === true) continue\n if (IntersectionState.get(node) === false) continue\n\n visibleNodes.push(node)\n parentsToObserve.add(parentElement)\n }\n\n if (visibleNodes.length > 0) {\n const io = ensureRectFetchObserver()\n rectFetchStartTime = performance.now()\n\n // observe all nodes\n for (let i = 0; i < visibleNodes.length; i++) {\n io.observe(visibleNodes[i])\n }\n // optimization: observe unique parents only (not N times for N children)\n for (const parent of parentsToObserve) {\n io.observe(parent)\n }\n\n // wait for callback\n await new Promise<boolean>((res) => {\n rectFetchResolve = res\n })\n\n // unobserve all to reset for next cycle\n for (let i = 0; i < visibleNodes.length; i++) {\n io.unobserve(visibleNodes[i])\n }\n for (const parent of parentsToObserve) {\n io.unobserve(parent)\n }\n\n // adaptive backoff: if IO was slow, skip more frames next cycle\n if (lastCallbackDelay > 50) {\n skipFrames = Math.min(skipFrames + 2, MAX_SKIP_FRAMES)\n } else if (lastCallbackDelay < 20) {\n // recover back to base rate when things are fast\n skipFrames = Math.max(skipFrames - 1, BASE_SKIP_FRAMES)\n }\n\n // process updates\n for (let i = 0; i < visibleNodes.length; i++) {\n updateLayoutIfChanged(visibleNodes[i])\n }\n }\n }\n\n // schedule next frame\n rAF ? rAF(layoutOnAnimationFrame) : setTimeout(layoutOnAnimationFrame, 16)\n }\n\n layoutOnAnimationFrame()\n}\n\nexport const getElementLayoutEvent = (\n nodeRect: DOMRectReadOnly,\n parentRect: DOMRectReadOnly,\n node?: HTMLElement\n): LayoutEvent => {\n return {\n nativeEvent: {\n layout: getRelativeDimensions(nodeRect, parentRect, node),\n target: nodeRect,\n },\n timeStamp: Date.now(),\n }\n}\n\n/**\n * get pre-transform dimensions for a node.\n * uses offsetWidth/offsetHeight which report CSS layout dimensions\n * unaffected by transforms - this matches React Native's onLayout behavior.\n *\n * see: https://github.com/tamagui/tamagui/pull/2329\n */\nconst getPreTransformDimensions = (\n node: HTMLElement\n): { width: number; height: number } => {\n return {\n width: node.offsetWidth,\n height: node.offsetHeight,\n }\n}\n\nconst getRelativeDimensions = (\n a: DOMRectReadOnly,\n b: DOMRectReadOnly,\n aNode?: HTMLElement\n) => {\n const { left, top } = a\n const x = left - b.left\n const y = top - b.top\n\n // get pre-transform dimensions when flag is enabled and node is available\n const { width, height } =\n usePretransformDimensions() && aNode\n ? getPreTransformDimensions(aNode)\n : { width: a.width, height: a.height }\n\n return { x, y, width, height, pageX: a.left, pageY: a.top }\n}\n\n// register an arbitrary DOM element into the measurement loop without React lifecycle\nexport function registerLayoutNode(\n node: HTMLElement,\n onChange: () => void,\n disableKey?: string\n): () => void {\n Nodes.add(node)\n LayoutHandlers.set(node, onChange)\n if (disableKey) LayoutDisableKey.set(node, disableKey)\n startGlobalObservers()\n if (globalIntersectionObserver) {\n globalIntersectionObserver.observe(node)\n IntersectionState.set(node, true)\n }\n return () => cleanupNode(node)\n}\n\nfunction cleanupNode(node: HTMLElement) {\n Nodes.delete(node)\n LayoutHandlers.delete(node)\n LayoutDisableKey.delete(node)\n NodeRectCache.delete(node)\n IntersectionState.delete(node)\n if (globalIntersectionObserver) {\n globalIntersectionObserver.unobserve(node)\n }\n}\n\nconst PrevHostNode = new WeakMap<object, HTMLElement | undefined>()\n\nexport function useElementLayout(\n ref: RefObject<TamaguiComponentStatePartial>,\n onLayout?: ((e: LayoutEvent) => void) | null\n): void {\n const disableKey = useContext(DisableLayoutContextKey)\n\n // keep handlers up to date so polling always calls the latest callback\n const node = ensureWebElement(ref.current?.host)\n if (node && onLayout) {\n LayoutHandlers.set(node, onLayout)\n LayoutDisableKey.set(node, disableKey)\n }\n\n // detect host swaps after commit and fire immediate sync layout\n useIsomorphicLayoutEffect(() => {\n if (!onLayout) return\n const nextNode = ensureWebElement(ref.current?.host)\n const prevNode = PrevHostNode.get(ref)\n if (nextNode === prevNode) return\n\n if (prevNode) cleanupNode(prevNode)\n PrevHostNode.set(ref, nextNode)\n if (!nextNode) return\n\n Nodes.add(nextNode)\n startGlobalObservers()\n if (globalIntersectionObserver) {\n globalIntersectionObserver.observe(nextNode)\n IntersectionState.set(nextNode, true)\n }\n\n const handler = LayoutHandlers.get(nextNode)\n if (typeof handler !== 'function') return\n const parentNode = nextNode.parentElement\n if (!parentNode) return\n\n const nodeRect = nextNode.getBoundingClientRect()\n const parentRect = parentNode.getBoundingClientRect()\n NodeRectCache.set(nextNode, nodeRect)\n NodeRectCache.set(parentNode, parentRect)\n handler(getElementLayoutEvent(nodeRect, parentRect, nextNode))\n })\n\n useIsomorphicLayoutEffect(() => {\n if (!onLayout) return\n const node = ref.current?.host\n if (!node) return\n\n Nodes.add(node)\n\n startGlobalObservers()\n if (globalIntersectionObserver) {\n globalIntersectionObserver.observe(node)\n IntersectionState.set(node, true)\n }\n\n if (process.env.NODE_ENV === 'development' && isDebugLayout()) {\n console.log('[useElementLayout] register', {\n tag: node.tagName,\n id: node.id || undefined,\n className: (node.className || '').slice(0, 60) || undefined,\n totalNodes: Nodes.size,\n })\n }\n\n // always do one immediate sync layout event for accuracy\n const parentNode = node.parentNode as HTMLElement | null\n if (parentNode) {\n onLayout(\n getElementLayoutEvent(\n node.getBoundingClientRect(),\n parentNode.getBoundingClientRect(),\n node\n )\n )\n }\n\n return () => {\n cleanupNode(node)\n\n // also clean up any node from a mid-lifecycle host swap\n const swappedNode = PrevHostNode.get(ref)\n if (swappedNode && swappedNode !== node) {\n cleanupNode(swappedNode)\n }\n PrevHostNode.delete(ref)\n }\n }, [ref, !!onLayout])\n}\n\nfunction ensureWebElement<X>(x: X): HTMLElement | undefined {\n if (typeof HTMLElement === 'undefined') {\n return undefined\n }\n return x instanceof HTMLElement ? x : undefined\n}\n\nexport const getBoundingClientRectAsync = (\n node: HTMLElement | null\n): Promise<DOMRectReadOnly | false> => {\n return new Promise<DOMRectReadOnly | false>((res) => {\n if (!node || node.nodeType !== 1) return res(false)\n\n const io = new IntersectionObserver(\n (entries) => {\n io.disconnect()\n return res(entries[0].boundingClientRect)\n },\n {\n threshold: 0,\n }\n )\n io.observe(node)\n })\n}\n\nexport const measureNode = async (\n node: HTMLElement,\n relativeTo?: HTMLElement | null\n): Promise<null | LayoutValue> => {\n const relativeNode = relativeTo || node?.parentElement\n if (relativeNode instanceof HTMLElement) {\n const [nodeDim, relativeNodeDim] = await Promise.all([\n getBoundingClientRectAsync(node),\n getBoundingClientRectAsync(relativeNode),\n ])\n if (relativeNodeDim && nodeDim) {\n return getRelativeDimensions(nodeDim, relativeNodeDim, node)\n }\n }\n return null\n}\n\ntype MeasureInWindowCb = (x: number, y: number, width: number, height: number) => void\n\ntype MeasureCb = (\n x: number,\n y: number,\n width: number,\n height: number,\n pageX: number,\n pageY: number\n) => void\n\nexport const measure = async (\n node: HTMLElement,\n callback: MeasureCb\n): Promise<LayoutValue | null> => {\n const out = await measureNode(\n node,\n node.parentNode instanceof HTMLElement ? node.parentNode : null\n )\n if (out) {\n callback?.(out.x, out.y, out.width, out.height, out.pageX, out.pageY)\n }\n return out\n}\n\nexport function createMeasure(\n node: HTMLElement\n): (callback: MeasureCb) => Promise<LayoutValue | null> {\n return (callback) => measure(node, callback)\n}\n\ntype WindowLayout = { pageX: number; pageY: number; width: number; height: number }\n\nexport const measureInWindow = async (\n node: HTMLElement,\n callback: MeasureInWindowCb\n): Promise<WindowLayout | null> => {\n const out = await measureNode(node, null)\n if (out) {\n callback?.(out.pageX, out.pageY, out.width, out.height)\n }\n return out\n}\n\nexport const createMeasureInWindow = (\n node: HTMLElement\n): ((callback: MeasureInWindowCb) => Promise<WindowLayout | null>) => {\n return (callback) => measureInWindow(node, callback)\n}\n\nexport const measureLayout = async (\n node: HTMLElement,\n relativeNode: HTMLElement,\n callback: MeasureCb\n): Promise<LayoutValue | null> => {\n const out = await measureNode(node, relativeNode)\n if (out) {\n callback?.(out.x, out.y, out.width, out.height, out.pageX, out.pageY)\n }\n return out\n}\n\nexport function createMeasureLayout(\n node: HTMLElement\n): (relativeTo: HTMLElement, callback: MeasureCb) => Promise<LayoutValue | null> {\n return (relativeTo, callback) => measureLayout(node, relativeTo, callback)\n}\n"
10
+ ]
11
11
  }