@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/LICENSE +21 -0
- package/dist/cjs/index.cjs +228 -0
- package/dist/cjs/index.js +166 -0
- package/dist/cjs/index.js.map +6 -0
- package/dist/cjs/index.native.js +210 -0
- package/dist/cjs/index.native.js.map +6 -0
- package/dist/esm/index.js +151 -0
- package/dist/esm/index.js.map +6 -0
- package/dist/esm/index.mjs +198 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/index.native.js +214 -0
- package/dist/esm/index.native.js.map +1 -0
- package/package.json +48 -0
- package/src/index.ts +304 -0
- package/types/index.d.ts +32 -0
- package/types/index.d.ts.map +27 -0
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
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -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
|
+
}
|