@tamagui/use-element-layout 2.7.7 → 3.0.0-beta.1093.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.cjs +253 -306
- package/dist/cjs/index.native.cjs +385 -0
- package/dist/cjs/index.native.js +332 -378
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/index.js +226 -280
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index.mjs +226 -280
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +309 -351
- package/dist/esm/index.native.js.map +1 -1
- package/package.json +6 -5
- package/src/index.tsx +78 -249
- package/types/index.d.ts +6 -8
- package/types/index.d.ts.map +2 -2
package/src/index.tsx
CHANGED
|
@@ -4,35 +4,22 @@ import { createContext, useContext, useId, type ReactNode, type RefObject } from
|
|
|
4
4
|
const LayoutHandlers = new WeakMap<HTMLElement, Function>()
|
|
5
5
|
const LayoutDisableKey = new WeakMap<HTMLElement, string>()
|
|
6
6
|
const Nodes = new Set<HTMLElement>()
|
|
7
|
+
const NodeRectCache = new WeakMap<HTMLElement, DOMRectReadOnly>()
|
|
8
|
+
const PrevHostNode = new WeakMap<object, HTMLElement | undefined>()
|
|
7
9
|
|
|
8
|
-
// feature flag to enable pre-transform dimension reporting (matches RN behavior)
|
|
9
|
-
// can be set via env var at build time or runtime global for testing
|
|
10
|
-
// see: https://github.com/tamagui/tamagui/pull/2329
|
|
11
10
|
const usePretransformDimensions = () =>
|
|
12
11
|
(globalThis as any).__TAMAGUI_ONLAYOUT_PRETRANSFORM === true ||
|
|
13
12
|
process.env.TAMAGUI_ONLAYOUT_PRETRANSFORM === '1'
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
function isDebugLayout() {
|
|
18
|
-
if (_debugLayout === undefined) {
|
|
19
|
-
_debugLayout =
|
|
20
|
-
typeof window !== 'undefined' &&
|
|
21
|
-
new URLSearchParams(window.location.search).has('__tamaDebugLayout')
|
|
22
|
-
}
|
|
23
|
-
return _debugLayout
|
|
24
|
-
}
|
|
14
|
+
const isDebugLayout = () =>
|
|
15
|
+
typeof window !== 'undefined' && window.location?.search?.includes('__tamaDebugLayout')
|
|
25
16
|
|
|
26
|
-
// separating to avoid all re-rendering
|
|
27
17
|
const DisableLayoutContextValues: Record<string, boolean> = {}
|
|
28
18
|
const DisableLayoutContextKey = createContext<string>('')
|
|
29
19
|
|
|
30
20
|
const ENABLE =
|
|
31
21
|
process.env.TAMAGUI_TARGET === 'web' && typeof IntersectionObserver !== 'undefined'
|
|
32
22
|
|
|
33
|
-
// internal testing - advanced helper to turn off layout measurement for extra performance
|
|
34
|
-
// TODO document!
|
|
35
|
-
// TODO could add frame skip control here
|
|
36
23
|
export const LayoutMeasurementController = ({
|
|
37
24
|
disable,
|
|
38
25
|
children,
|
|
@@ -41,16 +28,10 @@ export const LayoutMeasurementController = ({
|
|
|
41
28
|
children: ReactNode
|
|
42
29
|
}): ReactNode => {
|
|
43
30
|
const id = useId()
|
|
44
|
-
|
|
45
31
|
useIsomorphicLayoutEffect(() => {
|
|
46
32
|
const wasDisabled = DisableLayoutContextValues[id] === true
|
|
47
33
|
DisableLayoutContextValues[id] = disable
|
|
48
|
-
|
|
49
|
-
// waiting on a measurement to position itself, so don't make it sit out
|
|
50
|
-
// the frame-skip window
|
|
51
|
-
if (wasDisabled && !disable) {
|
|
52
|
-
measureOnNextFrame?.()
|
|
53
|
-
}
|
|
34
|
+
if (wasDisabled && !disable) measureOnNextFrame?.()
|
|
54
35
|
}, [disable, id])
|
|
55
36
|
|
|
56
37
|
return (
|
|
@@ -60,29 +41,13 @@ export const LayoutMeasurementController = ({
|
|
|
60
41
|
)
|
|
61
42
|
}
|
|
62
43
|
|
|
63
|
-
type TamaguiComponentStatePartial = {
|
|
64
|
-
host?: any
|
|
65
|
-
}
|
|
66
|
-
|
|
44
|
+
type TamaguiComponentStatePartial = { host?: any }
|
|
67
45
|
type LayoutMeasurementStrategy = 'off' | 'sync' | 'async'
|
|
68
46
|
|
|
69
47
|
let strategy: LayoutMeasurementStrategy = 'async'
|
|
70
|
-
|
|
71
|
-
// the measurement loop parks itself whenever it provably has nothing to do: no
|
|
72
|
-
// registered nodes, a hidden document, or measurement turned off. these are the
|
|
73
|
-
// only three ways work can reappear, so each one restarts it.
|
|
74
48
|
let resumeLayoutLoop: (() => void) | undefined
|
|
75
|
-
|
|
76
|
-
// restarts the loop AND makes its next frame a measuring frame, skipping the
|
|
77
|
-
// frame-skip backoff once. for consumers whose position depends on a pending
|
|
78
|
-
// measurement (an opening sheet parked off-screen).
|
|
79
49
|
let measureOnNextFrame: (() => void) | undefined
|
|
80
50
|
|
|
81
|
-
export function setOnLayoutStrategy(state: LayoutMeasurementStrategy): void {
|
|
82
|
-
strategy = state
|
|
83
|
-
resumeLayoutLoop?.()
|
|
84
|
-
}
|
|
85
|
-
|
|
86
51
|
export type LayoutValue = {
|
|
87
52
|
x: number
|
|
88
53
|
y: number
|
|
@@ -93,16 +58,10 @@ export type LayoutValue = {
|
|
|
93
58
|
}
|
|
94
59
|
|
|
95
60
|
export type LayoutEvent = {
|
|
96
|
-
nativeEvent: {
|
|
97
|
-
layout: LayoutValue
|
|
98
|
-
target: any
|
|
99
|
-
}
|
|
61
|
+
nativeEvent: { layout: LayoutValue; target: any }
|
|
100
62
|
timeStamp: number
|
|
101
63
|
}
|
|
102
64
|
|
|
103
|
-
const NodeRectCache = new WeakMap<HTMLElement, DOMRectReadOnly>()
|
|
104
|
-
|
|
105
|
-
// prevent thrashing during first hydration (somewhat, streaming gets trickier)
|
|
106
65
|
let avoidUpdates = true
|
|
107
66
|
const queuedUpdates = new Map<HTMLElement, Function>()
|
|
108
67
|
|
|
@@ -116,15 +75,12 @@ export function enable(): void {
|
|
|
116
75
|
}
|
|
117
76
|
}
|
|
118
77
|
|
|
119
|
-
// optimization: inline rect comparison to avoid function call overhead on hot path
|
|
120
78
|
function rectsEqual(a: DOMRectReadOnly, b: DOMRectReadOnly): boolean {
|
|
121
79
|
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height
|
|
122
80
|
}
|
|
123
81
|
|
|
124
82
|
if (ENABLE) {
|
|
125
83
|
const BoundingRects = new WeakMap<Element, DOMRectReadOnly>()
|
|
126
|
-
|
|
127
|
-
// optimization: persistent IO for rect fetching, reused across cycles
|
|
128
84
|
let rectFetchObserver: IntersectionObserver | null = null
|
|
129
85
|
let rectFetchResolve: ((value: boolean) => void) | null = null
|
|
130
86
|
let rectFetchStartTime = 0
|
|
@@ -132,16 +88,12 @@ if (ENABLE) {
|
|
|
132
88
|
|
|
133
89
|
function ensureRectFetchObserver() {
|
|
134
90
|
if (rectFetchObserver) return rectFetchObserver
|
|
135
|
-
|
|
136
91
|
rectFetchObserver = new IntersectionObserver(
|
|
137
92
|
(entries) => {
|
|
138
93
|
lastCallbackDelay = Math.round(performance.now() - rectFetchStartTime)
|
|
139
|
-
|
|
140
|
-
// store all rects
|
|
141
94
|
for (let i = 0; i < entries.length; i++) {
|
|
142
95
|
BoundingRects.set(entries[i].target, entries[i].boundingClientRect)
|
|
143
96
|
}
|
|
144
|
-
|
|
145
97
|
if (
|
|
146
98
|
process.env.NODE_ENV === 'development' &&
|
|
147
99
|
isDebugLayout() &&
|
|
@@ -154,90 +106,64 @@ if (ENABLE) {
|
|
|
154
106
|
'entries'
|
|
155
107
|
)
|
|
156
108
|
}
|
|
157
|
-
|
|
158
109
|
if (rectFetchResolve) {
|
|
159
110
|
rectFetchResolve(true)
|
|
160
111
|
rectFetchResolve = null
|
|
161
112
|
}
|
|
162
113
|
},
|
|
163
|
-
{
|
|
164
|
-
threshold: 0,
|
|
165
|
-
}
|
|
114
|
+
{ threshold: 0 }
|
|
166
115
|
)
|
|
167
|
-
|
|
168
116
|
return rectFetchObserver
|
|
169
117
|
}
|
|
170
118
|
|
|
171
|
-
|
|
119
|
+
function updateLayoutIfChanged(node: HTMLElement) {
|
|
172
120
|
const onLayout = LayoutHandlers.get(node)
|
|
173
121
|
if (typeof onLayout !== 'function') return
|
|
174
|
-
|
|
175
122
|
const parentNode = node.parentElement
|
|
176
123
|
if (!parentNode) return
|
|
177
124
|
|
|
178
125
|
let nodeRect: DOMRectReadOnly | undefined
|
|
179
126
|
let parentRect: DOMRectReadOnly | undefined
|
|
180
127
|
|
|
181
|
-
// respect the strategy contract
|
|
182
128
|
if (strategy === 'async') {
|
|
183
129
|
nodeRect = BoundingRects.get(node)
|
|
184
130
|
parentRect = BoundingRects.get(parentNode)
|
|
185
|
-
|
|
186
|
-
if (!nodeRect || !parentRect) {
|
|
187
|
-
return
|
|
188
|
-
}
|
|
131
|
+
if (!nodeRect || !parentRect) return
|
|
189
132
|
} else {
|
|
190
133
|
nodeRect = node.getBoundingClientRect()
|
|
191
134
|
parentRect = parentNode.getBoundingClientRect()
|
|
192
135
|
}
|
|
193
|
-
|
|
194
136
|
emitLayoutIfChanged(node, parentNode, nodeRect, parentRect)
|
|
195
137
|
}
|
|
196
138
|
|
|
197
139
|
const rAF =
|
|
198
140
|
typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame : undefined
|
|
199
|
-
|
|
200
|
-
// adaptive frame skipping with backoff
|
|
201
141
|
const userSkipVal = process.env.TAMAGUI_LAYOUT_FRAME_SKIP
|
|
202
142
|
const BASE_SKIP_FRAMES = userSkipVal ? +userSkipVal : 10
|
|
203
143
|
const MAX_SKIP_FRAMES = 20
|
|
204
144
|
let skipFrames = BASE_SKIP_FRAMES
|
|
205
145
|
let frameCount = 0
|
|
206
|
-
|
|
207
|
-
// stays true across the await below so a node registering mid-cycle cannot
|
|
208
|
-
// start a second concurrent loop
|
|
209
146
|
let frameScheduled = false
|
|
210
147
|
|
|
211
148
|
function scheduleLayoutFrame() {
|
|
212
|
-
if (frameScheduled || strategy === 'off' || Nodes.size === 0 || document.hidden)
|
|
149
|
+
if (frameScheduled || strategy === 'off' || Nodes.size === 0 || document.hidden)
|
|
213
150
|
return
|
|
214
|
-
}
|
|
215
151
|
frameScheduled = true
|
|
216
152
|
rAF ? rAF(layoutOnAnimationFrame) : setTimeout(layoutOnAnimationFrame, 16)
|
|
217
153
|
}
|
|
218
154
|
|
|
219
155
|
async function layoutOnAnimationFrame() {
|
|
220
|
-
// skip frames based on adaptive rate
|
|
221
156
|
if (frameCount++ % skipFrames !== 0) {
|
|
222
157
|
frameScheduled = false
|
|
223
158
|
scheduleLayoutFrame()
|
|
224
159
|
return
|
|
225
160
|
}
|
|
226
|
-
|
|
227
|
-
// reset frame count to avoid overflow
|
|
228
|
-
if (frameCount >= Number.MAX_SAFE_INTEGER) {
|
|
229
|
-
frameCount = 0
|
|
230
|
-
}
|
|
161
|
+
if (frameCount >= Number.MAX_SAFE_INTEGER) frameCount = 0
|
|
231
162
|
|
|
232
163
|
if (strategy !== 'off') {
|
|
233
164
|
const activeNodes: HTMLElement[] = []
|
|
234
|
-
// optimization: deduplicate parent observations
|
|
235
165
|
const parentsToObserve = new Set<HTMLElement>()
|
|
236
166
|
|
|
237
|
-
// collect non-disabled nodes and their unique parents. off-viewport
|
|
238
|
-
// nodes stay in: a sheet or popper parks off-screen until its own
|
|
239
|
-
// measurement arrives, so gating measurement on visibility deadlocks it
|
|
240
|
-
// there, and RN fires onLayout for off-screen views anyway
|
|
241
167
|
for (const node of Nodes) {
|
|
242
168
|
const parentElement = node.parentElement
|
|
243
169
|
if (!(parentElement instanceof HTMLElement)) {
|
|
@@ -253,45 +179,26 @@ if (ENABLE) {
|
|
|
253
179
|
if (activeNodes.length > 0) {
|
|
254
180
|
const io = ensureRectFetchObserver()
|
|
255
181
|
rectFetchStartTime = performance.now()
|
|
182
|
+
for (let i = 0; i < activeNodes.length; i++) io.observe(activeNodes[i])
|
|
183
|
+
for (const parent of parentsToObserve) io.observe(parent)
|
|
256
184
|
|
|
257
|
-
// observe all nodes
|
|
258
|
-
for (let i = 0; i < activeNodes.length; i++) {
|
|
259
|
-
io.observe(activeNodes[i])
|
|
260
|
-
}
|
|
261
|
-
// optimization: observe unique parents only (not N times for N children)
|
|
262
|
-
for (const parent of parentsToObserve) {
|
|
263
|
-
io.observe(parent)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// wait for callback
|
|
267
185
|
await new Promise<boolean>((res) => {
|
|
268
186
|
rectFetchResolve = res
|
|
269
187
|
})
|
|
270
188
|
|
|
271
|
-
|
|
272
|
-
for (
|
|
273
|
-
io.unobserve(activeNodes[i])
|
|
274
|
-
}
|
|
275
|
-
for (const parent of parentsToObserve) {
|
|
276
|
-
io.unobserve(parent)
|
|
277
|
-
}
|
|
189
|
+
for (let i = 0; i < activeNodes.length; i++) io.unobserve(activeNodes[i])
|
|
190
|
+
for (const parent of parentsToObserve) io.unobserve(parent)
|
|
278
191
|
|
|
279
|
-
// adaptive backoff: if IO was slow, skip more frames next cycle
|
|
280
192
|
if (lastCallbackDelay > 50) {
|
|
281
193
|
skipFrames = Math.min(skipFrames + 2, MAX_SKIP_FRAMES)
|
|
282
194
|
} else if (lastCallbackDelay < 20) {
|
|
283
|
-
// recover back to base rate when things are fast
|
|
284
195
|
skipFrames = Math.max(skipFrames - 1, BASE_SKIP_FRAMES)
|
|
285
196
|
}
|
|
286
197
|
|
|
287
|
-
|
|
288
|
-
for (let i = 0; i < activeNodes.length; i++) {
|
|
289
|
-
updateLayoutIfChanged(activeNodes[i])
|
|
290
|
-
}
|
|
198
|
+
for (let i = 0; i < activeNodes.length; i++) updateLayoutIfChanged(activeNodes[i])
|
|
291
199
|
}
|
|
292
200
|
}
|
|
293
201
|
|
|
294
|
-
// schedule next frame
|
|
295
202
|
frameScheduled = false
|
|
296
203
|
scheduleLayoutFrame()
|
|
297
204
|
}
|
|
@@ -309,48 +216,33 @@ export const getElementLayoutEvent = (
|
|
|
309
216
|
nodeRect: DOMRectReadOnly,
|
|
310
217
|
parentRect: DOMRectReadOnly,
|
|
311
218
|
node?: HTMLElement
|
|
312
|
-
): LayoutEvent => {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
}
|
|
219
|
+
): LayoutEvent => ({
|
|
220
|
+
nativeEvent: {
|
|
221
|
+
layout: getRelativeDimensions(nodeRect, parentRect, node),
|
|
222
|
+
target: nodeRect,
|
|
223
|
+
},
|
|
224
|
+
timeStamp: Date.now(),
|
|
225
|
+
})
|
|
321
226
|
|
|
322
|
-
/**
|
|
323
|
-
* get pre-transform dimensions for a node.
|
|
324
|
-
* uses offsetWidth/offsetHeight which report CSS layout dimensions
|
|
325
|
-
* unaffected by transforms - this matches React Native's onLayout behavior.
|
|
326
|
-
*
|
|
327
|
-
* see: https://github.com/tamagui/tamagui/pull/2329
|
|
328
|
-
*/
|
|
329
227
|
const getPreTransformDimensions = (
|
|
330
228
|
node: HTMLElement
|
|
331
|
-
): { width: number; height: number } => {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
}
|
|
229
|
+
): { width: number; height: number } => ({
|
|
230
|
+
width: node.offsetWidth,
|
|
231
|
+
height: node.offsetHeight,
|
|
232
|
+
})
|
|
337
233
|
|
|
338
234
|
const getRelativeDimensions = (
|
|
339
235
|
a: DOMRectReadOnly,
|
|
340
236
|
b: DOMRectReadOnly,
|
|
341
237
|
aNode?: HTMLElement
|
|
342
238
|
) => {
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
const y = top - b.top
|
|
346
|
-
|
|
347
|
-
// get pre-transform dimensions when flag is enabled and node is available
|
|
239
|
+
const left = a.left - b.left
|
|
240
|
+
const top = a.top - b.top
|
|
348
241
|
const { width, height } =
|
|
349
242
|
usePretransformDimensions() && aNode
|
|
350
243
|
? getPreTransformDimensions(aNode)
|
|
351
244
|
: { width: a.width, height: a.height }
|
|
352
|
-
|
|
353
|
-
return { x, y, width, height, pageX: a.left, pageY: a.top }
|
|
245
|
+
return { x: left, y: top, width, height, pageX: a.left, pageY: a.top }
|
|
354
246
|
}
|
|
355
247
|
|
|
356
248
|
function emitLayoutIfChanged(
|
|
@@ -364,7 +256,6 @@ function emitLayoutIfChanged(
|
|
|
364
256
|
|
|
365
257
|
const cachedRect = NodeRectCache.get(node)
|
|
366
258
|
const cachedParentRect = NodeRectCache.get(parentNode)
|
|
367
|
-
|
|
368
259
|
const nodeChanged = !cachedRect || !rectsEqual(cachedRect, nodeRect)
|
|
369
260
|
const parentChanged = !cachedParentRect || !rectsEqual(cachedParentRect, parentRect)
|
|
370
261
|
|
|
@@ -374,17 +265,6 @@ function emitLayoutIfChanged(
|
|
|
374
265
|
NodeRectCache.set(parentNode, parentRect)
|
|
375
266
|
|
|
376
267
|
const event = getElementLayoutEvent(nodeRect, parentRect, node)
|
|
377
|
-
|
|
378
|
-
if (process.env.NODE_ENV === 'development' && isDebugLayout()) {
|
|
379
|
-
console.log('[useElementLayout] change', {
|
|
380
|
-
tag: node.tagName,
|
|
381
|
-
id: node.id || undefined,
|
|
382
|
-
className: (node.className || '').slice(0, 60) || undefined,
|
|
383
|
-
layout: event.nativeEvent.layout,
|
|
384
|
-
first: !cachedRect,
|
|
385
|
-
})
|
|
386
|
-
}
|
|
387
|
-
|
|
388
268
|
if (avoidUpdates) {
|
|
389
269
|
queuedUpdates.set(node, () => onLayout(event))
|
|
390
270
|
} else {
|
|
@@ -394,15 +274,11 @@ function emitLayoutIfChanged(
|
|
|
394
274
|
|
|
395
275
|
function observeLayoutNode(node: HTMLElement, disableKey?: string) {
|
|
396
276
|
Nodes.add(node)
|
|
397
|
-
if (disableKey)
|
|
398
|
-
|
|
399
|
-
} else {
|
|
400
|
-
LayoutDisableKey.delete(node)
|
|
401
|
-
}
|
|
277
|
+
if (disableKey) LayoutDisableKey.set(node, disableKey)
|
|
278
|
+
else LayoutDisableKey.delete(node)
|
|
402
279
|
resumeLayoutLoop?.()
|
|
403
280
|
}
|
|
404
281
|
|
|
405
|
-
// register an arbitrary DOM element into the measurement loop without React lifecycle
|
|
406
282
|
export function registerLayoutNode(
|
|
407
283
|
node: HTMLElement,
|
|
408
284
|
onChange: () => void,
|
|
@@ -420,12 +296,6 @@ function cleanupNode(node: HTMLElement) {
|
|
|
420
296
|
NodeRectCache.delete(node)
|
|
421
297
|
}
|
|
422
298
|
|
|
423
|
-
const PrevHostNode = new WeakMap<object, HTMLElement | undefined>()
|
|
424
|
-
|
|
425
|
-
// spec: onLayout fires one synchronous initial event on mount and on host swap
|
|
426
|
-
// (RN parity, and before-paint so consumers can position without flicker).
|
|
427
|
-
// bypasses the avoidUpdates queue on purpose; seeds the rect cache so the
|
|
428
|
-
// measurement loop doesn't re-emit an identical event a frame later.
|
|
429
299
|
function emitLayoutSync(node: HTMLElement) {
|
|
430
300
|
const onLayout = LayoutHandlers.get(node)
|
|
431
301
|
if (typeof onLayout !== 'function') return
|
|
@@ -444,85 +314,51 @@ export function useElementLayout(
|
|
|
444
314
|
onLayout?: ((e: LayoutEvent) => void) | null
|
|
445
315
|
): void {
|
|
446
316
|
const disableKey = useContext(DisableLayoutContextKey)
|
|
447
|
-
|
|
448
|
-
// keep handlers up to date so polling always calls the latest callback
|
|
449
317
|
const node = ensureWebElement(ref.current?.host)
|
|
450
318
|
if (node && onLayout) {
|
|
451
319
|
LayoutHandlers.set(node, onLayout)
|
|
452
320
|
LayoutDisableKey.set(node, disableKey)
|
|
453
321
|
}
|
|
454
322
|
|
|
455
|
-
// detect mounts + host swaps after commit and fire the immediate sync layout event
|
|
456
323
|
useIsomorphicLayoutEffect(() => {
|
|
457
324
|
if (!onLayout) return
|
|
458
325
|
const nextNode = ensureWebElement(ref.current?.host)
|
|
459
326
|
const prevNode = PrevHostNode.get(ref)
|
|
460
|
-
if (nextNode
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
emitLayoutSync(nextNode)
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
useIsomorphicLayoutEffect(() => {
|
|
472
|
-
if (!onLayout) return
|
|
473
|
-
const node = ref.current?.host
|
|
474
|
-
if (!node) return
|
|
475
|
-
|
|
476
|
-
LayoutHandlers.set(node, onLayout)
|
|
477
|
-
observeLayoutNode(node, disableKey)
|
|
478
|
-
|
|
479
|
-
if (process.env.NODE_ENV === 'development' && isDebugLayout()) {
|
|
480
|
-
console.log('[useElementLayout] register', {
|
|
481
|
-
tag: node.tagName,
|
|
482
|
-
id: node.id || undefined,
|
|
483
|
-
className: (node.className || '').slice(0, 60) || undefined,
|
|
484
|
-
totalNodes: Nodes.size,
|
|
485
|
-
})
|
|
327
|
+
if (nextNode !== prevNode) {
|
|
328
|
+
if (prevNode) cleanupNode(prevNode)
|
|
329
|
+
PrevHostNode.set(ref, nextNode)
|
|
330
|
+
if (nextNode) {
|
|
331
|
+
LayoutHandlers.set(nextNode, onLayout)
|
|
332
|
+
observeLayoutNode(nextNode, disableKey)
|
|
333
|
+
emitLayoutSync(nextNode)
|
|
334
|
+
}
|
|
486
335
|
}
|
|
487
|
-
|
|
488
336
|
return () => {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if (swappedNode && swappedNode !== node) {
|
|
494
|
-
cleanupNode(swappedNode)
|
|
495
|
-
}
|
|
337
|
+
const activeNode = ensureWebElement(ref.current?.host)
|
|
338
|
+
if (activeNode) cleanupNode(activeNode)
|
|
339
|
+
const swapped = PrevHostNode.get(ref)
|
|
340
|
+
if (swapped && swapped !== activeNode) cleanupNode(swapped)
|
|
496
341
|
PrevHostNode.delete(ref)
|
|
497
342
|
}
|
|
498
343
|
}, [ref, !!onLayout])
|
|
499
344
|
}
|
|
500
345
|
|
|
501
346
|
function ensureWebElement<X>(x: X): HTMLElement | undefined {
|
|
502
|
-
if (typeof HTMLElement === 'undefined')
|
|
503
|
-
return undefined
|
|
504
|
-
}
|
|
347
|
+
if (typeof HTMLElement === 'undefined') return undefined
|
|
505
348
|
return x instanceof HTMLElement ? x : undefined
|
|
506
349
|
}
|
|
507
350
|
|
|
508
351
|
export const getBoundingClientRectAsync = (
|
|
509
352
|
node: HTMLElement | null
|
|
510
|
-
): Promise<DOMRectReadOnly | false> =>
|
|
511
|
-
|
|
353
|
+
): Promise<DOMRectReadOnly | false> =>
|
|
354
|
+
new Promise((res) => {
|
|
512
355
|
if (!node || node.nodeType !== 1) return res(false)
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
(entries)
|
|
516
|
-
|
|
517
|
-
return res(entries[0].boundingClientRect)
|
|
518
|
-
},
|
|
519
|
-
{
|
|
520
|
-
threshold: 0,
|
|
521
|
-
}
|
|
522
|
-
)
|
|
356
|
+
const io = new IntersectionObserver((entries) => {
|
|
357
|
+
io.disconnect()
|
|
358
|
+
res(entries[0].boundingClientRect)
|
|
359
|
+
})
|
|
523
360
|
io.observe(node)
|
|
524
361
|
})
|
|
525
|
-
}
|
|
526
362
|
|
|
527
363
|
export const measureNode = async (
|
|
528
364
|
node: HTMLElement,
|
|
@@ -541,9 +377,13 @@ export const measureNode = async (
|
|
|
541
377
|
return null
|
|
542
378
|
}
|
|
543
379
|
|
|
544
|
-
type MeasureInWindowCb = (
|
|
545
|
-
|
|
546
|
-
|
|
380
|
+
export type MeasureInWindowCb = (
|
|
381
|
+
x: number,
|
|
382
|
+
y: number,
|
|
383
|
+
width: number,
|
|
384
|
+
height: number
|
|
385
|
+
) => void
|
|
386
|
+
export type MeasureCb = (
|
|
547
387
|
x: number,
|
|
548
388
|
y: number,
|
|
549
389
|
width: number,
|
|
@@ -551,45 +391,35 @@ type MeasureCb = (
|
|
|
551
391
|
pageX: number,
|
|
552
392
|
pageY: number
|
|
553
393
|
) => void
|
|
394
|
+
export type WindowLayout = { pageX: number; pageY: number; width: number; height: number }
|
|
554
395
|
|
|
555
396
|
export const measure = async (
|
|
556
397
|
node: HTMLElement,
|
|
557
398
|
callback: MeasureCb
|
|
558
399
|
): Promise<LayoutValue | null> => {
|
|
559
|
-
const out = await measureNode(
|
|
560
|
-
|
|
561
|
-
node.parentNode instanceof HTMLElement ? node.parentNode : null
|
|
562
|
-
)
|
|
563
|
-
if (out) {
|
|
564
|
-
callback?.(out.x, out.y, out.width, out.height, out.pageX, out.pageY)
|
|
565
|
-
}
|
|
400
|
+
const out = await measureNode(node, node.parentElement)
|
|
401
|
+
if (out) callback?.(out.x, out.y, out.width, out.height, out.pageX, out.pageY)
|
|
566
402
|
return out
|
|
567
403
|
}
|
|
568
404
|
|
|
569
|
-
export
|
|
570
|
-
node: HTMLElement
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
type WindowLayout = { pageX: number; pageY: number; width: number; height: number }
|
|
405
|
+
export const createMeasure =
|
|
406
|
+
(node: HTMLElement): ((callback: MeasureCb) => Promise<LayoutValue | null>) =>
|
|
407
|
+
(callback: MeasureCb) =>
|
|
408
|
+
measure(node, callback)
|
|
576
409
|
|
|
577
410
|
export const measureInWindow = async (
|
|
578
411
|
node: HTMLElement,
|
|
579
412
|
callback: MeasureInWindowCb
|
|
580
413
|
): Promise<WindowLayout | null> => {
|
|
581
414
|
const out = await measureNode(node, null)
|
|
582
|
-
if (out)
|
|
583
|
-
callback?.(out.pageX, out.pageY, out.width, out.height)
|
|
584
|
-
}
|
|
415
|
+
if (out) callback?.(out.pageX, out.pageY, out.width, out.height)
|
|
585
416
|
return out
|
|
586
417
|
}
|
|
587
418
|
|
|
588
|
-
export const createMeasureInWindow =
|
|
589
|
-
node: HTMLElement
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
}
|
|
419
|
+
export const createMeasureInWindow =
|
|
420
|
+
(node: HTMLElement): ((callback: MeasureInWindowCb) => Promise<WindowLayout | null>) =>
|
|
421
|
+
(callback: MeasureInWindowCb) =>
|
|
422
|
+
measureInWindow(node, callback)
|
|
593
423
|
|
|
594
424
|
export const measureLayout = async (
|
|
595
425
|
node: HTMLElement,
|
|
@@ -597,14 +427,13 @@ export const measureLayout = async (
|
|
|
597
427
|
callback: MeasureCb
|
|
598
428
|
): Promise<LayoutValue | null> => {
|
|
599
429
|
const out = await measureNode(node, relativeNode)
|
|
600
|
-
if (out)
|
|
601
|
-
callback?.(out.x, out.y, out.width, out.height, out.pageX, out.pageY)
|
|
602
|
-
}
|
|
430
|
+
if (out) callback?.(out.x, out.y, out.width, out.height, out.pageX, out.pageY)
|
|
603
431
|
return out
|
|
604
432
|
}
|
|
605
433
|
|
|
606
|
-
export
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
434
|
+
export const createMeasureLayout =
|
|
435
|
+
(
|
|
436
|
+
node: HTMLElement
|
|
437
|
+
): ((relativeTo: HTMLElement, callback: MeasureCb) => Promise<LayoutValue | null>) =>
|
|
438
|
+
(relativeTo: HTMLElement, callback: MeasureCb) =>
|
|
439
|
+
measureLayout(node, relativeTo, callback)
|
package/types/index.d.ts
CHANGED
|
@@ -6,8 +6,6 @@ export declare const LayoutMeasurementController: ({ disable, children }: {
|
|
|
6
6
|
type TamaguiComponentStatePartial = {
|
|
7
7
|
host?: any;
|
|
8
8
|
};
|
|
9
|
-
type LayoutMeasurementStrategy = "off" | "sync" | "async";
|
|
10
|
-
export declare function setOnLayoutStrategy(state: LayoutMeasurementStrategy): void;
|
|
11
9
|
export type LayoutValue = {
|
|
12
10
|
x: number;
|
|
13
11
|
y: number;
|
|
@@ -29,20 +27,20 @@ export declare function registerLayoutNode(node: HTMLElement, onChange: () => vo
|
|
|
29
27
|
export declare function useElementLayout(ref: RefObject<TamaguiComponentStatePartial>, onLayout?: ((e: LayoutEvent) => void) | null): void;
|
|
30
28
|
export declare const getBoundingClientRectAsync: (node: HTMLElement | null) => Promise<DOMRectReadOnly | false>;
|
|
31
29
|
export declare const measureNode: (node: HTMLElement, relativeTo?: HTMLElement | null) => Promise<null | LayoutValue>;
|
|
32
|
-
type MeasureInWindowCb = (x: number, y: number, width: number, height: number) => void;
|
|
33
|
-
type MeasureCb = (x: number, y: number, width: number, height: number, pageX: number, pageY: number) => void;
|
|
34
|
-
export
|
|
35
|
-
export declare function createMeasure(node: HTMLElement): (callback: MeasureCb) => Promise<LayoutValue | null>;
|
|
36
|
-
type WindowLayout = {
|
|
30
|
+
export type MeasureInWindowCb = (x: number, y: number, width: number, height: number) => void;
|
|
31
|
+
export type MeasureCb = (x: number, y: number, width: number, height: number, pageX: number, pageY: number) => void;
|
|
32
|
+
export type WindowLayout = {
|
|
37
33
|
pageX: number;
|
|
38
34
|
pageY: number;
|
|
39
35
|
width: number;
|
|
40
36
|
height: number;
|
|
41
37
|
};
|
|
38
|
+
export declare const measure: (node: HTMLElement, callback: MeasureCb) => Promise<LayoutValue | null>;
|
|
39
|
+
export declare const createMeasure: (node: HTMLElement) => ((callback: MeasureCb) => Promise<LayoutValue | null>);
|
|
42
40
|
export declare const measureInWindow: (node: HTMLElement, callback: MeasureInWindowCb) => Promise<WindowLayout | null>;
|
|
43
41
|
export declare const createMeasureInWindow: (node: HTMLElement) => ((callback: MeasureInWindowCb) => Promise<WindowLayout | null>);
|
|
44
42
|
export declare const measureLayout: (node: HTMLElement, relativeNode: HTMLElement, callback: MeasureCb) => Promise<LayoutValue | null>;
|
|
45
|
-
export declare
|
|
43
|
+
export declare const createMeasureLayout: (node: HTMLElement) => ((relativeTo: HTMLElement, callback: MeasureCb) => Promise<LayoutValue | null>);
|
|
46
44
|
export {};
|
|
47
45
|
|
|
48
46
|
//# sourceMappingURL=index.d.ts.map
|