@tamagui/use-element-layout 2.0.0-rc.3 → 2.0.0-rc.30

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.
@@ -31,19 +31,24 @@ __export(index_exports, {
31
31
  measureInWindow: () => measureInWindow,
32
32
  measureLayout: () => measureLayout,
33
33
  measureNode: () => measureNode,
34
+ registerLayoutNode: () => registerLayoutNode,
34
35
  setOnLayoutStrategy: () => setOnLayoutStrategy,
35
36
  useElementLayout: () => useElementLayout
36
37
  });
37
38
  module.exports = __toCommonJS(index_exports);
38
39
  var import_constants = require("@tamagui/constants"),
39
- import_is_equal_shallow = require("@tamagui/is-equal-shallow"),
40
40
  import_react = require("react"),
41
41
  import_jsx_runtime = require("react/jsx-runtime");
42
42
  const LayoutHandlers = /* @__PURE__ */new WeakMap(),
43
43
  LayoutDisableKey = /* @__PURE__ */new WeakMap(),
44
44
  Nodes = /* @__PURE__ */new Set(),
45
45
  IntersectionState = /* @__PURE__ */new WeakMap(),
46
- DisableLayoutContextValues = {},
46
+ usePretransformDimensions = () => globalThis.__TAMAGUI_ONLAYOUT_PRETRANSFORM === !0 || process.env.TAMAGUI_ONLAYOUT_PRETRANSFORM === "1";
47
+ let _debugLayout;
48
+ function isDebugLayout() {
49
+ return _debugLayout === void 0 && (_debugLayout = typeof window < "u" && new URLSearchParams(window.location.search).has("__tamaDebugLayout")), _debugLayout;
50
+ }
51
+ const DisableLayoutContextValues = {},
47
52
  DisableLayoutContextKey = (0, import_react.createContext)(""),
48
53
  ENABLE = typeof IntersectionObserver < "u",
