@tamagui/use-element-layout 1.130.7 → 1.130.8

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/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { isClient, useIsomorphicLayoutEffect } from '@tamagui/constants'
2
2
  import { isEqualShallow } from '@tamagui/is-equal-shallow'
3
- import type { RefObject } from 'react'
3
+ import { useCallback, type RefObject } from 'react'
4
4
 
5
5
  const LayoutHandlers = new WeakMap<HTMLElement, Function>()
6
6
  const Nodes = new Set<HTMLElement>()
@@ -26,8 +26,8 @@ export type LayoutValue = {
26
26
  y: number
27
27
  width: number
28
28
  height: number
29
- left: number
30
- top: number
29
+ pageX: number
30
+ pageY: number
31
31
  }
32
32
 
33
33
  export type LayoutEvent = {
@@ -58,7 +58,7 @@ export function enable(): void {
58
58
  }
59
59
  }
60
60
 
61
- function startGlobalIntersectionObserver() {
61
+ function startGlobalObservers() {
62
62
  if (!isClient || globalIntersectionObserver) return
63
63
 
64
64
  globalIntersectionObserver = new IntersectionObserver(
@@ -150,7 +150,9 @@ if (isClient) {
150
150
 
151
151
  // only run once in a few frames, this could be adjustable
152
152
  let frameCount = 0
153
- const RUN_EVERY_X_FRAMES = 8
153
+
154
+ const userSkipVal = process.env.TAMAGUI_LAYOUT_FRAME_SKIP
155
+ const RUN_EVERY_X_FRAMES = userSkipVal ? +userSkipVal : 10
154
156
 
155
157
  function layoutOnAnimationFrame() {
156
158
  if (strategy !== 'off') {
@@ -193,76 +195,11 @@ export const getElementLayoutEvent = (
193
195
  }
194
196
  }
195
197
 
196
- export const measureLayout = (
197
- node: HTMLElement,
198
- relativeTo: HTMLElement | null,
199
- callback: (
200
- x: number,
201
- y: number,
202
- width: number,
203
- height: number,
204
- left: number,
205
- top: number
206
- ) => void
207
- ): void => {
208
- const relativeNode = relativeTo || node?.parentElement
209
- if (relativeNode instanceof HTMLElement) {
210
- const nodeDim = node.getBoundingClientRect()
211
- const relativeNodeDim = relativeNode.getBoundingClientRect()
212
-
213
- if (relativeNodeDim && nodeDim) {
214
- const { x, y, width, height, left, top } = getRelativeDimensions(
215
- nodeDim,
216
- relativeNodeDim
217
- )
218
- callback(x, y, width, height, left, top)
219
- }
220
- }
221
- }
222
-
223
- export const getElementLayoutEventAsync = async (
224
- target: HTMLElement
225
- ): Promise<LayoutEvent> => {
226
- const layout = await measureLayoutAsync(target)
227
- if (!layout) {
228
- throw new Error(`‼️`) // impossible
229
- }
230
- return {
231
- nativeEvent: {
232
- layout,
233
- target,
234
- },
235
- timeStamp: Date.now(),
236
- }
237
- }
238
-
239
- export const measureLayoutAsync = async (
240
- node: HTMLElement,
241
- relativeTo?: HTMLElement | null
242
- ): Promise<null | LayoutValue> => {
243
- const relativeNode = relativeTo || node?.parentElement
244
- if (relativeNode instanceof HTMLElement) {
245
- const [nodeDim, relativeNodeDim] = await Promise.all([
246
- getBoundingClientRectAsync(node),
247
- getBoundingClientRectAsync(relativeNode),
248
- ])
249
-
250
- if (relativeNodeDim && nodeDim) {
251
- const { x, y, width, height, left, top } = getRelativeDimensions(
252
- nodeDim,
253
- relativeNodeDim
254
- )
255
- return { x, y, width, height, left, top }
256
- }
257
- }
258
- return null
259
- }
260
-
261
198
  const getRelativeDimensions = (a: DOMRectReadOnly, b: DOMRectReadOnly) => {
262
199
  const { height, left, top, width } = a
263
200
  const x = left - b.left
264
201
  const y = top - b.top
265
- return { x, y, width, height, left, top }
202
+ return { x, y, width, height, pageX: a.left, pageY: a.top }
266
203
  }
267
204
 
268
205
  export function useElementLayout(
@@ -283,7 +220,7 @@ export function useElementLayout(
283
220
  Nodes.add(node)
284
221
 
285
222
  // Add node to intersection observer
286
- startGlobalIntersectionObserver()
223
+ startGlobalObservers()
287
224
  if (globalIntersectionObserver) {
288
225
  globalIntersectionObserver.observe(node)
289
226
  // Initialize as intersecting by default
@@ -323,7 +260,7 @@ function ensureWebElement<X>(x: X): HTMLElement | undefined {
323
260
  return x instanceof HTMLElement ? x : undefined
324
261
  }
325
262
 
326
- const getBoundingClientRectAsync = (
263
+ export const getBoundingClientRectAsync = (
327
264
  node: HTMLElement | null
328
265
  ): Promise<DOMRectReadOnly | false> => {
329
266
  return new Promise<DOMRectReadOnly | false>((res) => {
@@ -342,14 +279,87 @@ const getBoundingClientRectAsync = (
342
279
  })
343
280
  }
344
281
 
345
- const getBoundingClientRect = (node: HTMLElement | null): undefined | DOMRect => {
346
- if (!node || node.nodeType !== 1) return
347
- return node.getBoundingClientRect?.()
282
+ export const measureNode = async (
283
+ node: HTMLElement,
284
+ relativeTo?: HTMLElement | null
285
+ ): Promise<null | LayoutValue> => {
286
+ const relativeNode = relativeTo || node?.parentElement
287
+ if (relativeNode instanceof HTMLElement) {
288
+ const [nodeDim, relativeNodeDim] = await Promise.all([
289
+ getBoundingClientRectAsync(node),
290
+ getBoundingClientRectAsync(relativeNode),
291
+ ])
292
+ if (relativeNodeDim && nodeDim) {
293
+ return getRelativeDimensions(nodeDim, relativeNodeDim)
294
+ }
295
+ }
296
+ return null
297
+ }
298
+
299
+ type MeasureInWindowCb = (x: number, y: number, width: number, height: number) => void
300
+
301
+ type MeasureCb = (
302
+ x: number,
303
+ y: number,
304
+ width: number,
305
+ height: number,
306
+ pageX: number,
307
+ pageY: number
308
+ ) => void
309
+
310
+ export const measure = async (
311
+ node: HTMLElement,
312
+ callback: MeasureCb
313
+ ): Promise<LayoutValue | null> => {
314
+ const out = await measureNode(
315
+ node,
316
+ node.parentNode instanceof HTMLElement ? node.parentNode : null
317
+ )
318
+ if (out) {
319
+ callback?.(out.x, out.y, out.width, out.height, out.pageX, out.pageY)
320
+ }
321
+ return out
322
+ }
323
+
324
+ export function createMeasure(
325
+ node: HTMLElement
326
+ ): (callback: MeasureCb) => Promise<LayoutValue | null> {
327
+ return (callback) => measure(node, callback)
328
+ }
329
+
330
+ type WindowLayout = { pageX: number; pageY: number; width: number; height: number }
331
+
332
+ export const measureInWindow = async (
333
+ node: HTMLElement,
334
+ callback: MeasureInWindowCb
335
+ ): Promise<WindowLayout | null> => {
336
+ const out = await measureNode(node, null)
337
+ if (out) {
338
+ callback?.(out.pageX, out.pageY, out.width, out.height)
339
+ }
340
+ return out
341
+ }
342
+
343
+ export const createMeasureInWindow = (
344
+ node: HTMLElement
345
+ ): ((callback: MeasureInWindowCb) => Promise<WindowLayout | null>) => {
346
+ return (callback) => measureInWindow(node, callback)
347
+ }
348
+
349
+ export const measureLayout = async (
350
+ node: HTMLElement,
351
+ relativeNode: HTMLElement,
352
+ callback: MeasureCb
353
+ ): Promise<LayoutValue | null> => {
354
+ const out = await measureNode(node, relativeNode)
355
+ if (out) {
356
+ callback?.(out.x, out.y, out.width, out.height, out.pageX, out.pageY)
357
+ }
358
+ return out
348
359
  }
349
360
 
350
- export const getRect = (node: HTMLElement): LayoutValue | undefined => {
351
- const rect = getBoundingClientRect(node)
352
- if (!rect) return
353
- const { x, y, top, left } = rect
354
- return { x, y, width: node.offsetWidth, height: node.offsetHeight, top, left }
361
+ export function createMeasureLayout(
362
+ node: HTMLElement
363
+ ): (relativeTo: HTMLElement, callback: MeasureCb) => Promise<LayoutValue | null> {
364
+ return (relativeTo, callback) => measureLayout(node, relativeTo, callback)
355
365
  }
package/types/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { RefObject } from "react";
1
+ import { type RefObject } from "react";
2
2
  type TamaguiComponentStatePartial = {
3
3
  host?: any;
4
4
  };
@@ -9,8 +9,8 @@ export type LayoutValue = {
9
9
  y: number;
10
10
  width: number;
11
11
  height: number;
12
- left: number;
13
- top: number;
12
+ pageX: number;
13
+ pageY: number;
14
14
  };
15
15
  export type LayoutEvent = {
16
16
  nativeEvent: {
@@ -21,11 +21,23 @@ export type LayoutEvent = {
21
21
  };
22
22
  export declare function enable(): void;
23
23
  export declare const getElementLayoutEvent: (nodeRect: DOMRectReadOnly, parentRect: DOMRectReadOnly) => LayoutEvent;
24
- export declare const measureLayout: (node: HTMLElement, relativeTo: HTMLElement | null, callback: (x: number, y: number, width: number, height: number, left: number, top: number) => void) => void;
25
- export declare const getElementLayoutEventAsync: (target: HTMLElement) => Promise<LayoutEvent>;
26
- export declare const measureLayoutAsync: (node: HTMLElement, relativeTo?: HTMLElement | null) => Promise<null | LayoutValue>;
27
24
  export declare function useElementLayout(ref: RefObject<TamaguiComponentStatePartial>, onLayout?: ((e: LayoutEvent) => void) | null): void;
28
- export declare const getRect: (node: HTMLElement) => LayoutValue | undefined;
25
+ export declare const getBoundingClientRectAsync: (node: HTMLElement | null) => Promise<DOMRectReadOnly | false>;
26
+ export declare const measureNode: (node: HTMLElement, relativeTo?: HTMLElement | null) => Promise<null | LayoutValue>;
27
+ type MeasureInWindowCb = (x: number, y: number, width: number, height: number) => void;
28
+ type MeasureCb = (x: number, y: number, width: number, height: number, pageX: number, pageY: number) => void;
29
+ export declare const measure: (node: HTMLElement, callback: MeasureCb) => Promise<LayoutValue | null>;
30
+ export declare function createMeasure(node: HTMLElement): (callback: MeasureCb) => Promise<LayoutValue | null>;
31
+ type WindowLayout = {
32
+ pageX: number;
33
+ pageY: number;
34
+ width: number;
35
+ height: number;
36
+ };
37
+ export declare const measureInWindow: (node: HTMLElement, callback: MeasureInWindowCb) => Promise<WindowLayout | null>;
38
+ export declare const createMeasureInWindow: (node: HTMLElement) => ((callback: MeasureInWindowCb) => Promise<WindowLayout | null>);
39
+ export declare const measureLayout: (node: HTMLElement, relativeNode: HTMLElement, callback: MeasureCb) => Promise<LayoutValue | null>;
40
+ export declare function createMeasureLayout(node: HTMLElement): (relativeTo: HTMLElement, callback: MeasureCb) => Promise<LayoutValue | null>;
29
41
  export {};
30
42
 
31
43
  //# sourceMappingURL=index.d.ts.map
@@ -1,29 +1,31 @@
1
1
  {
2
- "mappings": "AAEA,cAAc,iBAAiB,OAAO;KASjC,+BAA+B;CAClC;AACD;KAEI,4BAA4B,QAAQ,SAAS;AAIlD,OAAO,iBAAS,oBAAoBA,OAAO;AAI3C,YAAY,cAAc;CACxB;CACA;CACA;CACA;CACA;CACA;AACD;AAED,YAAY,cAAc;CACxB,aAAa;EACX,QAAQ;EACR;CACD;CACD;AACD;AAYD,OAAO,iBAAS;AAoIhB,OAAO,cAAM,wBACXC,UAAU,iBACVC,YAAY,oBACX;AAUH,OAAO,cAAM,gBACXC,MAAM,aACNC,YAAY,oBACZC,WACEC,WACAC,WACAC,eACAC,gBACAC,cACAC;AAkBJ,OAAO,cAAM,6BACXC,QAAQ,gBACP,QAAQ;AAcX,OAAO,cAAM,qBACXT,MAAM,aACNU,aAAa,uBACZ,eAAe;AA0BlB,OAAO,iBAAS,iBACdC,KAAK,UAAU,+BACfC,aAAaC,GAAG;AAgFlB,OAAO,cAAM,UAAWb,MAAM,gBAAc",
2
+ "mappings": "AAEA,cAA2B,iBAAiB,OAAO;KAS9C,+BAA+B;CAClC;AACD;KAEI,4BAA4B,QAAQ,SAAS;AAIlD,OAAO,iBAAS,oBAAoBA,OAAO;AAI3C,YAAY,cAAc;CACxB;CACA;CACA;CACA;CACA;CACA;AACD;AAED,YAAY,cAAc;CACxB,aAAa;EACX,QAAQ;EACR;CACD;CACD;AACD;AAYD,OAAO,iBAAS;AAsIhB,OAAO,cAAM,wBACXC,UAAU,iBACVC,YAAY,oBACX;AAiBH,OAAO,iBAAS,iBACdC,KAAK,UAAU,+BACfC,aAAaC,GAAG;AAwDlB,OAAO,cAAM,6BACXC,MAAM,uBACL,QAAQ,kBAAkB;AAiB7B,OAAO,cAAM,cACXC,MAAM,aACNC,aAAa,uBACZ,eAAe;KAcb,qBAAqBC,WAAWC,WAAWC,eAAeC;KAE1D,aACHH,WACAC,WACAC,eACAC,gBACAC,eACAC;AAGF,OAAO,cAAM,UACXP,MAAM,aACNQ,UAAU,cACT,QAAQ;AAWX,OAAO,iBAAS,cACdR,MAAM,eACJQ,UAAU,cAAc,QAAQ;KAI/B,eAAe;CAAE;CAAe;CAAe;CAAe;AAAgB;AAEnF,OAAO,cAAM,kBACXR,MAAM,aACNS,UAAU,sBACT,QAAQ;AAQX,OAAO,cAAM,wBACXT,MAAM,kBACHS,UAAU,sBAAsB,QAAQ;AAI7C,OAAO,cAAM,gBACXT,MAAM,aACNU,cAAc,aACdF,UAAU,cACT,QAAQ;AAQX,OAAO,iBAAS,oBACdR,MAAM,eACJW,YAAY,aAAaH,UAAU,cAAc,QAAQ",
3
3
  "names": [
4
4
  "state: LayoutMeasurementStrategy",
5
5
  "nodeRect: DOMRectReadOnly",
6
6
  "parentRect: DOMRectReadOnly",
7
+ "ref: RefObject<TamaguiComponentStatePartial>",
8
+ "onLayout?: ((e: LayoutEvent) => void) | null",
9
+ "e: LayoutEvent",
10
+ "node: HTMLElement | null",
7
11
  "node: HTMLElement",
8
- "relativeTo: HTMLElement | null",
9
- "callback: (\n x: number,\n y: number,\n width: number,\n height: number,\n left: number,\n top: number\n ) => void",
12
+ "relativeTo?: HTMLElement | null",
10
13
  "x: number",
11
14
  "y: number",
12
15
  "width: number",
13
16
  "height: number",
14
- "left: number",
15
- "top: number",
16
- "target: HTMLElement",
17
- "relativeTo?: HTMLElement | null",
18
- "ref: RefObject<TamaguiComponentStatePartial>",
19
- "onLayout?: ((e: LayoutEvent) => void) | null",
20
- "e: LayoutEvent"
17
+ "pageX: number",
18
+ "pageY: number",
19
+ "callback: MeasureCb",
20
+ "callback: MeasureInWindowCb",
21
+ "relativeNode: HTMLElement",
22
+ "relativeTo: HTMLElement"
21
23
  ],
22
24
  "sources": [
23
25
  "src/index.ts"
24
26
  ],
25
27
  "sourcesContent": [
26
- "import { isClient, useIsomorphicLayoutEffect } from '@tamagui/constants'\nimport { isEqualShallow } from '@tamagui/is-equal-shallow'\nimport type { RefObject } from 'react'\n\nconst LayoutHandlers = new WeakMap<HTMLElement, Function>()\nconst Nodes = new Set<HTMLElement>()\nconst IntersectionState = new WeakMap<HTMLElement, boolean>()\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 left: number\n top: 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 ParentRectCache = new WeakMap<HTMLElement, DOMRect>()\nconst LastChangeTime = new WeakMap<HTMLElement, number>()\n\nconst rAF = typeof window !== 'undefined' ? window.requestAnimationFrame : undefined\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 startGlobalIntersectionObserver() {\n if (!isClient || 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 (isClient) {\n if (rAF) {\n const supportsCheckVisibility = 'checkVisibility' in document.body\n\n async function updateLayoutIfChanged(node: HTMLElement) {\n if (IntersectionState.get(node) === false) {\n // avoid due to not intersecting\n return\n }\n // triggers style recalculation in safari which is slower than not\n if (process.env.TAMAGUI_ONLAYOUT_VISIBILITY_CHECK === '1') {\n if (supportsCheckVisibility && !(node as any).checkVisibility()) {\n // avoid due to not visible\n return\n }\n }\n\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 getBoundingClientRectAsync(node),\n getBoundingClientRectAsync(parentNode),\n ])\n\n if (nr === false || pr === false) {\n return\n }\n\n nodeRect = nr\n parentRect = pr\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 if (\n !cachedRect ||\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 (!cachedParentRect || !isEqualShallow(cachedParentRect, parentRect)))\n ) {\n NodeRectCache.set(node, nodeRect)\n ParentRectCache.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 rAF!(layoutOnAnimationFrame)\n\n // only run once in a few frames, this could be adjustable\n let frameCount = 0\n const RUN_EVERY_X_FRAMES = 8\n\n function layoutOnAnimationFrame() {\n if (strategy !== 'off') {\n if (frameCount++ % RUN_EVERY_X_FRAMES !== 0) {\n // skip a few frames to avoid work\n rAF!(layoutOnAnimationFrame)\n return\n }\n\n if (frameCount === Number.MAX_SAFE_INTEGER) {\n frameCount = 0\n }\n\n Nodes.forEach((node) => {\n updateLayoutIfChanged(node)\n })\n }\n\n rAF!(layoutOnAnimationFrame)\n }\n } else {\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n `No requestAnimationFrame - please polyfill for onLayout to work correctly`\n )\n }\n }\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\nexport const measureLayout = (\n node: HTMLElement,\n relativeTo: HTMLElement | null,\n callback: (\n x: number,\n y: number,\n width: number,\n height: number,\n left: number,\n top: number\n ) => void\n): void => {\n const relativeNode = relativeTo || node?.parentElement\n if (relativeNode instanceof HTMLElement) {\n const nodeDim = node.getBoundingClientRect()\n const relativeNodeDim = relativeNode.getBoundingClientRect()\n\n if (relativeNodeDim && nodeDim) {\n const { x, y, width, height, left, top } = getRelativeDimensions(\n nodeDim,\n relativeNodeDim\n )\n callback(x, y, width, height, left, top)\n }\n }\n}\n\nexport const getElementLayoutEventAsync = async (\n target: HTMLElement\n): Promise<LayoutEvent> => {\n const layout = await measureLayoutAsync(target)\n if (!layout) {\n throw new Error(`‼️`) // impossible\n }\n return {\n nativeEvent: {\n layout,\n target,\n },\n timeStamp: Date.now(),\n }\n}\n\nexport const measureLayoutAsync = 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\n if (relativeNodeDim && nodeDim) {\n const { x, y, width, height, left, top } = getRelativeDimensions(\n nodeDim,\n relativeNodeDim\n )\n return { x, y, width, height, left, top }\n }\n }\n return null\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, left, top }\n}\n\nexport function useElementLayout(\n ref: RefObject<TamaguiComponentStatePartial>,\n onLayout?: ((e: LayoutEvent) => void) | null\n): void {\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 }\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 startGlobalIntersectionObserver()\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\nconst 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\nconst getBoundingClientRect = (node: HTMLElement | null): undefined | DOMRect => {\n if (!node || node.nodeType !== 1) return\n return node.getBoundingClientRect?.()\n}\n\nexport const getRect = (node: HTMLElement): LayoutValue | undefined => {\n const rect = getBoundingClientRect(node)\n if (!rect) return\n const { x, y, top, left } = rect\n return { x, y, width: node.offsetWidth, height: node.offsetHeight, top, left }\n}\n"
28
+ "import { isClient, useIsomorphicLayoutEffect } from '@tamagui/constants'\nimport { isEqualShallow } from '@tamagui/is-equal-shallow'\nimport { useCallback, type RefObject } from 'react'\n\nconst LayoutHandlers = new WeakMap<HTMLElement, Function>()\nconst Nodes = new Set<HTMLElement>()\nconst IntersectionState = new WeakMap<HTMLElement, boolean>()\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 ParentRectCache = new WeakMap<HTMLElement, DOMRect>()\nconst LastChangeTime = new WeakMap<HTMLElement, number>()\n\nconst rAF = typeof window !== 'undefined' ? window.requestAnimationFrame : undefined\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 (!isClient || 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 (isClient) {\n if (rAF) {\n const supportsCheckVisibility = 'checkVisibility' in document.body\n\n async function updateLayoutIfChanged(node: HTMLElement) {\n if (IntersectionState.get(node) === false) {\n // avoid due to not intersecting\n return\n }\n // triggers style recalculation in safari which is slower than not\n if (process.env.TAMAGUI_ONLAYOUT_VISIBILITY_CHECK === '1') {\n if (supportsCheckVisibility && !(node as any).checkVisibility()) {\n // avoid due to not visible\n return\n }\n }\n\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 getBoundingClientRectAsync(node),\n getBoundingClientRectAsync(parentNode),\n ])\n\n if (nr === false || pr === false) {\n return\n }\n\n nodeRect = nr\n parentRect = pr\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 if (\n !cachedRect ||\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 (!cachedParentRect || !isEqualShallow(cachedParentRect, parentRect)))\n ) {\n NodeRectCache.set(node, nodeRect)\n ParentRectCache.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 rAF!(layoutOnAnimationFrame)\n\n // only run once in a few frames, this could be adjustable\n let frameCount = 0\n\n const userSkipVal = process.env.TAMAGUI_LAYOUT_FRAME_SKIP\n const RUN_EVERY_X_FRAMES = userSkipVal ? +userSkipVal : 10\n\n function layoutOnAnimationFrame() {\n if (strategy !== 'off') {\n if (frameCount++ % RUN_EVERY_X_FRAMES !== 0) {\n // skip a few frames to avoid work\n rAF!(layoutOnAnimationFrame)\n return\n }\n\n if (frameCount === Number.MAX_SAFE_INTEGER) {\n frameCount = 0\n }\n\n Nodes.forEach((node) => {\n updateLayoutIfChanged(node)\n })\n }\n\n rAF!(layoutOnAnimationFrame)\n }\n } else {\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n `No requestAnimationFrame - please polyfill for onLayout to work correctly`\n )\n }\n }\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 // 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 }\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"
27
29
  ],
28
30
  "version": 3
29
31
  }