@hanzogui/use-element-layout 8.3.1 → 8.3.2
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.js +444 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/index.js +381 -278
- package/dist/esm/index.js.map +1 -1
- package/package.json +20 -15
- package/types/index.d.ts +21 -22
- package/types/index.d.ts.map +1 -11
- package/dist/cjs/index.cjs +0 -362
- package/dist/cjs/index.native.js +0 -435
- package/dist/cjs/index.native.js.map +0 -1
- package/dist/esm/index.mjs +0 -324
- package/dist/esm/index.mjs.map +0 -1
- package/dist/esm/index.native.js +0 -394
- package/dist/esm/index.native.js.map +0 -1
package/dist/esm/index.js
CHANGED
|
@@ -1,324 +1,427 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
const LayoutHandlers =
|
|
5
|
-
const LayoutDisableKey =
|
|
6
|
-
const Nodes =
|
|
7
|
-
const IntersectionState =
|
|
8
|
-
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useIsomorphicLayoutEffect } from '@hanzogui/constants';
|
|
3
|
+
import { createContext, useContext, useId } from 'react';
|
|
4
|
+
const LayoutHandlers = new WeakMap();
|
|
5
|
+
const LayoutDisableKey = new WeakMap();
|
|
6
|
+
const Nodes = new Set();
|
|
7
|
+
const IntersectionState = new WeakMap();
|
|
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/hanzoai/gui/pull/2329
|
|
11
|
+
const usePretransformDimensions = () => globalThis.__GUI_ONLAYOUT_PRETRANSFORM === true ||
|
|
12
|
+
process.env.GUI_ONLAYOUT_PRETRANSFORM === '1';
|
|
9
13
|
let _debugLayout;
|
|
10
14
|
function isDebugLayout() {
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
if (_debugLayout === undefined) {
|
|
16
|
+
_debugLayout =
|
|
17
|
+
typeof window !== 'undefined' &&
|
|
18
|
+
new URLSearchParams(window.location.search).has('__tamaDebugLayout');
|
|
19
|
+
}
|
|
20
|
+
return _debugLayout;
|
|
13
21
|
}
|
|
22
|
+
// separating to avoid all re-rendering
|
|
14
23
|
const DisableLayoutContextValues = {};
|
|
15
|
-
const DisableLayoutContextKey = createContext(
|
|
16
|
-
const ENABLE = typeof IntersectionObserver !==
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}) => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
value: id,
|
|
27
|
-
children
|
|
28
|
-
});
|
|
24
|
+
const DisableLayoutContextKey = createContext('');
|
|
25
|
+
const ENABLE = "web" === 'web' && typeof IntersectionObserver !== 'undefined';
|
|
26
|
+
// internal testing - advanced helper to turn off layout measurement for extra performance
|
|
27
|
+
// TODO document!
|
|
28
|
+
// TODO could add frame skip control here
|
|
29
|
+
export const LayoutMeasurementController = ({ disable, children, }) => {
|
|
30
|
+
const id = useId();
|
|
31
|
+
useIsomorphicLayoutEffect(() => {
|
|
32
|
+
DisableLayoutContextValues[id] = disable;
|
|
33
|
+
}, [disable, id]);
|
|
34
|
+
return (_jsx(DisableLayoutContextKey.Provider, { value: id, children: children }));
|
|
29
35
|
};
|
|
36
|
+
// Single persistent IntersectionObserver for visibility tracking
|
|
30
37
|
let globalIntersectionObserver = null;
|
|
31
|
-
let strategy =
|
|
32
|
-
function setOnLayoutStrategy(state) {
|
|
33
|
-
|
|
38
|
+
let strategy = 'async';
|
|
39
|
+
export function setOnLayoutStrategy(state) {
|
|
40
|
+
strategy = state;
|
|
34
41
|
}
|
|
35
|
-
const NodeRectCache =
|
|
42
|
+
const NodeRectCache = new WeakMap();
|
|
43
|
+
// prevent thrashing during first hydration (somewhat, streaming gets trickier)
|
|
36
44
|
let avoidUpdates = true;
|
|
37
|
-
const queuedUpdates =
|
|
38
|
-
function enable() {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
const queuedUpdates = new Map();
|
|
46
|
+
export function enable() {
|
|
47
|
+
if (avoidUpdates) {
|
|
48
|
+
avoidUpdates = false;
|
|
49
|
+
if (queuedUpdates) {
|
|
50
|
+
queuedUpdates.forEach((cb) => cb());
|
|
51
|
+
queuedUpdates.clear();
|
|
52
|
+
}
|
|
44
53
|
}
|
|
45
|
-
}
|
|
46
54
|
}
|
|
47
55
|
function startGlobalObservers() {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
if (!ENABLE || globalIntersectionObserver)
|
|
57
|
+
return;
|
|
58
|
+
globalIntersectionObserver = new IntersectionObserver((entries) => {
|
|
59
|
+
for (let i = 0; i < entries.length; i++) {
|
|
60
|
+
const entry = entries[i];
|
|
61
|
+
const node = entry.target;
|
|
62
|
+
if (IntersectionState.get(node) !== entry.isIntersecting) {
|
|
63
|
+
IntersectionState.set(node, entry.isIntersecting);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}, {
|
|
67
|
+
threshold: 0,
|
|
68
|
+
});
|
|
58
69
|
}
|
|
70
|
+
// optimization: inline rect comparison to avoid function call overhead on hot path
|
|
59
71
|
function rectsEqual(a, b) {
|
|
60
|
-
|
|
72
|
+
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
|
|
61
73
|
}
|
|
62
74
|
if (ENABLE) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
rectFetchObserver =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
parentRect = BoundingRects.get(parentNode);
|
|
92
|
-
if (!nodeRect || !parentRect) return;
|
|
93
|
-
} else {
|
|
94
|
-
nodeRect = node.getBoundingClientRect();
|
|
95
|
-
parentRect = parentNode.getBoundingClientRect();
|
|
75
|
+
const BoundingRects = new WeakMap();
|
|
76
|
+
// optimization: persistent IO for rect fetching, reused across cycles
|
|
77
|
+
let rectFetchObserver = null;
|
|
78
|
+
let rectFetchResolve = null;
|
|
79
|
+
let rectFetchStartTime = 0;
|
|
80
|
+
let lastCallbackDelay = 0;
|
|
81
|
+
function ensureRectFetchObserver() {
|
|
82
|
+
if (rectFetchObserver)
|
|
83
|
+
return rectFetchObserver;
|
|
84
|
+
rectFetchObserver = new IntersectionObserver((entries) => {
|
|
85
|
+
lastCallbackDelay = Math.round(performance.now() - rectFetchStartTime);
|
|
86
|
+
// store all rects
|
|
87
|
+
for (let i = 0; i < entries.length; i++) {
|
|
88
|
+
BoundingRects.set(entries[i].target, entries[i].boundingClientRect);
|
|
89
|
+
}
|
|
90
|
+
if (process.env.NODE_ENV === 'development' &&
|
|
91
|
+
isDebugLayout() &&
|
|
92
|
+
lastCallbackDelay > 50) {
|
|
93
|
+
console.warn('[onLayout-io-delay]', lastCallbackDelay + 'ms', entries.length, 'entries');
|
|
94
|
+
}
|
|
95
|
+
if (rectFetchResolve) {
|
|
96
|
+
rectFetchResolve(true);
|
|
97
|
+
rectFetchResolve = null;
|
|
98
|
+
}
|
|
99
|
+
}, {
|
|
100
|
+
threshold: 0,
|
|
101
|
+
});
|
|
102
|
+
return rectFetchObserver;
|
|
96
103
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
async function updateLayoutIfChanged(node) {
|
|
105
|
+
const onLayout = LayoutHandlers.get(node);
|
|
106
|
+
if (typeof onLayout !== 'function')
|
|
107
|
+
return;
|
|
108
|
+
const parentNode = node.parentElement;
|
|
109
|
+
if (!parentNode)
|
|
110
|
+
return;
|
|
111
|
+
let nodeRect;
|
|
112
|
+
let parentRect;
|
|
113
|
+
// respect the strategy contract
|
|
114
|
+
if (strategy === 'async') {
|
|
115
|
+
nodeRect = BoundingRects.get(node);
|
|
116
|
+
parentRect = BoundingRects.get(parentNode);
|
|
117
|
+
if (!nodeRect || !parentRect) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
nodeRect = node.getBoundingClientRect();
|
|
123
|
+
parentRect = parentNode.getBoundingClientRect();
|
|
124
|
+
}
|
|
125
|
+
emitLayoutIfChanged(node, parentNode, nodeRect, parentRect);
|
|
109
126
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
const rAF = typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame : undefined;
|
|
128
|
+
// adaptive frame skipping with backoff
|
|
129
|
+
const userSkipVal = process.env.GUI_LAYOUT_FRAME_SKIP;
|
|
130
|
+
const BASE_SKIP_FRAMES = userSkipVal ? +userSkipVal : 10;
|
|
131
|
+
const MAX_SKIP_FRAMES = 20;
|
|
132
|
+
let skipFrames = BASE_SKIP_FRAMES;
|
|
133
|
+
let frameCount = 0;
|
|
134
|
+
async function layoutOnAnimationFrame() {
|
|
135
|
+
// skip frames based on adaptive rate
|
|
136
|
+
if (frameCount++ % skipFrames !== 0) {
|
|
137
|
+
rAF ? rAF(layoutOnAnimationFrame) : setTimeout(layoutOnAnimationFrame, 16);
|
|
138
|
+
return;
|
|
119
139
|
}
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
// reset frame count to avoid overflow
|
|
141
|
+
if (frameCount >= Number.MAX_SAFE_INTEGER) {
|
|
142
|
+
frameCount = 0;
|
|
143
|
+
}
|
|
144
|
+
if (strategy !== 'off') {
|
|
145
|
+
const visibleNodes = [];
|
|
146
|
+
// optimization: deduplicate parent observations
|
|
147
|
+
const parentsToObserve = new Set();
|
|
148
|
+
// collect visible nodes and their unique parents
|
|
149
|
+
for (const node of Nodes) {
|
|
150
|
+
const parentElement = node.parentElement;
|
|
151
|
+
if (!(parentElement instanceof HTMLElement)) {
|
|
152
|
+
cleanupNode(node);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const disableKey = LayoutDisableKey.get(node);
|
|
156
|
+
if (disableKey && DisableLayoutContextValues[disableKey] === true)
|
|
157
|
+
continue;
|
|
158
|
+
if (IntersectionState.get(node) === false)
|
|
159
|
+
continue;
|
|
160
|
+
visibleNodes.push(node);
|
|
161
|
+
parentsToObserve.add(parentElement);
|
|
162
|
+
}
|
|
163
|
+
if (visibleNodes.length > 0) {
|
|
164
|
+
const io = ensureRectFetchObserver();
|
|
165
|
+
rectFetchStartTime = performance.now();
|
|
166
|
+
// observe all nodes
|
|
167
|
+
for (let i = 0; i < visibleNodes.length; i++) {
|
|
168
|
+
io.observe(visibleNodes[i]);
|
|
169
|
+
}
|
|
170
|
+
// optimization: observe unique parents only (not N times for N children)
|
|
171
|
+
for (const parent of parentsToObserve) {
|
|
172
|
+
io.observe(parent);
|
|
173
|
+
}
|
|
174
|
+
// wait for callback
|
|
175
|
+
await new Promise((res) => {
|
|
176
|
+
rectFetchResolve = res;
|
|
177
|
+
});
|
|
178
|
+
// unobserve all to reset for next cycle
|
|
179
|
+
for (let i = 0; i < visibleNodes.length; i++) {
|
|
180
|
+
io.unobserve(visibleNodes[i]);
|
|
181
|
+
}
|
|
182
|
+
for (const parent of parentsToObserve) {
|
|
183
|
+
io.unobserve(parent);
|
|
184
|
+
}
|
|
185
|
+
// adaptive backoff: if IO was slow, skip more frames next cycle
|
|
186
|
+
if (lastCallbackDelay > 50) {
|
|
187
|
+
skipFrames = Math.min(skipFrames + 2, MAX_SKIP_FRAMES);
|
|
188
|
+
}
|
|
189
|
+
else if (lastCallbackDelay < 20) {
|
|
190
|
+
// recover back to base rate when things are fast
|
|
191
|
+
skipFrames = Math.max(skipFrames - 1, BASE_SKIP_FRAMES);
|
|
192
|
+
}
|
|
193
|
+
// process updates
|
|
194
|
+
for (let i = 0; i < visibleNodes.length; i++) {
|
|
195
|
+
updateLayoutIfChanged(visibleNodes[i]);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// schedule next frame
|
|
200
|
+
rAF ? rAF(layoutOnAnimationFrame) : setTimeout(layoutOnAnimationFrame, 16);
|
|
139
201
|
}
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
layoutOnAnimationFrame();
|
|
202
|
+
layoutOnAnimationFrame();
|
|
143
203
|
}
|
|
144
|
-
const getElementLayoutEvent = (nodeRect, parentRect, node) => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
204
|
+
export const getElementLayoutEvent = (nodeRect, parentRect, node) => {
|
|
205
|
+
return {
|
|
206
|
+
nativeEvent: {
|
|
207
|
+
layout: getRelativeDimensions(nodeRect, parentRect, node),
|
|
208
|
+
target: nodeRect,
|
|
209
|
+
},
|
|
210
|
+
timeStamp: Date.now(),
|
|
211
|
+
};
|
|
152
212
|
};
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
213
|
+
/**
|
|
214
|
+
* get pre-transform dimensions for a node.
|
|
215
|
+
* uses offsetWidth/offsetHeight which report CSS layout dimensions
|
|
216
|
+
* unaffected by transforms - this matches React Native's onLayout behavior.
|
|
217
|
+
*
|
|
218
|
+
* see: https://github.com/hanzoai/gui/pull/2329
|
|
219
|
+
*/
|
|
220
|
+
const getPreTransformDimensions = (node) => {
|
|
221
|
+
return {
|
|
222
|
+
width: node.offsetWidth,
|
|
223
|
+
height: node.offsetHeight,
|
|
224
|
+
};
|
|
158
225
|
};
|
|
159
226
|
const getRelativeDimensions = (a, b, aNode) => {
|
|
160
|
-
|
|
161
|
-
left
|
|
162
|
-
top
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
width,
|
|
168
|
-
height
|
|
169
|
-
} = usePretransformDimensions() && aNode ? getPreTransformDimensions(aNode) : {
|
|
170
|
-
width: a.width,
|
|
171
|
-
height: a.height
|
|
172
|
-
};
|
|
173
|
-
return {
|
|
174
|
-
x,
|
|
175
|
-
y,
|
|
176
|
-
width,
|
|
177
|
-
height,
|
|
178
|
-
pageX: a.left,
|
|
179
|
-
pageY: a.top
|
|
180
|
-
};
|
|
227
|
+
const { left, top } = a;
|
|
228
|
+
const x = left - b.left;
|
|
229
|
+
const y = top - b.top;
|
|
230
|
+
// get pre-transform dimensions when flag is enabled and node is available
|
|
231
|
+
const { width, height } = usePretransformDimensions() && aNode
|
|
232
|
+
? getPreTransformDimensions(aNode)
|
|
233
|
+
: { width: a.width, height: a.height };
|
|
234
|
+
return { x, y, width, height, pageX: a.left, pageY: a.top };
|
|
181
235
|
};
|
|
182
236
|
function emitLayoutIfChanged(node, parentNode, nodeRect, parentRect) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
237
|
+
const onLayout = LayoutHandlers.get(node);
|
|
238
|
+
if (typeof onLayout !== 'function')
|
|
239
|
+
return;
|
|
240
|
+
const cachedRect = NodeRectCache.get(node);
|
|
241
|
+
const cachedParentRect = NodeRectCache.get(parentNode);
|
|
242
|
+
const nodeChanged = !cachedRect || !rectsEqual(cachedRect, nodeRect);
|
|
243
|
+
const parentChanged = !cachedParentRect || !rectsEqual(cachedParentRect, parentRect);
|
|
244
|
+
if (!nodeChanged && !parentChanged)
|
|
245
|
+
return;
|
|
246
|
+
NodeRectCache.set(node, nodeRect);
|
|
247
|
+
NodeRectCache.set(parentNode, parentRect);
|
|
248
|
+
const event = getElementLayoutEvent(nodeRect, parentRect, node);
|
|
249
|
+
if (process.env.NODE_ENV === 'development' && isDebugLayout()) {
|
|
250
|
+
console.log('[useElementLayout] change', {
|
|
251
|
+
tag: node.tagName,
|
|
252
|
+
id: node.id || undefined,
|
|
253
|
+
className: (node.className || '').slice(0, 60) || undefined,
|
|
254
|
+
layout: event.nativeEvent.layout,
|
|
255
|
+
first: !cachedRect,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (avoidUpdates) {
|
|
259
|
+
queuedUpdates.set(node, () => onLayout(event));
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
onLayout(event);
|
|
263
|
+
}
|
|
201
264
|
}
|
|
202
265
|
function observeLayoutNode(node, disableKey) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
266
|
+
Nodes.add(node);
|
|
267
|
+
if (disableKey) {
|
|
268
|
+
LayoutDisableKey.set(node, disableKey);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
LayoutDisableKey.delete(node);
|
|
272
|
+
}
|
|
273
|
+
startGlobalObservers();
|
|
274
|
+
if (globalIntersectionObserver) {
|
|
275
|
+
globalIntersectionObserver.observe(node);
|
|
276
|
+
IntersectionState.set(node, true);
|
|
277
|
+
}
|
|
210
278
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
279
|
+
// register an arbitrary DOM element into the measurement loop without React lifecycle
|
|
280
|
+
export function registerLayoutNode(node, onChange, disableKey) {
|
|
281
|
+
LayoutHandlers.set(node, onChange);
|
|
282
|
+
observeLayoutNode(node, disableKey);
|
|
283
|
+
return () => cleanupNode(node);
|
|
215
284
|
}
|
|
216
285
|
function cleanupNode(node) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
286
|
+
Nodes.delete(node);
|
|
287
|
+
LayoutHandlers.delete(node);
|
|
288
|
+
LayoutDisableKey.delete(node);
|
|
289
|
+
NodeRectCache.delete(node);
|
|
290
|
+
IntersectionState.delete(node);
|
|
291
|
+
if (globalIntersectionObserver) {
|
|
292
|
+
globalIntersectionObserver.unobserve(node);
|
|
293
|
+
}
|
|
223
294
|
}
|
|
224
|
-
const PrevHostNode =
|
|
295
|
+
const PrevHostNode = new WeakMap();
|
|
296
|
+
// spec: onLayout fires one synchronous initial event on mount and on host swap
|
|
297
|
+
// (RN parity, and before-paint so consumers can position without flicker).
|
|
298
|
+
// bypasses the avoidUpdates queue on purpose; seeds the rect cache so the
|
|
299
|
+
// measurement loop doesn't re-emit an identical event a frame later.
|
|
225
300
|
function emitLayoutSync(node) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
301
|
+
const onLayout = LayoutHandlers.get(node);
|
|
302
|
+
if (typeof onLayout !== 'function')
|
|
303
|
+
return;
|
|
304
|
+
const parentNode = node.parentElement;
|
|
305
|
+
if (!parentNode)
|
|
306
|
+
return;
|
|
307
|
+
const nodeRect = node.getBoundingClientRect();
|
|
308
|
+
const parentRect = parentNode.getBoundingClientRect();
|
|
309
|
+
NodeRectCache.set(node, nodeRect);
|
|
310
|
+
NodeRectCache.set(parentNode, parentRect);
|
|
311
|
+
onLayout(getElementLayoutEvent(nodeRect, parentRect, node));
|
|
235
312
|
}
|
|
236
|
-
function useElementLayout(ref, onLayout) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
observeLayoutNode(node2, disableKey);
|
|
261
|
-
if (process.env.NODE_ENV === "development" && isDebugLayout()) console.log("[useElementLayout] register", {
|
|
262
|
-
tag: node2.tagName,
|
|
263
|
-
id: node2.id || void 0,
|
|
264
|
-
className: (node2.className || "").slice(0, 60) || void 0,
|
|
265
|
-
totalNodes: Nodes.size
|
|
313
|
+
export function useElementLayout(ref, onLayout) {
|
|
314
|
+
const disableKey = useContext(DisableLayoutContextKey);
|
|
315
|
+
// keep handlers up to date so polling always calls the latest callback
|
|
316
|
+
const node = ensureWebElement(ref.current?.host);
|
|
317
|
+
if (node && onLayout) {
|
|
318
|
+
LayoutHandlers.set(node, onLayout);
|
|
319
|
+
LayoutDisableKey.set(node, disableKey);
|
|
320
|
+
}
|
|
321
|
+
// detect mounts + host swaps after commit and fire the immediate sync layout event
|
|
322
|
+
useIsomorphicLayoutEffect(() => {
|
|
323
|
+
if (!onLayout)
|
|
324
|
+
return;
|
|
325
|
+
const nextNode = ensureWebElement(ref.current?.host);
|
|
326
|
+
const prevNode = PrevHostNode.get(ref);
|
|
327
|
+
if (nextNode === prevNode)
|
|
328
|
+
return;
|
|
329
|
+
if (prevNode)
|
|
330
|
+
cleanupNode(prevNode);
|
|
331
|
+
PrevHostNode.set(ref, nextNode);
|
|
332
|
+
if (!nextNode)
|
|
333
|
+
return;
|
|
334
|
+
LayoutHandlers.set(nextNode, onLayout);
|
|
335
|
+
observeLayoutNode(nextNode, disableKey);
|
|
336
|
+
emitLayoutSync(nextNode);
|
|
266
337
|
});
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
338
|
+
useIsomorphicLayoutEffect(() => {
|
|
339
|
+
if (!onLayout)
|
|
340
|
+
return;
|
|
341
|
+
const node = ref.current?.host;
|
|
342
|
+
if (!node)
|
|
343
|
+
return;
|
|
344
|
+
LayoutHandlers.set(node, onLayout);
|
|
345
|
+
observeLayoutNode(node, disableKey);
|
|
346
|
+
if (process.env.NODE_ENV === 'development' && isDebugLayout()) {
|
|
347
|
+
console.log('[useElementLayout] register', {
|
|
348
|
+
tag: node.tagName,
|
|
349
|
+
id: node.id || undefined,
|
|
350
|
+
className: (node.className || '').slice(0, 60) || undefined,
|
|
351
|
+
totalNodes: Nodes.size,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return () => {
|
|
355
|
+
cleanupNode(node);
|
|
356
|
+
// also clean up any node from a mid-lifecycle host swap
|
|
357
|
+
const swappedNode = PrevHostNode.get(ref);
|
|
358
|
+
if (swappedNode && swappedNode !== node) {
|
|
359
|
+
cleanupNode(swappedNode);
|
|
360
|
+
}
|
|
361
|
+
PrevHostNode.delete(ref);
|
|
362
|
+
};
|
|
363
|
+
}, [ref, !!onLayout]);
|
|
274
364
|
}
|
|
275
365
|
function ensureWebElement(x) {
|
|
276
|
-
|
|
277
|
-
|
|
366
|
+
if (typeof HTMLElement === 'undefined') {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
return x instanceof HTMLElement ? x : undefined;
|
|
278
370
|
}
|
|
279
|
-
const getBoundingClientRectAsync = node => {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
371
|
+
export const getBoundingClientRectAsync = (node) => {
|
|
372
|
+
return new Promise((res) => {
|
|
373
|
+
if (!node || node.nodeType !== 1)
|
|
374
|
+
return res(false);
|
|
375
|
+
const io = new IntersectionObserver((entries) => {
|
|
376
|
+
io.disconnect();
|
|
377
|
+
return res(entries[0].boundingClientRect);
|
|
378
|
+
}, {
|
|
379
|
+
threshold: 0,
|
|
380
|
+
});
|
|
381
|
+
io.observe(node);
|
|
287
382
|
});
|
|
288
|
-
io.observe(node);
|
|
289
|
-
});
|
|
290
383
|
};
|
|
291
|
-
const measureNode = async (node, relativeTo) => {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
384
|
+
export const measureNode = async (node, relativeTo) => {
|
|
385
|
+
const relativeNode = relativeTo || node?.parentElement;
|
|
386
|
+
if (relativeNode instanceof HTMLElement) {
|
|
387
|
+
const [nodeDim, relativeNodeDim] = await Promise.all([
|
|
388
|
+
getBoundingClientRectAsync(node),
|
|
389
|
+
getBoundingClientRectAsync(relativeNode),
|
|
390
|
+
]);
|
|
391
|
+
if (relativeNodeDim && nodeDim) {
|
|
392
|
+
return getRelativeDimensions(nodeDim, relativeNodeDim, node);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return null;
|
|
298
396
|
};
|
|
299
|
-
const measure = async (node, callback) => {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
397
|
+
export const measure = async (node, callback) => {
|
|
398
|
+
const out = await measureNode(node, node.parentNode instanceof HTMLElement ? node.parentNode : null);
|
|
399
|
+
if (out) {
|
|
400
|
+
callback?.(out.x, out.y, out.width, out.height, out.pageX, out.pageY);
|
|
401
|
+
}
|
|
402
|
+
return out;
|
|
303
403
|
};
|
|
304
|
-
function createMeasure(node) {
|
|
305
|
-
|
|
404
|
+
export function createMeasure(node) {
|
|
405
|
+
return (callback) => measure(node, callback);
|
|
306
406
|
}
|
|
307
|
-
const measureInWindow = async (node, callback) => {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
407
|
+
export const measureInWindow = async (node, callback) => {
|
|
408
|
+
const out = await measureNode(node, null);
|
|
409
|
+
if (out) {
|
|
410
|
+
callback?.(out.pageX, out.pageY, out.width, out.height);
|
|
411
|
+
}
|
|
412
|
+
return out;
|
|
311
413
|
};
|
|
312
|
-
const createMeasureInWindow = node => {
|
|
313
|
-
|
|
414
|
+
export const createMeasureInWindow = (node) => {
|
|
415
|
+
return (callback) => measureInWindow(node, callback);
|
|
314
416
|
};
|
|
315
|
-
const measureLayout = async (node, relativeNode, callback) => {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
417
|
+
export const measureLayout = async (node, relativeNode, callback) => {
|
|
418
|
+
const out = await measureNode(node, relativeNode);
|
|
419
|
+
if (out) {
|
|
420
|
+
callback?.(out.x, out.y, out.width, out.height, out.pageX, out.pageY);
|
|
421
|
+
}
|
|
422
|
+
return out;
|
|
319
423
|
};
|
|
320
|
-
function createMeasureLayout(node) {
|
|
321
|
-
|
|
424
|
+
export function createMeasureLayout(node) {
|
|
425
|
+
return (relativeTo, callback) => measureLayout(node, relativeTo, callback);
|
|
322
426
|
}
|
|
323
|
-
|
|
324
|
-
//# sourceMappingURL=index.js.map
|
|
427
|
+
//# sourceMappingURL=index.js.map
|