49
54
  LayoutMeasurementController = ({
@@ -63,8 +68,7 @@ let globalIntersectionObserver = null,
63
68
  function setOnLayoutStrategy(state) {
64
69
  strategy = state;
65
70
  }
66
- const NodeRectCache = /* @__PURE__ */new WeakMap(),
67
- LastChangeTime = /* @__PURE__ */new WeakMap();
71
+ const NodeRectCache = /* @__PURE__ */new WeakMap();
68
72
  let avoidUpdates = !0;
69
73
  const queuedUpdates = /* @__PURE__ */new Map();
70
74
  function enable() {
@@ -72,16 +76,33 @@ function enable() {
72
76
  }
73
77
  function startGlobalObservers() {
74
78
  !ENABLE || globalIntersectionObserver || (globalIntersectionObserver = new IntersectionObserver(entries => {
75
- entries.forEach(entry => {
76
- const node = entry.target;
79
+ for (let i = 0; i < entries.length; i++) {
80
+ const entry = entries[i],
81
+ node = entry.target;
77
82
  IntersectionState.get(node) !== entry.isIntersecting && IntersectionState.set(node, entry.isIntersecting);
78
- });
83
+ }
79
84
  }, {
80
85
  threshold: 0
81
86
  }));
82
87
  }
88
+ function rectsEqual(a, b) {
89
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
90
+ }
83
91
  if (ENABLE) {
92
+ let ensureRectFetchObserver = function () {
93
+ return rectFetchObserver || (rectFetchObserver = new IntersectionObserver(entries => {
94
+ lastCallbackDelay = Math.round(performance.now() - rectFetchStartTime);
95
+ for (let i = 0; i < entries.length; i++) BoundingRects.set(entries[i].target, entries[i].boundingClientRect);
96
+ process.env.NODE_ENV === "development" && isDebugLayout() && lastCallbackDelay > 50 && console.warn("[onLayout-io-delay]", lastCallbackDelay + "ms", entries.length, "entries"), rectFetchResolve && (rectFetchResolve(!0), rectFetchResolve = null);
97
+ }, {
98
+ threshold: 0
99
+ }), rectFetchObserver);
100
+ };
84
101
  const BoundingRects = /* @__PURE__ */new WeakMap();
102
+ let rectFetchObserver = null,
103
+ rectFetchResolve = null,
104
+ rectFetchStartTime = 0,
105
+ lastCallbackDelay = 0;
85
106
  async function updateLayoutIfChanged(node) {
86
107
  const onLayout = LayoutHandlers.get(node);
87
108
  if (typeof onLayout != "function") return;
@@ -89,68 +110,90 @@ if (ENABLE) {
89
110
  if (!parentNode) return;
90
111
  let nodeRect, parentRect;
91
112
  if (strategy === "async") {
92
- const [nr, pr] = await Promise.all([BoundingRects.get(node), BoundingRects.get(parentNode)]);
93
- if (!nr || !pr) return;
94
- nodeRect = nr, parentRect = pr;
113
+ if (nodeRect = BoundingRects.get(node), parentRect = BoundingRects.get(parentNode), !nodeRect || !parentRect) return;
95
114
  } else nodeRect = node.getBoundingClientRect(), parentRect = parentNode.getBoundingClientRect();
96
- if (!nodeRect || !parentRect) return;
97
115
  const cachedRect = NodeRectCache.get(node),
98
- cachedParentRect = NodeRectCache.get(parentNode);
99
- if (!cachedRect || !cachedParentRect ||
100
- // has changed one rect
101
- // @ts-expect-error DOMRectReadOnly can go into object
102
- !(0, import_is_equal_shallow.isEqualShallow)(cachedRect, nodeRect) ||
103
- // @ts-expect-error DOMRectReadOnly can go into object
104
- !(0, import_is_equal_shallow.isEqualShallow)(cachedParentRect, parentRect)) {
116
+ cachedParentRect = NodeRectCache.get(parentNode),
117
+ nodeChanged = !cachedRect || !rectsEqual(cachedRect, nodeRect),
118
+ parentChanged = !cachedParentRect || !rectsEqual(cachedParentRect, parentRect);
119
+ if (nodeChanged || parentChanged) {
105
120
  NodeRectCache.set(node, nodeRect), NodeRectCache.set(parentNode, parentRect);
106
- const event = getElementLayoutEvent(nodeRect, parentRect);
107
- avoidUpdates ? queuedUpdates.set(node, () => onLayout(event)) : onLayout(event);
121
+ const event = getElementLayoutEvent(nodeRect, parentRect, node);
122
+ process.env.NODE_ENV === "development" && isDebugLayout() && console.log("[useElementLayout] change", {
123
+ tag: node.tagName,
124
+ id: node.id || void 0,
125
+ className: (node.className || "").slice(0, 60) || void 0,
126
+ layout: event.nativeEvent.layout,
127
+ first: !cachedRect
128
+ }), avoidUpdates ? queuedUpdates.set(node, () => onLayout(event)) : onLayout(event);
108
129
  }
109
130
  }
110
- const userSkipVal = process.env.TAMAGUI_LAYOUT_FRAME_SKIP,
111
- RUN_EVERY_X_FRAMES = userSkipVal ? +userSkipVal : 14;
131
+ const rAF = typeof requestAnimationFrame < "u" ? requestAnimationFrame : void 0,
132
+ userSkipVal = process.env.TAMAGUI_LAYOUT_FRAME_SKIP,
133
+ BASE_SKIP_FRAMES = userSkipVal ? +userSkipVal : 10,
134
+ MAX_SKIP_FRAMES = 20;
135
+ let skipFrames = BASE_SKIP_FRAMES,
136
+ frameCount = 0;
112
137
  async function layoutOnAnimationFrame() {
113
- if (strategy !== "off") {
114
- const visibleNodes = [];
115
- (await new Promise(res => {
116
- const io = new IntersectionObserver(entries => {
117
- io.disconnect();
118
- for (const entry of entries) BoundingRects.set(entry.target, entry.boundingClientRect);
119
- res(!0);
120
- }, {
121
- threshold: 0
122
- });
123
- let didObserve = !1;
124
- for (const node of Nodes) {
125
- if (!(node.parentElement instanceof HTMLElement)) continue;
126
- const disableKey = LayoutDisableKey.get(node);
127
- disableKey && DisableLayoutContextValues[disableKey] === !0 || IntersectionState.get(node) !== !1 && (didObserve = !0, io.observe(node), io.observe(node.parentElement), visibleNodes.push(node));
138
+ if (frameCount++ % skipFrames !== 0) {
139
+ rAF ? rAF(layoutOnAnimationFrame) : setTimeout(layoutOnAnimationFrame, 16);
140
+ return;
141
+ }
142
+ if (frameCount >= Number.MAX_SAFE_INTEGER && (frameCount = 0), strategy !== "off") {
143
+ const visibleNodes = [],
144
+ parentsToObserve = /* @__PURE__ */new Set();
145
+ for (const node of Nodes) {
146
+ const parentElement = node.parentElement;
147
+ if (!(parentElement instanceof HTMLElement)) {
148
+ cleanupNode(node);
149
+ continue;
128
150
  }
129
- didObserve || res(!1);
130
- })) && visibleNodes.forEach(node => {
131
- updateLayoutIfChanged(node);
132
- });
151
+ const disableKey = LayoutDisableKey.get(node);
152
+ disableKey && DisableLayoutContextValues[disableKey] === !0 || IntersectionState.get(node) !== !1 && (visibleNodes.push(node), parentsToObserve.add(parentElement));
153
+ }
154
+ if (visibleNodes.length > 0) {
155
+ const io = ensureRectFetchObserver();
156
+ rectFetchStartTime = performance.now();
157
+ for (let i = 0; i < visibleNodes.length; i++) io.observe(visibleNodes[i]);
158
+ for (const parent of parentsToObserve) io.observe(parent);
159
+ await new Promise(res => {
160
+ rectFetchResolve = res;
161
+ });
162
+ for (let i = 0; i < visibleNodes.length; i++) io.unobserve(visibleNodes[i]);
163
+ for (const parent of parentsToObserve) io.unobserve(parent);
164
+ lastCallbackDelay > 50 ? skipFrames = Math.min(skipFrames + 2, MAX_SKIP_FRAMES) : lastCallbackDelay < 20 && (skipFrames = Math.max(skipFrames - 1, BASE_SKIP_FRAMES));
165
+ for (let i = 0; i < visibleNodes.length; i++) updateLayoutIfChanged(visibleNodes[i]);
166
+ }
133
167
  }
134
- setTimeout(layoutOnAnimationFrame, 16.6667 * RUN_EVERY_X_FRAMES);
168
+ rAF ? rAF(layoutOnAnimationFrame) : setTimeout(layoutOnAnimationFrame, 16);
135
169
  }
136
170
  layoutOnAnimationFrame();
137
171
  }
138
- const getElementLayoutEvent = (nodeRect, parentRect) => ({
172
+ const getElementLayoutEvent = (nodeRect, parentRect, node) => ({
139
173
  nativeEvent: {
140
- layout: getRelativeDimensions(nodeRect, parentRect),
174
+ layout: getRelativeDimensions(nodeRect, parentRect, node),
141
175
  target: nodeRect
142
176
  },
143
177
  timeStamp: Date.now()
144
178
  }),
145
- getRelativeDimensions = (a, b) => {
179
+ getPreTransformDimensions = node => ({
180
+ width: node.offsetWidth,
181
+ height: node.offsetHeight
182
+ }),
183
+ getRelativeDimensions = (a, b, aNode) => {
146
184
  const {
147
- height,
148
185
  left,
149
- top,
150
- width
186
+ top
151
187
  } = a,
152
188
  x = left - b.left,
153
- y = top - b.top;
189
+ y = top - b.top,
190
+ {
191
+ width,
192
+ height
193
+ } = usePretransformDimensions() && aNode ? getPreTransformDimensions(aNode) : {
194
+ width: a.width,
195
+ height: a.height
196
+ };
154
197
  return {
155
198
  x,
156
199
  y,
@@ -160,17 +203,44 @@ const getElementLayoutEvent = (nodeRect, parentRect) => ({
160
203
  pageY: a.top
161
204
  };
162
205
  };
206
+ function registerLayoutNode(node, onChange, disableKey) {
207
+ return Nodes.add(node), LayoutHandlers.set(node, onChange), disableKey && LayoutDisableKey.set(node, disableKey), startGlobalObservers(), globalIntersectionObserver && (globalIntersectionObserver.observe(node), IntersectionState.set(node, !0)), () => cleanupNode(node);
208
+ }
209
+ function cleanupNode(node) {
210
+ Nodes.delete(node), LayoutHandlers.delete(node), LayoutDisableKey.delete(node), NodeRectCache.delete(node), IntersectionState.delete(node), globalIntersectionObserver && globalIntersectionObserver.unobserve(node);
211
+ }
212
+ const PrevHostNode = /* @__PURE__ */new WeakMap();
163
213
  function useElementLayout(ref, onLayout) {
164
214
  const disableKey = (0, import_react.useContext)(DisableLayoutContextKey),
165
215
  node = ensureWebElement(ref.current?.host);
166
216
  node && onLayout && (LayoutHandlers.set(node, onLayout), LayoutDisableKey.set(node, disableKey)), (0, import_constants.useIsomorphicLayoutEffect)(() => {
217
+ if (!onLayout) return;
218
+ const nextNode = ensureWebElement(ref.current?.host),
219
+ prevNode = PrevHostNode.get(ref);
220
+ if (nextNode === prevNode || (prevNode && cleanupNode(prevNode), PrevHostNode.set(ref, nextNode), !nextNode)) return;
221
+ Nodes.add(nextNode), startGlobalObservers(), globalIntersectionObserver && (globalIntersectionObserver.observe(nextNode), IntersectionState.set(nextNode, !0));
222
+ const handler = LayoutHandlers.get(nextNode);
223
+ if (typeof handler != "function") return;
224
+ const parentNode = nextNode.parentElement;
225
+ if (!parentNode) return;
226
+ const nodeRect = nextNode.getBoundingClientRect(),
227
+ parentRect = parentNode.getBoundingClientRect();
228
+ NodeRectCache.set(nextNode, nodeRect), NodeRectCache.set(parentNode, parentRect), handler(getElementLayoutEvent(nodeRect, parentRect, nextNode));
229
+ }), (0, import_constants.useIsomorphicLayoutEffect)(() => {
167
230
  if (!onLayout) return;
168
231
  const node2 = ref.current?.host;
169
232
  if (!node2) return;
170
- Nodes.add(node2), startGlobalObservers(), globalIntersectionObserver && (globalIntersectionObserver.observe(node2), IntersectionState.set(node2, !0));
233
+ Nodes.add(node2), startGlobalObservers(), globalIntersectionObserver && (globalIntersectionObserver.observe(node2), IntersectionState.set(node2, !0)), process.env.NODE_ENV === "development" && isDebugLayout() && console.log("[useElementLayout] register", {
234
+ tag: node2.tagName,
235
+ id: node2.id || void 0,
236
+ className: (node2.className || "").slice(0, 60) || void 0,
237
+ totalNodes: Nodes.size
238
+ });
171
239
  const parentNode = node2.parentNode;
172
- return parentNode && onLayout(getElementLayoutEvent(node2.getBoundingClientRect(), parentNode.getBoundingClientRect())), () => {
173
- Nodes.delete(node2), LayoutHandlers.delete(node2), NodeRectCache.delete(node2), LastChangeTime.delete(node2), IntersectionState.delete(node2), globalIntersectionObserver && globalIntersectionObserver.unobserve(node2);
240
+ return parentNode && onLayout(getElementLayoutEvent(node2.getBoundingClientRect(), parentNode.getBoundingClientRect(), node2)), () => {
241
+ cleanupNode(node2);
242
+ const swappedNode = PrevHostNode.get(ref);
243
+ swappedNode && swappedNode !== node2 && cleanupNode(swappedNode), PrevHostNode.delete(ref);
174
244
  };
175
245
  }, [ref, !!onLayout]);
176
246
  }
@@ -188,7 +258,7 @@ const getBoundingClientRectAsync = node => new Promise(res => {
188
258
  const relativeNode = relativeTo || node?.parentElement;
189
259
  if (relativeNode instanceof HTMLElement) {
190
260
  const [nodeDim, relativeNodeDim] = await Promise.all([getBoundingClientRectAsync(node), getBoundingClientRectAsync(relativeNode)]);
191
- if (relativeNodeDim && nodeDim) return getRelativeDimensions(nodeDim, relativeNodeDim);
261
+ if (relativeNodeDim && nodeDim) return getRelativeDimensions(nodeDim, relativeNodeDim, node);
192
262
  }
193
263
  return null;
194
264
  },