@hanzogui/use-element-layout 8.3.0 → 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/esm/index.js CHANGED
@@ -1,324 +1,427 @@
1
- import { useIsomorphicLayoutEffect } from "@hanzogui/constants";
2
- import { createContext, useContext, useId } from "react";
3
- import { jsx } from "react/jsx-runtime";
4
- const LayoutHandlers = /* @__PURE__ */new WeakMap();
5
- const LayoutDisableKey = /* @__PURE__ */new WeakMap();
6
- const Nodes = /* @__PURE__ */new Set();
7
- const IntersectionState = /* @__PURE__ */new WeakMap();
8
- const usePretransformDimensions = () => globalThis.__GUI_ONLAYOUT_PRETRANSFORM === true || process.env.GUI_ONLAYOUT_PRETRANSFORM === "1";
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
- if (_debugLayout === void 0) _debugLayout = typeof window !== "undefined" && new URLSearchParams(window.location.search).has("__tamaDebugLayout");
12
- return _debugLayout;
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 !== "undefined";
17
- const LayoutMeasurementController = ({
18
- disable,
19
- children
20
- }) => {
21
- const id = useId();
22
- useIsomorphicLayoutEffect(() => {
23
- DisableLayoutContextValues[id] = disable;
24
- }, [disable, id]);
25
- return /* @__PURE__ */jsx(DisableLayoutContextKey.Provider, {
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 = "async";
32
- function setOnLayoutStrategy(state) {
33
- strategy = state;
38
+ let strategy = 'async';
39
+ export function setOnLayoutStrategy(state) {
40
+ strategy = state;
34
41
  }
35
- const NodeRectCache = /* @__PURE__ */new WeakMap();
42
+ const NodeRectCache = new WeakMap();
43
+ // prevent thrashing during first hydration (somewhat, streaming gets trickier)
36
44
  let avoidUpdates = true;
37
- const queuedUpdates = /* @__PURE__ */new Map();
38
- function enable() {
39
- if (avoidUpdates) {
40
- avoidUpdates = false;
41
- if (queuedUpdates) {
42
- queuedUpdates.forEach(cb => cb());
43
- queuedUpdates.clear();
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
- if (!ENABLE || globalIntersectionObserver) return;
49
- globalIntersectionObserver = new IntersectionObserver(entries => {
50
- for (let i = 0; i < entries.length; i++) {
51
- const entry = entries[i];
52
- const node = entry.target;
53
- if (IntersectionState.get(node) !== entry.isIntersecting) IntersectionState.set(node, entry.isIntersecting);
54
- }
55
- }, {
56
- threshold: 0
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
- return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
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
- let ensureRectFetchObserver = function () {
64
- if (rectFetchObserver) return rectFetchObserver;
65
- rectFetchObserver = new IntersectionObserver(entries => {
66
- lastCallbackDelay = Math.round(performance.now() - rectFetchStartTime);
67
- for (let i = 0; i < entries.length; i++) BoundingRects.set(entries[i].target, entries[i].boundingClientRect);
68
- if (process.env.NODE_ENV === "development" && isDebugLayout() && lastCallbackDelay > 50) console.warn("[onLayout-io-delay]", lastCallbackDelay + "ms", entries.length, "entries");
69
- if (rectFetchResolve) {
70
- rectFetchResolve(true);
71
- rectFetchResolve = null;
72
- }
73
- }, {
74
- threshold: 0
75
- });
76
- return rectFetchObserver;
77
- };
78
- const BoundingRects = /* @__PURE__ */new WeakMap();
79
- let rectFetchObserver = null;
80
- let rectFetchResolve = null;
81
- let rectFetchStartTime = 0;
82
- let lastCallbackDelay = 0;
83
- async function updateLayoutIfChanged(node) {
84
- if (typeof LayoutHandlers.get(node) !== "function") return;
85
- const parentNode = node.parentElement;
86
- if (!parentNode) return;
87
- let nodeRect;
88
- let parentRect;
89
- if (strategy === "async") {
90
- nodeRect = BoundingRects.get(node);
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
- emitLayoutIfChanged(node, parentNode, nodeRect, parentRect);
98
- }
99
- const rAF = typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : void 0;
100
- const userSkipVal = process.env.GUI_LAYOUT_FRAME_SKIP;
101
- const BASE_SKIP_FRAMES = userSkipVal ? +userSkipVal : 10;
102
- const MAX_SKIP_FRAMES = 20;
103
- let skipFrames = BASE_SKIP_FRAMES;
104
- let frameCount = 0;
105
- async function layoutOnAnimationFrame() {
106
- if (frameCount++ % skipFrames !== 0) {
107
- rAF ? rAF(layoutOnAnimationFrame) : setTimeout(layoutOnAnimationFrame, 16);
108
- return;
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
- if (frameCount >= Number.MAX_SAFE_INTEGER) frameCount = 0;
111
- if (strategy !== "off") {
112
- const visibleNodes = [];
113
- const parentsToObserve = /* @__PURE__ */new Set();
114
- for (const node of Nodes) {
115
- const parentElement = node.parentElement;
116
- if (!(parentElement instanceof HTMLElement)) {
117
- cleanupNode(node);
118
- continue;
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
- const disableKey = LayoutDisableKey.get(node);
121
- if (disableKey && DisableLayoutContextValues[disableKey] === true) continue;
122
- if (IntersectionState.get(node) === false) continue;
123
- visibleNodes.push(node);
124
- parentsToObserve.add(parentElement);
125
- }
126
- if (visibleNodes.length > 0) {
127
- const io = ensureRectFetchObserver();
128
- rectFetchStartTime = performance.now();
129
- for (let i = 0; i < visibleNodes.length; i++) io.observe(visibleNodes[i]);
130
- for (const parent of parentsToObserve) io.observe(parent);
131
- await new Promise(res => {
132
- rectFetchResolve = res;
133
- });
134
- for (let i = 0; i < visibleNodes.length; i++) io.unobserve(visibleNodes[i]);
135
- for (const parent of parentsToObserve) io.unobserve(parent);
136
- if (lastCallbackDelay > 50) skipFrames = Math.min(skipFrames + 2, MAX_SKIP_FRAMES);else if (lastCallbackDelay < 20) skipFrames = Math.max(skipFrames - 1, BASE_SKIP_FRAMES);
137
- for (let i = 0; i < visibleNodes.length; i++) updateLayoutIfChanged(visibleNodes[i]);
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
- rAF ? rAF(layoutOnAnimationFrame) : setTimeout(layoutOnAnimationFrame, 16);
141
- }
142
- layoutOnAnimationFrame();
202
+ layoutOnAnimationFrame();
143
203
  }
144
- const getElementLayoutEvent = (nodeRect, parentRect, node) => {
145
- return {
146
- nativeEvent: {
147
- layout: getRelativeDimensions(nodeRect, parentRect, node),
148
- target: nodeRect
149
- },
150
- timeStamp: Date.now()
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
- const getPreTransformDimensions = node => {
154
- return {
155
- width: node.offsetWidth,
156
- height: node.offsetHeight
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
- const {
161
- left,
162
- top
163
- } = a;
164
- const x = left - b.left;
165
- const y = top - b.top;
166
- const {
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
- const onLayout = LayoutHandlers.get(node);
184
- if (typeof onLayout !== "function") return;
185
- const cachedRect = NodeRectCache.get(node);
186
- const cachedParentRect = NodeRectCache.get(parentNode);
187
- const nodeChanged = !cachedRect || !rectsEqual(cachedRect, nodeRect);
188
- const parentChanged = !cachedParentRect || !rectsEqual(cachedParentRect, parentRect);
189
- if (!nodeChanged && !parentChanged) return;
190
- NodeRectCache.set(node, nodeRect);
191
- NodeRectCache.set(parentNode, parentRect);
192
- const event = getElementLayoutEvent(nodeRect, parentRect, node);
193
- if (process.env.NODE_ENV === "development" && isDebugLayout()) console.log("[useElementLayout] change", {
194
- tag: node.tagName,
195
- id: node.id || void 0,
196
- className: (node.className || "").slice(0, 60) || void 0,
197
- layout: event.nativeEvent.layout,
198
- first: !cachedRect
199
- });
200
- if (avoidUpdates) queuedUpdates.set(node, () => onLayout(event));else onLayout(event);
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
- Nodes.add(node);
204
- if (disableKey) LayoutDisableKey.set(node, disableKey);else LayoutDisableKey.delete(node);
205
- startGlobalObservers();
206
- if (globalIntersectionObserver) {
207
- globalIntersectionObserver.observe(node);
208
- IntersectionState.set(node, true);
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
- function registerLayoutNode(node, onChange, disableKey) {
212
- LayoutHandlers.set(node, onChange);
213
- observeLayoutNode(node, disableKey);
214
- return () => cleanupNode(node);
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
- Nodes.delete(node);
218
- LayoutHandlers.delete(node);
219
- LayoutDisableKey.delete(node);
220
- NodeRectCache.delete(node);
221
- IntersectionState.delete(node);
222
- if (globalIntersectionObserver) globalIntersectionObserver.unobserve(node);
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 = /* @__PURE__ */new WeakMap();
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
- const onLayout = LayoutHandlers.get(node);
227
- if (typeof onLayout !== "function") return;
228
- const parentNode = node.parentElement;
229
- if (!parentNode) return;
230
- const nodeRect = node.getBoundingClientRect();
231
- const parentRect = parentNode.getBoundingClientRect();
232
- NodeRectCache.set(node, nodeRect);
233
- NodeRectCache.set(parentNode, parentRect);
234
- onLayout(getElementLayoutEvent(nodeRect, parentRect, node));
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
- const disableKey = useContext(DisableLayoutContextKey);
238
- const node = ensureWebElement(ref.current?.host);
239
- if (node && onLayout) {
240
- LayoutHandlers.set(node, onLayout);
241
- LayoutDisableKey.set(node, disableKey);
242
- }
243
- useIsomorphicLayoutEffect(() => {
244
- if (!onLayout) return;
245
- const nextNode = ensureWebElement(ref.current?.host);
246
- const prevNode = PrevHostNode.get(ref);
247
- if (nextNode === prevNode) return;
248
- if (prevNode) cleanupNode(prevNode);
249
- PrevHostNode.set(ref, nextNode);
250
- if (!nextNode) return;
251
- LayoutHandlers.set(nextNode, onLayout);
252
- observeLayoutNode(nextNode, disableKey);
253
- emitLayoutSync(nextNode);
254
- });
255
- useIsomorphicLayoutEffect(() => {
256
- if (!onLayout) return;
257
- const node2 = ref.current?.host;
258
- if (!node2) return;
259
- LayoutHandlers.set(node2, onLayout);
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
- return () => {
268
- cleanupNode(node2);
269
- const swappedNode = PrevHostNode.get(ref);
270
- if (swappedNode && swappedNode !== node2) cleanupNode(swappedNode);
271
- PrevHostNode.delete(ref);
272
- };
273
- }, [ref, !!onLayout]);
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
- if (typeof HTMLElement === "undefined") return;
277
- return x instanceof HTMLElement ? x : void 0;
366
+ if (typeof HTMLElement === 'undefined') {
367
+ return undefined;
368
+ }
369
+ return x instanceof HTMLElement ? x : undefined;
278
370
  }
279
- const getBoundingClientRectAsync = node => {
280
- return new Promise(res => {
281
- if (!node || node.nodeType !== 1) return res(false);
282
- const io = new IntersectionObserver(entries => {
283
- io.disconnect();
284
- return res(entries[0].boundingClientRect);
285
- }, {
286
- threshold: 0
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
- const relativeNode = relativeTo || node?.parentElement;
293
- if (relativeNode instanceof HTMLElement) {
294
- const [nodeDim, relativeNodeDim] = await Promise.all([getBoundingClientRectAsync(node), getBoundingClientRectAsync(relativeNode)]);
295
- if (relativeNodeDim && nodeDim) return getRelativeDimensions(nodeDim, relativeNodeDim, node);
296
- }
297
- return null;
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
- const out = await measureNode(node, node.parentNode instanceof HTMLElement ? node.parentNode : null);
301
- if (out) callback?.(out.x, out.y, out.width, out.height, out.pageX, out.pageY);
302
- return out;
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
- return callback => measure(node, callback);
404
+ export function createMeasure(node) {
405
+ return (callback) => measure(node, callback);
306
406
  }
307
- const measureInWindow = async (node, callback) => {
308
- const out = await measureNode(node, null);
309
- if (out) callback?.(out.pageX, out.pageY, out.width, out.height);
310
- return out;
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
- return callback => measureInWindow(node, callback);
414
+ export const createMeasureInWindow = (node) => {
415
+ return (callback) => measureInWindow(node, callback);
314
416
  };
315
- const measureLayout = async (node, relativeNode, callback) => {
316
- const out = await measureNode(node, relativeNode);
317
- if (out) callback?.(out.x, out.y, out.width, out.height, out.pageX, out.pageY);
318
- return out;
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
- return (relativeTo, callback) => measureLayout(node, relativeTo, callback);
424
+ export function createMeasureLayout(node) {
425
+ return (relativeTo, callback) => measureLayout(node, relativeTo, callback);
322
426
  }
323
- export { LayoutMeasurementController, createMeasure, createMeasureInWindow, createMeasureLayout, enable, getBoundingClientRectAsync, getElementLayoutEvent, measure, measureInWindow, measureLayout, measureNode, registerLayoutNode, setOnLayoutStrategy, useElementLayout };
324
- //# sourceMappingURL=index.js.map
427
+ //# sourceMappingURL=index.js.map