@tamagui/core 1.126.18 → 1.127.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.
Files changed (33) hide show
  1. package/dist/cjs/helpers/getBoundingClientRect.cjs +16 -3
  2. package/dist/cjs/helpers/getBoundingClientRect.js +17 -2
  3. package/dist/cjs/helpers/getBoundingClientRect.js.map +1 -1
  4. package/dist/cjs/helpers/getBoundingClientRect.native.js +20 -2
  5. package/dist/cjs/helpers/getBoundingClientRect.native.js.map +2 -2
  6. package/dist/cjs/hooks/useElementLayout.cjs +90 -22
  7. package/dist/cjs/hooks/useElementLayout.js +72 -16
  8. package/dist/cjs/hooks/useElementLayout.js.map +1 -1
  9. package/dist/cjs/hooks/useElementLayout.native.js +82 -20
  10. package/dist/cjs/hooks/useElementLayout.native.js.map +2 -2
  11. package/dist/esm/helpers/getBoundingClientRect.js +17 -2
  12. package/dist/esm/helpers/getBoundingClientRect.js.map +1 -1
  13. package/dist/esm/helpers/getBoundingClientRect.mjs +15 -3
  14. package/dist/esm/helpers/getBoundingClientRect.mjs.map +1 -1
  15. package/dist/esm/helpers/getBoundingClientRect.native.js +22 -4
  16. package/dist/esm/helpers/getBoundingClientRect.native.js.map +1 -1
  17. package/dist/esm/hooks/useElementLayout.js +72 -16
  18. package/dist/esm/hooks/useElementLayout.js.map +1 -1
  19. package/dist/esm/hooks/useElementLayout.mjs +89 -23
  20. package/dist/esm/hooks/useElementLayout.mjs.map +1 -1
  21. package/dist/esm/hooks/useElementLayout.native.js +101 -36
  22. package/dist/esm/hooks/useElementLayout.native.js.map +1 -1
  23. package/dist/native.js +81 -22
  24. package/dist/native.js.map +2 -2
  25. package/dist/test.native.js +81 -22
  26. package/dist/test.native.js.map +2 -2
  27. package/package.json +7 -7
  28. package/src/helpers/getBoundingClientRect.tsx +26 -0
  29. package/src/hooks/useElementLayout.tsx +107 -8
  30. package/types/helpers/getBoundingClientRect.d.ts +1 -0
  31. package/types/helpers/getBoundingClientRect.d.ts.map +1 -1
  32. package/types/hooks/useElementLayout.d.ts +4 -2
  33. package/types/hooks/useElementLayout.d.ts.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamagui/core",
3
- "version": "1.126.18",
3
+ "version": "1.127.1",
4
4
  "source": "src/index.tsx",
5
5
  "main": "dist/cjs",
6
6
  "module": "dist/esm",
@@ -35,14 +35,14 @@
35
35
  "native-test.d.ts"
36
36
  ],
37
37
  "dependencies": {
38
- "@tamagui/react-native-media-driver": "1.126.18",
39
- "@tamagui/react-native-use-pressable": "1.126.18",
40
- "@tamagui/react-native-use-responder-events": "1.126.18",
41
- "@tamagui/use-event": "1.126.18",
42
- "@tamagui/web": "1.126.18"
38
+ "@tamagui/react-native-media-driver": "1.127.1",
39
+ "@tamagui/react-native-use-pressable": "1.127.1",
40
+ "@tamagui/react-native-use-responder-events": "1.127.1",
41
+ "@tamagui/use-event": "1.127.1",
42
+ "@tamagui/web": "1.127.1"
43
43
  },
44
44
  "devDependencies": {
45
- "@tamagui/build": "1.126.18",
45
+ "@tamagui/build": "1.127.1",
46
46
  "@testing-library/react": "^16.1.0",
47
47
  "csstype": "^3.0.10",
48
48
  "typescript": "^5.8.2",
@@ -2,3 +2,29 @@ export const getBoundingClientRect = (node: HTMLElement | null): undefined | DOM
2
2
  if (!node || node.nodeType !== 1) return
3
3
  return node.getBoundingClientRect?.()
4
4
  }
