@tamagui/use-element-layout 1.129.6-1751237024118

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 ADDED
@@ -0,0 +1,304 @@
1
+ import { isClient, useIsomorphicLayoutEffect } from '@tamagui/constants'
2
+ import { isEqualShallow } from '@tamagui/is-equal-shallow'
3
+ import type { RefObject } from 'react'
4
+
5
+ const LayoutHandlers = new WeakMap<HTMLElement, Function>()
6
+ const Nodes = new Set<HTMLElement>()
7
+
8
+ type TamaguiComponentStatePartial = {
9
+ host?: any
10
+ }
11
+
12
+ type LayoutMeasurementStrategy = 'off' | 'sync' | 'async'
13
+
14
+ let strategy: LayoutMeasurementStrategy = 'async'
15
+
16
+ export function setOnLayoutStrategy(state: LayoutMeasurementStrategy): void {
17
+ strategy = state
18
+ }
19
+
20
+ export type LayoutValue = {
21
+ x: number
22
+ y: number
23
+ width: number
24
+ height: number
25
+ left: number
26
+ top: number
27
+ }
28
+
29
+ export type LayoutEvent = {
30
+ nativeEvent: {
31
+ layout: LayoutValue
32
+ target: any
33
+ }
34
+ timeStamp: number
35
+ }
36
+
37
+ const NodeRectCache = new WeakMap<HTMLElement, DOMRect>()
38
+ const ParentRectCache = new WeakMap<HTMLElement, DOMRect>()
39
+ const DebounceTimers = new WeakMap<HTMLElement, NodeJS.Timeout>()
40
+ const LastChangeTime = new WeakMap<HTMLElement, number>()
41
+
42
+ const rAF = typeof window !== 'undefined' ? window.requestAnimationFrame : undefined
43
+ const DEBOUNCE_DELAY = 32 // 32ms debounce (2 frames at 60fps)
44
+
45
+ // prevent thrashing during first hydration (somewhat, streaming gets trickier)
46
+ let avoidUpdates = true
47
+ const queuedUpdates = new Map<HTMLElement, Function>()
48
+
49
+ export function enable(): void {
50
+ if (avoidUpdates) {
51
+ avoidUpdates = false
52
+ if (queuedUpdates) {
53
+ queuedUpdates.forEach((cb) => cb())
54
+ queuedUpdates.clear()
55
+ }
56
+ }
57
+ }
58
+
59
+ if (isClient) {
60
+ if (rAF) {
61
+ // track frame timing to detect sync work and avoid updates during heavy periods
62
+ let lastFrameAt = Date.now()
63
+ const numDroppedFramesUntilPause = 2 // adjust sensitivity
64
+
65
+ async function updateLayoutIfChanged(node: HTMLElement) {
66
+ const nodeRect = node.getBoundingClientRect()
67
+ const parentNode = node.parentElement
68
+ const parentRect = parentNode?.getBoundingClientRect()
69
+
70
+ const onLayout = LayoutHandlers.get(node)
71
+ if (typeof onLayout !== 'function') return
72
+
73
+ const cachedRect = NodeRectCache.get(node)
74
+ const cachedParentRect = parentNode ? NodeRectCache.get(parentNode) : null
75
+
76
+ if (
77
+ !cachedRect ||
78
+ // has changed one rect
79
+ (!isEqualShallow(cachedRect, nodeRect) &&
80
+ (!cachedParentRect || !isEqualShallow(cachedParentRect, parentRect)))
81
+ ) {
82
+ NodeRectCache.set(node, nodeRect)
83
+ if (parentRect && parentNode) {
84
+ ParentRectCache.set(parentNode, parentRect)
85
+ }
86
+
87
+ if (avoidUpdates) {
88
+ // Use sync version for queued updates to avoid promise complications
89
+ const event = getElementLayoutEvent(node)
90
+ queuedUpdates.set(node, () => onLayout(event))
91
+ } else if (strategy === 'async') {
92
+ // For async strategy, debounce the layout update
93
+ const now = Date.now()
94
+ LastChangeTime.set(node, now)
95
+
96
+ // Clear existing debounce timer
97
+ const existingTimer = DebounceTimers.get(node)
98
+ if (existingTimer) {
99
+ clearTimeout(existingTimer)
100
+ }
101
+
102
+ // Set new debounce timer
103
+ const timer = setTimeout(async () => {
104
+ const lastChange = LastChangeTime.get(node) || 0
105
+ const timeSinceChange = Date.now() - lastChange
106
+
107
+ // Only fire if at least DEBOUNCE_DELAY has passed since last change
108
+ if (timeSinceChange >= DEBOUNCE_DELAY) {
109
+ const event = await getElementLayoutEventAsync(node)
110
+ onLayout(event)
111
+ DebounceTimers.delete(node)
112
+ } else {
113
+ // Reschedule if not enough time has passed
114
+ const remainingDelay = DEBOUNCE_DELAY - timeSinceChange
115
+ const newTimer = setTimeout(async () => {
116
+ const event = await getElementLayoutEventAsync(node)
117
+ onLayout(event)
118
+ DebounceTimers.delete(node)
119
+ }, remainingDelay)
120
+ DebounceTimers.set(node, newTimer)
121
+ }
122
+ }, DEBOUNCE_DELAY)
123
+
124
+ DebounceTimers.set(node, timer)
125
+ } else {
126
+ // Sync strategy - use sync version
127
+ const event = getElementLayoutEvent(node)
128
+ onLayout(event)
129
+ }
130
+ }
131
+ }
132
+
133
+ // note that getBoundingClientRect() does not thrash layout if its after an animation frame
134
+ rAF!(layoutOnAnimationFrame)
135
+
136
+ function layoutOnAnimationFrame() {
137
+ const now = Date.now()
138
+ const timeSinceLastFrame = now - lastFrameAt
139
+ lastFrameAt = now
140
+
141
+ if (strategy !== 'off') {
142
+ // avoid updates if we've been dropping frames (indicates sync work happening)
143
+ const expectedFrameTime = 16.67 // ~60fps
144
+ const hasRecentSyncWork =
145
+ timeSinceLastFrame > expectedFrameTime * numDroppedFramesUntilPause
146
+
147
+ if (!hasRecentSyncWork) {
148
+ Nodes.forEach(updateLayoutIfChanged)
149
+ }
150
+ }
151
+ rAF!(layoutOnAnimationFrame)
152
+ }
153
+ } else {
154
+ if (process.env.NODE_ENV === 'development') {
155
+ console.warn(
156
+ `No requestAnimationFrame - please polyfill for onLayout to work correctly`
157
+ )
158
+ }
159
+ }
160
+ }
161
+
162
+ // Sync versions
163
+ export const getElementLayoutEvent = (target: HTMLElement): LayoutEvent => {
164
+ let res: LayoutEvent | null = null
165
+ measureLayout(target, null, (x, y, width, height, left, top) => {
166
+ res = {
167
+ nativeEvent: {
168
+ layout: { x, y, width, height, left, top },
169
+ target,
170
+ },
171
+ timeStamp: Date.now(),
172
+ }
173
+ })
174
+ if (!res) {
175
+ throw new Error(`‼️`) // impossible
176
+ }
177
+ return res
178
+ }
179
+
180
+ export const measureLayout = (
181
+ node: HTMLElement,
182
+ relativeTo: HTMLElement | null,
183
+ callback: (
184
+ x: number,
185
+ y: number,
186
+ width: number,
187
+ height: number,
188
+ left: number,
189
+ top: number
190
+ ) => void
191
+ ): void => {
192
+ const relativeNode = relativeTo || node?.parentElement
193
+ if (relativeNode instanceof HTMLElement) {
194
+ const nodeDim = node.getBoundingClientRect()
195
+ const relativeNodeDim = relativeNode.getBoundingClientRect()
196
+
197
+ if (relativeNodeDim && nodeDim) {
198
+ const { x, y, width, height, left, top } = getRelativeDimensions(
199
+ nodeDim,
200
+ relativeNodeDim
201
+ )
202
+ callback(x, y, width, height, left, top)
203
+ }
204
+ }
205
+ }
206
+
207
+ export const getElementLayoutEventAsync = async (
208
+ target: HTMLElement
209
+ ): Promise<LayoutEvent> => {
210
+ const layout = await measureLayoutAsync(target)
211
+ if (!layout) {
212
+ throw new Error(`‼️`) // impossible
213
+ }
214
+ return {
215
+ nativeEvent: {
216
+ layout,
217
+ target,
218
+ },
219
+ timeStamp: Date.now(),
220
+ }
221
+ }
222
+
223
+ export const measureLayoutAsync = async (
224
+ node: HTMLElement,
225
+ relativeTo?: HTMLElement | null
226
+ ): Promise<null | LayoutValue> => {
227
+ const relativeNode = relativeTo || node?.parentElement
228
+ if (relativeNode instanceof HTMLElement) {
229
+ const [nodeDim, relativeNodeDim] = await Promise.all([
230
+ node.getBoundingClientRect(),
231
+ relativeNode.getBoundingClientRect(),
232
+ ])
233
+
234
+ if (relativeNodeDim && nodeDim) {
235
+ const { x, y, width, height, left, top } = getRelativeDimensions(
236
+ nodeDim,
237
+ relativeNodeDim
238
+ )
239
+ return { x, y, width, height, left, top }
240
+ }
241
+ }
242
+ return null
243
+ }
244
+
245
+ const getRelativeDimensions = (a: DOMRectReadOnly, b: DOMRectReadOnly) => {
246
+ const { height, left, top, width } = a
247
+ const x = left - b.left
248
+ const y = top - b.top
249
+ return { x, y, width, height, left, top }
250
+ }
251
+
252
+ export function useElementLayout(
253
+ ref: RefObject<TamaguiComponentStatePartial>,
254
+ onLayout?: ((e: LayoutEvent) => void) | null
255
+ ): void {
256
+ // ensure always up to date so we can avoid re-running effect
257
+ const node = ensureWebElement(ref.current?.host)
258
+ if (node && onLayout) {
259
+ LayoutHandlers.set(node, onLayout)
260
+ }
261
+
262
+ useIsomorphicLayoutEffect(() => {
263
+ if (!onLayout) return
264
+ const node = ref.current?.host
265
+ if (!node) return
266
+
267
+ LayoutHandlers.set(node, onLayout)
268
+ Nodes.add(node)
269
+ onLayout(getElementLayoutEvent(node))
270
+
271
+ return () => {
272
+ Nodes.delete(node)
273
+ LayoutHandlers.delete(node)
274
+ NodeRectCache.delete(node)
275
+
276
+ // Clean up debounce timer and tracking
277
+ const timer = DebounceTimers.get(node)
278
+ if (timer) {
279
+ clearTimeout(timer)
280
+ DebounceTimers.delete(node)
281
+ }
282
+ LastChangeTime.delete(node)
283
+ }
284
+ }, [ref, !!onLayout])
285
+ }
286
+
287
+ function ensureWebElement<X>(x: X): HTMLElement | undefined {
288
+ if (typeof HTMLElement === 'undefined') {
289
+ return undefined
290
+ }
291
+ return x instanceof HTMLElement ? x : undefined
292
+ }
293
+
294
+ const getBoundingClientRect = (node: HTMLElement | null): undefined | DOMRect => {
295
+ if (!node || node.nodeType !== 1) return
296
+ return node.getBoundingClientRect?.()
297
+ }
298
+
299
+ export const getRect = (node: HTMLElement): LayoutValue | undefined => {
300
+ const rect = getBoundingClientRect(node)
301
+ if (!rect) return
302
+ const { x, y, top, left } = rect
303
+ return { x, y, width: node.offsetWidth, height: node.offsetHeight, top, left }
304
+ }
@@ -0,0 +1,32 @@
1
+ import type { RefObject } from "react";
2
+ type TamaguiComponentStatePartial = {
3
+ host?: any;
4
+ };
5
+ type LayoutMeasurementStrategy = "off" | "sync" | "async";
6
+ export declare function setOnLayoutStrategy(state: LayoutMeasurementStrategy): void;
7
+ export type LayoutValue = {
8
+ x: number;
9
+ y: number;
10
+ width: number;
11
+ height: number;
12
+ left: number;
13
+ top: number;
14
+ };
15
+ export type LayoutEvent = {
16
+ nativeEvent: {
17
+ layout: LayoutValue;
18
+ target: any;
19
+ };
20
+ timeStamp: number;
21
+ };
22
+ export declare function enable(): void;
23
+ // Sync versions
24
+ export declare const getElementLayoutEvent: (target: HTMLElement) => LayoutEvent;
25
+ export declare const measureLayout: (node: HTMLElement, relativeTo: HTMLElement | null, callback: (x: number, y: number, width: number, height: number, left: number, top: number) => void) => void;
26
+ export declare const getElementLayoutEventAsync: (target: HTMLElement) => Promise<LayoutEvent>;
27
+ export declare const measureLayoutAsync: (node: HTMLElement, relativeTo?: HTMLElement | null) => Promise<null | LayoutValue>;
28
+ export declare function useElementLayout(ref: RefObject<TamaguiComponentStatePartial>, onLayout?: ((e: LayoutEvent) => void) | null): void;
29
+ export declare const getRect: (node: HTMLElement) => LayoutValue | undefined;
30
+ export {};
31
+
32
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,27 @@
1
+ {
2
+ "mappings": "AAEA,cAAc,iBAAiB,OAAO;KAKjC,+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;AAcD,OAAO,iBAAS;;AAkHhB,OAAO,cAAM,wBAAyBC,QAAQ,gBAAc;AAiB5D,OAAO,cAAM,gBACXC,MAAM,aACNC,YAAY,oBACZC,WACEC,WACAC,WACAC,eACAC,gBACAC,cACAC;AAkBJ,OAAO,cAAM,6BACXT,QAAQ,gBACP,QAAQ;AAcX,OAAO,cAAM,qBACXC,MAAM,aACNS,aAAa,uBACZ,eAAe;AA0BlB,OAAO,iBAAS,iBACdC,KAAK,UAAU,+BACfC,aAAaC,GAAG;AA6ClB,OAAO,cAAM,UAAWZ,MAAM,gBAAc",
3
+ "names": [
4
+ "state: LayoutMeasurementStrategy",
5
+ "target: HTMLElement",
6
+ "node: HTMLElement",
7
+ "relativeTo: HTMLElement | null",
8
+ "callback: (\n x: number,\n y: number,\n width: number,\n height: number,\n left: number,\n top: number\n ) => void",
9
+ "x: number",
10
+ "y: number",
11
+ "width: number",
12
+ "height: number",
13
+ "left: number",
14
+ "top: number",
15
+ "relativeTo?: HTMLElement | null",
16
+ "ref: RefObject<TamaguiComponentStatePartial>",
17
+ "onLayout?: ((e: LayoutEvent) => void) | null",
18
+ "e: LayoutEvent"
19
+ ],
20
+ "sources": [
21
+ "src/index.ts"
22
+ ],
23
+ "sourcesContent": [
24
+ "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>()\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 DebounceTimers = new WeakMap<HTMLElement, NodeJS.Timeout>()\nconst LastChangeTime = new WeakMap<HTMLElement, number>()\n\nconst rAF = typeof window !== 'undefined' ? window.requestAnimationFrame : undefined\nconst DEBOUNCE_DELAY = 32 // 32ms debounce (2 frames at 60fps)\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\nif (isClient) {\n if (rAF) {\n // track frame timing to detect sync work and avoid updates during heavy periods\n let lastFrameAt = Date.now()\n const numDroppedFramesUntilPause = 2 // adjust sensitivity\n\n async function updateLayoutIfChanged(node: HTMLElement) {\n const nodeRect = node.getBoundingClientRect()\n const parentNode = node.parentElement\n const parentRect = parentNode?.getBoundingClientRect()\n\n const onLayout = LayoutHandlers.get(node)\n if (typeof onLayout !== 'function') return\n\n const cachedRect = NodeRectCache.get(node)\n const cachedParentRect = parentNode ? NodeRectCache.get(parentNode) : null\n\n if (\n !cachedRect ||\n // has changed one rect\n (!isEqualShallow(cachedRect, nodeRect) &&\n (!cachedParentRect || !isEqualShallow(cachedParentRect, parentRect)))\n ) {\n NodeRectCache.set(node, nodeRect)\n if (parentRect && parentNode) {\n ParentRectCache.set(parentNode, parentRect)\n }\n\n if (avoidUpdates) {\n // Use sync version for queued updates to avoid promise complications\n const event = getElementLayoutEvent(node)\n queuedUpdates.set(node, () => onLayout(event))\n } else if (strategy === 'async') {\n // For async strategy, debounce the layout update\n const now = Date.now()\n LastChangeTime.set(node, now)\n\n // Clear existing debounce timer\n const existingTimer = DebounceTimers.get(node)\n if (existingTimer) {\n clearTimeout(existingTimer)\n }\n\n // Set new debounce timer\n const timer = setTimeout(async () => {\n const lastChange = LastChangeTime.get(node) || 0\n const timeSinceChange = Date.now() - lastChange\n\n // Only fire if at least DEBOUNCE_DELAY has passed since last change\n if (timeSinceChange >= DEBOUNCE_DELAY) {\n const event = await getElementLayoutEventAsync(node)\n onLayout(event)\n DebounceTimers.delete(node)\n } else {\n // Reschedule if not enough time has passed\n const remainingDelay = DEBOUNCE_DELAY - timeSinceChange\n const newTimer = setTimeout(async () => {\n const event = await getElementLayoutEventAsync(node)\n onLayout(event)\n DebounceTimers.delete(node)\n }, remainingDelay)\n DebounceTimers.set(node, newTimer)\n }\n }, DEBOUNCE_DELAY)\n\n DebounceTimers.set(node, timer)\n } else {\n // Sync strategy - use sync version\n const event = getElementLayoutEvent(node)\n onLayout(event)\n }\n }\n }\n\n // note that getBoundingClientRect() does not thrash layout if its after an animation frame\n rAF!(layoutOnAnimationFrame)\n\n function layoutOnAnimationFrame() {\n const now = Date.now()\n const timeSinceLastFrame = now - lastFrameAt\n lastFrameAt = now\n\n if (strategy !== 'off') {\n // avoid updates if we've been dropping frames (indicates sync work happening)\n const expectedFrameTime = 16.67 // ~60fps\n const hasRecentSyncWork =\n timeSinceLastFrame > expectedFrameTime * numDroppedFramesUntilPause\n\n if (!hasRecentSyncWork) {\n Nodes.forEach(updateLayoutIfChanged)\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\n// Sync versions\nexport const getElementLayoutEvent = (target: HTMLElement): LayoutEvent => {\n let res: LayoutEvent | null = null\n measureLayout(target, null, (x, y, width, height, left, top) => {\n res = {\n nativeEvent: {\n layout: { x, y, width, height, left, top },\n target,\n },\n timeStamp: Date.now(),\n }\n })\n if (!res) {\n throw new Error(`‼️`) // impossible\n }\n return res\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 node.getBoundingClientRect(),\n relativeNode.getBoundingClientRect(),\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 LayoutHandlers.set(node, onLayout)\n Nodes.add(node)\n onLayout(getElementLayoutEvent(node))\n\n return () => {\n Nodes.delete(node)\n LayoutHandlers.delete(node)\n NodeRectCache.delete(node)\n\n // Clean up debounce timer and tracking\n const timer = DebounceTimers.get(node)\n if (timer) {\n clearTimeout(timer)\n DebounceTimers.delete(node)\n }\n LastChangeTime.delete(node)\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 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"
25
+ ],
26
+ "version": 3
27
+ }