5
+
6
+ export const getBoundingClientRectAsync = (
7
+ element: HTMLElement
8
+ ): Promise<DOMRectReadOnly | undefined> => {
9
+ return new Promise((resolve) => {
10
+ let didFallback = false
11
+ function fallbackToSync() {
12
+ didFallback = true
13
+ resolve(getBoundingClientRect(element))
14
+ }
15
+ const tm = setTimeout(fallbackToSync, 32)
16
+ const observer = new IntersectionObserver(
17
+ (entries, ob) => {
18
+ clearTimeout(tm)
19
+ ob.disconnect()
20
+ if (!didFallback) {
21
+ resolve(entries[0]?.boundingClientRect)
22
+ }
23
+ },
24
+ {
25
+ threshold: 0.0001,
26
+ }
27
+ )
28
+ observer.observe(element)
29
+ })
30
+ }
@@ -9,11 +9,12 @@ import type { RefObject } from 'react'
9
9
  const LayoutHandlers = new WeakMap<HTMLElement, Function>()
10
10
  const Nodes = new Set<HTMLElement>()
11
11
 
12
- type LayoutMeasurementStatus = 'inactive' | 'active'
12
+ type LayoutMeasurementStrategy = 'off' | 'sync' | 'async'
13
13
 
14
- let status: LayoutMeasurementStatus = 'active'
15
- export function setOnLayoutStrategy(state: LayoutMeasurementStatus) {
16
- status = state
14
+ let strategy: LayoutMeasurementStrategy = 'async'
15
+
16
+ export function setOnLayoutStrategy(state: LayoutMeasurementStrategy) {
17
+ strategy = state
17
18
  }
18
19
 
19
20
  export type LayoutValue = {
@@ -35,8 +36,11 @@ export type LayoutEvent = {
35
36
 
36
37
  const NodeRectCache = new WeakMap<HTMLElement, DOMRect>()
37
38
  const ParentRectCache = new WeakMap<HTMLElement, DOMRect>()
39
+ const DebounceTimers = new WeakMap<HTMLElement, NodeJS.Timeout>()
40
+ const LastChangeTime = new WeakMap<HTMLElement, number>()
38
41
 
39
42
  const rAF = typeof window !== 'undefined' ? window.requestAnimationFrame : undefined
43
+ const DEBOUNCE_DELAY = 32 // 32ms debounce (2 frames at 60fps)
40
44
 
41
45
  if (isClient) {
42
46
  if (rAF) {
@@ -56,7 +60,7 @@ if (isClient) {
56
60
  }
57
61
  })
58
62
 
59
- function updateLayoutIfChanged(node: HTMLElement) {
63
+ async function updateLayoutIfChanged(node: HTMLElement) {
60
64
  const nodeRect = node.getBoundingClientRect()
61
65
  const parentNode = node.parentElement
62
66
  const parentRect = parentNode?.getBoundingClientRect()
@@ -77,10 +81,48 @@ if (isClient) {
77
81
  if (parentRect && parentNode) {
78
82
  ParentRectCache.set(parentNode, parentRect)
79
83
  }
80
- const event = getElementLayoutEvent(node)
84
+
81
85
  if (avoidUpdates) {
86
+ // Use sync version for queued updates to avoid promise complications
87
+ const event = getElementLayoutEvent(node)
82
88
  queuedUpdates.set(node, () => onLayout(event))
89
+ } else if (strategy === 'async') {
90
+ // For async strategy, debounce the layout update
91
+ const now = Date.now()
92
+ LastChangeTime.set(node, now)
93
+
94
+ // Clear existing debounce timer
95
+ const existingTimer = DebounceTimers.get(node)
96
+ if (existingTimer) {
97
+ clearTimeout(existingTimer)
98
+ }
99
+
100
+ // Set new debounce timer
101
+ const timer = setTimeout(async () => {
102
+ const lastChange = LastChangeTime.get(node) || 0
103
+ const timeSinceChange = Date.now() - lastChange
104
+
105
+ // Only fire if at least DEBOUNCE_DELAY has passed since last change
106
+ if (timeSinceChange >= DEBOUNCE_DELAY) {
107
+ const event = await getElementLayoutEventAsync(node)
108
+ onLayout(event)
109
+ DebounceTimers.delete(node)
110
+ } else {
111
+ // Reschedule if not enough time has passed
112
+ const remainingDelay = DEBOUNCE_DELAY - timeSinceChange
113
+ const newTimer = setTimeout(async () => {
114
+ const event = await getElementLayoutEventAsync(node)
115
+ onLayout(event)
116
+ DebounceTimers.delete(node)
117
+ }, remainingDelay)
118
+ DebounceTimers.set(node, newTimer)
119
+ }
120
+ }, DEBOUNCE_DELAY)
121
+
122
+ DebounceTimers.set(node, timer)
83
123
  } else {
124
+ // Sync strategy - use sync version
125
+ const event = getElementLayoutEvent(node)
84
126
  onLayout(event)
85
127
  }
86
128
  }
@@ -93,7 +135,7 @@ if (isClient) {
93
135
  const timeSinceLastFrame = now - lastFrameAt
94
136
  lastFrameAt = now
95
137
 
96
- if (status !== 'inactive') {
138
+ if (strategy !== 'off') {
97
139
  // avoid updates if we've been dropping frames (indicates sync work happening)
98
140
  const expectedFrameTime = 16.67 // ~60fps
99
141
  const hasRecentSyncWork =
@@ -114,6 +156,7 @@ if (isClient) {
114
156
  }
115
157
  }
116
158
 
159
+ // Sync versions
117
160
  export const getElementLayoutEvent = (target: HTMLElement): LayoutEvent => {
118
161
  let res: LayoutEvent | null = null
119
162
  measureLayout(target, null, (x, y, width, height, left, top) => {
@@ -131,7 +174,6 @@ export const getElementLayoutEvent = (target: HTMLElement): LayoutEvent => {
131
174
  return res
132
175
  }
133
176
 
134
- // matching old RN callback API (can we remove?)
135
177
  export const measureLayout = (
136
178
  node: HTMLElement,
137
179
  relativeTo: HTMLElement | null,
@@ -148,6 +190,55 @@ export const measureLayout = (
148
190
  if (relativeNode instanceof HTMLElement) {
149
191
  const nodeDim = node.getBoundingClientRect()
150
192
  const relativeNodeDim = relativeNode.getBoundingClientRect()
193
+
194
+ if (relativeNodeDim && nodeDim) {
195
+ const { x, y, width, height, left, top } = getRelativeDimensions(
196
+ nodeDim,
197
+ relativeNodeDim
198
+ )
199
+ callback(x, y, width, height, left, top)
200
+ }
201
+ }
202
+ }
203
+
204
+ export const getElementLayoutEventAsync = async (
205
+ target: HTMLElement
206
+ ): Promise<LayoutEvent> => {
207
+ let res: LayoutEvent | null = null
208
+ await measureLayoutAsync(target, null, (x, y, width, height, left, top) => {
209
+ res = {
210
+ nativeEvent: {
211
+ layout: { x, y, width, height, left, top },
212
+ target,
213
+ },
214
+ timeStamp: Date.now(),
215
+ }
216
+ })
217
+ if (!res) {
218
+ throw new Error(`‼️`) // impossible
219
+ }
220
+ return res
221
+ }
222
+
223
+ export const measureLayoutAsync = async (
224
+ node: HTMLElement,
225
+ relativeTo: HTMLElement | null,
226
+ callback: (
227
+ x: number,
228
+ y: number,
229
+ width: number,
230
+ height: number,
231
+ left: number,
232
+ top: number
233
+ ) => void
234
+ ) => {
235
+ const relativeNode = relativeTo || node?.parentElement
236
+ if (relativeNode instanceof HTMLElement) {
237
+ const [nodeDim, relativeNodeDim] = await Promise.all([
238
+ node.getBoundingClientRect(),
239
+ relativeNode.getBoundingClientRect(),
240
+ ])
241
+
151
242
  if (relativeNodeDim && nodeDim) {
152
243
  const { x, y, width, height, left, top } = getRelativeDimensions(
153
244
  nodeDim,
@@ -188,6 +279,14 @@ export function useElementLayout(
188
279
  Nodes.delete(node)
189
280
  LayoutHandlers.delete(node)
190
281
  NodeRectCache.delete(node)
282
+
283
+ // Clean up debounce timer and tracking
284
+ const timer = DebounceTimers.get(node)
285
+ if (timer) {
286
+ clearTimeout(timer)
287
+ DebounceTimers.delete(node)
288
+ }
289
+ LastChangeTime.delete(node)
191
290
  }
192
291
  }, [ref, !!onLayout])
193
292
  }
@@ -1,2 +1,3 @@
1
1
  export declare const getBoundingClientRect: (node: HTMLElement | null) => undefined | DOMRect;
2
+ export declare const getBoundingClientRectAsync: (element: HTMLElement) => Promise<DOMRectReadOnly | undefined>;
2
3
  //# sourceMappingURL=getBoundingClientRect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"getBoundingClientRect.d.ts","sourceRoot":"","sources":["../../src/helpers/getBoundingClientRect.tsx"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,GAAI,MAAM,WAAW,GAAG,IAAI,KAAG,SAAS,GAAG,OAG5E,CAAA"}
1
+ {"version":3,"file":"getBoundingClientRect.d.ts","sourceRoot":"","sources":["../../src/helpers/getBoundingClientRect.tsx"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,GAAI,MAAM,WAAW,GAAG,IAAI,KAAG,SAAS,GAAG,OAG5E,CAAA;AAED,eAAO,MAAM,0BAA0B,GACrC,SAAS,WAAW,KACnB,OAAO,CAAC,eAAe,GAAG,SAAS,CAsBrC,CAAA"}
@@ -1,7 +1,7 @@
1
1
  import { type TamaguiComponentStateRef } from '@tamagui/web';
2
2
  import type { RefObject } from 'react';
3
- type LayoutMeasurementStatus = 'inactive' | 'active';
4
- export declare function setOnLayoutStrategy(state: LayoutMeasurementStatus): void;
3
+ type LayoutMeasurementStrategy = 'off' | 'sync' | 'async';
4
+ export declare function setOnLayoutStrategy(state: LayoutMeasurementStrategy): void;
5
5
  export type LayoutValue = {
6
6
  x: number;
7
7
  y: number;
@@ -19,6 +19,8 @@ export type LayoutEvent = {
19
19
  };
20
20
  export declare const getElementLayoutEvent: (target: HTMLElement) => LayoutEvent;
21
21
  export declare const measureLayout: (node: HTMLElement, relativeTo: HTMLElement | null, callback: (x: number, y: number, width: number, height: number, left: number, top: number) => void) => void;
22
+ export declare const getElementLayoutEventAsync: (target: HTMLElement) => Promise<LayoutEvent>;
23
+ export declare const measureLayoutAsync: (node: HTMLElement, relativeTo: HTMLElement | null, callback: (x: number, y: number, width: number, height: number, left: number, top: number) => void) => Promise<void>;
22
24
  export declare function useElementLayout(ref: RefObject<TamaguiComponentStateRef>, onLayout?: ((e: LayoutEvent) => void) | null): void;
23
25
  export {};
24
26
  //# sourceMappingURL=useElementLayout.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useElementLayout.d.ts","sourceRoot":"","sources":["../../src/hooks/useElementLayout.tsx"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,wBAAwB,EAE9B,MAAM,cAAc,CAAA;AACrB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,KAAK,uBAAuB,GAAG,UAAU,GAAG,QAAQ,CAAA;AAGpD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,uBAAuB,QAEjE;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,WAAW,EAAE;QACX,MAAM,EAAE,WAAW,CAAA;QACnB,MAAM,EAAE,GAAG,CAAA;KACZ,CAAA;IACD,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAmFD,eAAO,MAAM,qBAAqB,GAAI,QAAQ,WAAW,KAAG,WAe3D,CAAA;AAGD,eAAO,MAAM,aAAa,GACxB,MAAM,WAAW,EACjB,YAAY,WAAW,GAAG,IAAI,EAC9B,UAAU,CACR,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,KACR,IAAI,SAcV,CAAA;AASD,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,SAAS,CAAC,wBAAwB,CAAC,EACxC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,KAAK,IAAI,CAAC,GAAG,IAAI,QAuB7C"}
1
+ {"version":3,"file":"useElementLayout.d.ts","sourceRoot":"","sources":["../../src/hooks/useElementLayout.tsx"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,wBAAwB,EAE9B,MAAM,cAAc,CAAA;AACrB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,KAAK,yBAAyB,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAA;AAIzD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,yBAAyB,QAEnE;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,WAAW,EAAE;QACX,MAAM,EAAE,WAAW,CAAA;QACnB,MAAM,EAAE,GAAG,CAAA;KACZ,CAAA;IACD,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AA6HD,eAAO,MAAM,qBAAqB,GAAI,QAAQ,WAAW,KAAG,WAe3D,CAAA;AAED,eAAO,MAAM,aAAa,GACxB,MAAM,WAAW,EACjB,YAAY,WAAW,GAAG,IAAI,EAC9B,UAAU,CACR,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,KACR,IAAI,SAeV,CAAA;AAED,eAAO,MAAM,0BAA0B,GACrC,QAAQ,WAAW,KAClB,OAAO,CAAC,WAAW,CAerB,CAAA;AAED,eAAO,MAAM,kBAAkB,GAC7B,MAAM,WAAW,EACjB,YAAY,WAAW,GAAG,IAAI,EAC9B,UAAU,CACR,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,KACR,IAAI,kBAiBV,CAAA;AASD,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,SAAS,CAAC,wBAAwB,CAAC,EACxC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,KAAK,IAAI,CAAC,GAAG,IAAI,QA+B7C"}