@tamagui/native 2.7.7 → 3.0.0-beta.643.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 (225) hide show
  1. package/dist/cjs/PressBoundary.cjs +53 -71
  2. package/dist/cjs/PressBoundary.native.cjs +86 -0
  3. package/dist/cjs/PressBoundary.native.js +63 -80
  4. package/dist/cjs/PressBoundary.native.js.map +1 -1
  5. package/dist/cjs/burntState.cjs +29 -32
  6. package/dist/cjs/burntState.native.cjs +45 -0
  7. package/dist/cjs/burntState.native.js +29 -31
  8. package/dist/cjs/burntState.native.js.map +1 -1
  9. package/dist/cjs/components.cjs +35 -55
  10. package/dist/cjs/components.native.cjs +56 -0
  11. package/dist/cjs/components.native.js +35 -54
  12. package/dist/cjs/components.native.js.map +1 -1
  13. package/dist/cjs/expoUIMenuAdapter.cjs +240 -254
  14. package/dist/cjs/expoUIMenuAdapter.native.cjs +315 -0
  15. package/dist/cjs/expoUIMenuAdapter.native.js +285 -303
  16. package/dist/cjs/expoUIMenuAdapter.native.js.map +1 -1
  17. package/dist/cjs/gesture-handler.cjs +21 -22
  18. package/dist/cjs/gesture-handler.native.cjs +38 -0
  19. package/dist/cjs/gesture-handler.native.js +21 -21
  20. package/dist/cjs/gesture-handler.native.js.map +1 -1
  21. package/dist/cjs/gestureState.cjs +202 -207
  22. package/dist/cjs/gestureState.native.cjs +262 -0
  23. package/dist/cjs/gestureState.native.js +215 -219
  24. package/dist/cjs/gestureState.native.js.map +1 -1
  25. package/dist/cjs/globalState.cjs +31 -34
  26. package/dist/cjs/globalState.native.cjs +44 -0
  27. package/dist/cjs/globalState.native.js +31 -33
  28. package/dist/cjs/globalState.native.js.map +1 -1
  29. package/dist/cjs/index.cjs +34 -34
  30. package/dist/cjs/index.native.cjs +59 -0
  31. package/dist/cjs/index.native.js +34 -33
  32. package/dist/cjs/index.native.js.map +1 -1
  33. package/dist/cjs/keyboardControllerState.cjs +28 -29
  34. package/dist/cjs/keyboardControllerState.native.cjs +50 -0
  35. package/dist/cjs/keyboardControllerState.native.js +28 -28
  36. package/dist/cjs/keyboardControllerState.native.js.map +1 -1
  37. package/dist/cjs/linearGradientState.cjs +28 -31
  38. package/dist/cjs/linearGradientState.native.cjs +44 -0
  39. package/dist/cjs/linearGradientState.native.js +28 -30
  40. package/dist/cjs/linearGradientState.native.js.map +1 -1
  41. package/dist/cjs/nativeMenuContext.cjs +15 -18
  42. package/dist/cjs/nativeMenuContext.native.cjs +28 -0
  43. package/dist/cjs/nativeMenuContext.native.js +15 -17
  44. package/dist/cjs/nativeMenuContext.native.js.map +1 -1
  45. package/dist/cjs/nativeMenuState.cjs +27 -28
  46. package/dist/cjs/nativeMenuState.native.cjs +47 -0
  47. package/dist/cjs/nativeMenuState.native.js +27 -27
  48. package/dist/cjs/nativeMenuState.native.js.map +1 -1
  49. package/dist/cjs/portalState.cjs +28 -31
  50. package/dist/cjs/portalState.native.cjs +44 -0
  51. package/dist/cjs/portalState.native.js +28 -30
  52. package/dist/cjs/portalState.native.js.map +1 -1
  53. package/dist/cjs/safeAreaState.cjs +77 -53
  54. package/dist/cjs/safeAreaState.native.cjs +101 -0
  55. package/dist/cjs/safeAreaState.native.js +79 -52
  56. package/dist/cjs/safeAreaState.native.js.map +1 -1
  57. package/dist/cjs/setup-burnt.cjs +15 -14
  58. package/dist/cjs/setup-burnt.native.cjs +20 -0
  59. package/dist/cjs/setup-burnt.native.js +15 -13
  60. package/dist/cjs/setup-burnt.native.js.map +1 -1
  61. package/dist/cjs/setup-expo-linear-gradient.cjs +14 -13
  62. package/dist/cjs/setup-expo-linear-gradient.native.cjs +19 -0
  63. package/dist/cjs/setup-expo-linear-gradient.native.js +14 -12
  64. package/dist/cjs/setup-expo-linear-gradient.native.js.map +1 -1
  65. package/dist/cjs/setup-expo-ui-menu.cjs +12 -13
  66. package/dist/cjs/setup-expo-ui-menu.native.cjs +18 -0
  67. package/dist/cjs/setup-expo-ui-menu.native.js +12 -12
  68. package/dist/cjs/setup-expo-ui-menu.native.js.map +1 -1
  69. package/dist/cjs/setup-gesture-handler.cjs +51 -57
  70. package/dist/cjs/setup-gesture-handler.native.cjs +72 -0
  71. package/dist/cjs/setup-gesture-handler.native.js +51 -56
  72. package/dist/cjs/setup-gesture-handler.native.js.map +1 -1
  73. package/dist/cjs/setup-keyboard-controller.native.cjs +28 -0
  74. package/dist/cjs/setup-keyboard-controller.native.js +23 -29
  75. package/dist/cjs/setup-keyboard-controller.native.js.map +1 -1
  76. package/dist/cjs/setup-safe-area.cjs +17 -3
  77. package/dist/cjs/setup-safe-area.native.cjs +28 -0
  78. package/dist/cjs/setup-safe-area.native.js +23 -16
  79. package/dist/cjs/setup-safe-area.native.js.map +1 -1
  80. package/dist/cjs/setup-teleport.cjs +16 -15
  81. package/dist/cjs/setup-teleport.native.cjs +21 -0
  82. package/dist/cjs/setup-teleport.native.js +16 -14
  83. package/dist/cjs/setup-teleport.native.js.map +1 -1
  84. package/dist/cjs/setup-worklets.cjs +19 -18
  85. package/dist/cjs/setup-worklets.native.cjs +24 -0
  86. package/dist/cjs/setup-worklets.native.js +19 -17
  87. package/dist/cjs/setup-worklets.native.js.map +1 -1
  88. package/dist/cjs/setup-zeego.cjs +18 -17
  89. package/dist/cjs/setup-zeego.native.cjs +23 -0
  90. package/dist/cjs/setup-zeego.native.js +18 -16
  91. package/dist/cjs/setup-zeego.native.js.map +1 -1
  92. package/dist/cjs/types.cjs +10 -11
  93. package/dist/cjs/types.native.cjs +19 -0
  94. package/dist/cjs/types.native.js +10 -10
  95. package/dist/cjs/types.native.js.map +1 -1
  96. package/dist/cjs/workletsState.cjs +31 -34
  97. package/dist/cjs/workletsState.native.cjs +47 -0
  98. package/dist/cjs/workletsState.native.js +31 -33
  99. package/dist/cjs/workletsState.native.js.map +1 -1
  100. package/dist/esm/PressBoundary.mjs +37 -45
  101. package/dist/esm/PressBoundary.mjs.map +1 -1
  102. package/dist/esm/PressBoundary.native.js +48 -56
  103. package/dist/esm/PressBoundary.native.js.map +1 -1
  104. package/dist/esm/burntState.mjs +16 -14
  105. package/dist/esm/burntState.mjs.map +1 -1
  106. package/dist/esm/burntState.native.js +16 -14
  107. package/dist/esm/burntState.native.js.map +1 -1
  108. package/dist/esm/components.mjs +20 -37
  109. package/dist/esm/components.mjs.map +1 -1
  110. package/dist/esm/components.native.js +21 -38
  111. package/dist/esm/components.native.js.map +1 -1
  112. package/dist/esm/expoUIMenuAdapter.mjs +224 -228
  113. package/dist/esm/expoUIMenuAdapter.mjs.map +1 -1
  114. package/dist/esm/expoUIMenuAdapter.native.js +270 -279
  115. package/dist/esm/expoUIMenuAdapter.native.js.map +1 -1
  116. package/dist/esm/gesture-handler.mjs +3 -4
  117. package/dist/esm/gesture-handler.native.js +3 -4
  118. package/dist/esm/gestureState.mjs +185 -187
  119. package/dist/esm/gestureState.mjs.map +1 -1
  120. package/dist/esm/gestureState.native.js +198 -200
  121. package/dist/esm/gestureState.native.js.map +1 -1
  122. package/dist/esm/globalState.mjs +17 -16
  123. package/dist/esm/globalState.mjs.map +1 -1
  124. package/dist/esm/globalState.native.js +17 -16
  125. package/dist/esm/globalState.native.js.map +1 -1
  126. package/dist/esm/index.js +5 -6
  127. package/dist/esm/index.mjs +5 -6
  128. package/dist/esm/index.native.js +5 -6
  129. package/dist/esm/keyboardControllerState.mjs +13 -11
  130. package/dist/esm/keyboardControllerState.mjs.map +1 -1
  131. package/dist/esm/keyboardControllerState.native.js +13 -11
  132. package/dist/esm/keyboardControllerState.native.js.map +1 -1
  133. package/dist/esm/linearGradientState.mjs +15 -13
  134. package/dist/esm/linearGradientState.mjs.map +1 -1
  135. package/dist/esm/linearGradientState.native.js +15 -13
  136. package/dist/esm/linearGradientState.native.js.map +1 -1
  137. package/dist/esm/nativeMenuContext.mjs +2 -0
  138. package/dist/esm/nativeMenuContext.mjs.map +1 -1
  139. package/dist/esm/nativeMenuContext.native.js +2 -0
  140. package/dist/esm/nativeMenuContext.native.js.map +1 -1
  141. package/dist/esm/nativeMenuState.mjs +13 -11
  142. package/dist/esm/nativeMenuState.mjs.map +1 -1
  143. package/dist/esm/nativeMenuState.native.js +13 -11
  144. package/dist/esm/nativeMenuState.native.js.map +1 -1
  145. package/dist/esm/portalState.mjs +15 -13
  146. package/dist/esm/portalState.mjs.map +1 -1
  147. package/dist/esm/portalState.native.js +15 -13
  148. package/dist/esm/portalState.native.js.map +1 -1
  149. package/dist/esm/safeAreaState.mjs +64 -38
  150. package/dist/esm/safeAreaState.mjs.map +1 -1
  151. package/dist/esm/safeAreaState.native.js +66 -38
  152. package/dist/esm/safeAreaState.native.js.map +1 -1
  153. package/dist/esm/setup-burnt.mjs +20 -13
  154. package/dist/esm/setup-burnt.mjs.map +1 -1
  155. package/dist/esm/setup-burnt.native.js +20 -13
  156. package/dist/esm/setup-burnt.native.js.map +1 -1
  157. package/dist/esm/setup-expo-linear-gradient.mjs +19 -12
  158. package/dist/esm/setup-expo-linear-gradient.mjs.map +1 -1
  159. package/dist/esm/setup-expo-linear-gradient.native.js +19 -12
  160. package/dist/esm/setup-expo-linear-gradient.native.js.map +1 -1
  161. package/dist/esm/setup-expo-ui-menu.mjs +17 -12
  162. package/dist/esm/setup-expo-ui-menu.mjs.map +1 -1
  163. package/dist/esm/setup-expo-ui-menu.native.js +17 -12
  164. package/dist/esm/setup-expo-ui-menu.native.js.map +1 -1
  165. package/dist/esm/setup-gesture-handler.mjs +42 -40
  166. package/dist/esm/setup-gesture-handler.mjs.map +1 -1
  167. package/dist/esm/setup-gesture-handler.native.js +42 -40
  168. package/dist/esm/setup-gesture-handler.native.js.map +1 -1
  169. package/dist/esm/setup-keyboard-controller.mjs +0 -2
  170. package/dist/esm/setup-keyboard-controller.native.js +28 -29
  171. package/dist/esm/setup-keyboard-controller.native.js.map +1 -1
  172. package/dist/esm/setup-safe-area.mjs +0 -4
  173. package/dist/esm/setup-safe-area.native.js +23 -22
  174. package/dist/esm/setup-safe-area.native.js.map +1 -1
  175. package/dist/esm/setup-teleport.mjs +21 -14
  176. package/dist/esm/setup-teleport.mjs.map +1 -1
  177. package/dist/esm/setup-teleport.native.js +21 -14
  178. package/dist/esm/setup-teleport.native.js.map +1 -1
  179. package/dist/esm/setup-worklets.mjs +24 -17
  180. package/dist/esm/setup-worklets.mjs.map +1 -1
  181. package/dist/esm/setup-worklets.native.js +24 -17
  182. package/dist/esm/setup-worklets.native.js.map +1 -1
  183. package/dist/esm/setup-zeego.mjs +23 -16
  184. package/dist/esm/setup-zeego.mjs.map +1 -1
  185. package/dist/esm/setup-zeego.native.js +23 -16
  186. package/dist/esm/setup-zeego.native.js.map +1 -1
  187. package/dist/esm/types.mjs +0 -2
  188. package/dist/esm/types.native.js +0 -2
  189. package/dist/esm/workletsState.mjs +18 -16
  190. package/dist/esm/workletsState.mjs.map +1 -1
  191. package/dist/esm/workletsState.native.js +18 -16
  192. package/dist/esm/workletsState.native.js.map +1 -1
  193. package/package.json +84 -7
  194. package/src/PressBoundary.tsx +48 -45
  195. package/src/expoUIMenuAdapter.tsx +1 -2
  196. package/src/gestureState.ts +2 -2
  197. package/src/index.ts +1 -1
  198. package/src/safeAreaState.ts +59 -27
  199. package/src/setup-safe-area.ts +27 -22
  200. package/src/setup-safe-area.web.ts +2 -0
  201. package/src/types.ts +1 -0
  202. package/types/PressBoundary.d.ts +3 -1
  203. package/types/PressBoundary.d.ts.map +2 -2
  204. package/types/expoUIMenuAdapter.d.ts.map +2 -2
  205. package/types/gestureState.d.ts.map +1 -1
  206. package/types/index.d.ts +1 -1
  207. package/types/index.d.ts.map +2 -2
  208. package/types/safeAreaState.d.ts +5 -2
  209. package/types/safeAreaState.d.ts.map +2 -2
  210. package/types/setup-safe-area.d.ts.map +1 -1
  211. package/types/setup-safe-area.web.d.ts +3 -0
  212. package/types/setup-safe-area.web.d.ts.map +11 -0
  213. package/types/types.d.ts +1 -0
  214. package/types/types.d.ts.map +2 -2
  215. package/.turbo/turbo-build.log +0 -2
  216. package/dist/esm/gesture-handler.mjs.map +0 -1
  217. package/dist/esm/gesture-handler.native.js.map +0 -1
  218. package/dist/esm/index.js.map +0 -1
  219. package/dist/esm/index.mjs.map +0 -1
  220. package/dist/esm/index.native.js.map +0 -1
  221. package/dist/esm/setup-keyboard-controller.mjs.map +0 -1
  222. package/dist/esm/setup-safe-area.mjs.map +0 -1
  223. package/dist/esm/types.mjs.map +0 -1
  224. package/dist/esm/types.native.js.map +0 -1
  225. package/tsconfig.json +0 -12
package/package.json CHANGED
@@ -1,12 +1,37 @@
1
1
  {
2
2
  "name": "@tamagui/native",
3
- "version": "2.7.7",
3
+ "version": "3.0.0-beta.643.1",
4
4
  "description": "Native setup modules for Tamagui - portals, gesture handler, worklets, and more",
5
5
  "type": "module",
6
6
  "sideEffects": [
7
7
  "**/setup-*",
8
8
  "**/gesture-handler*"
9
9
  ],
10
+ "files": [
11
+ "src",
12
+ "types",
13
+ "dist",
14
+ "gesture-handler",
15
+ "gesture-handler.cjs",
16
+ "setup-burnt",
17
+ "setup-burnt.cjs",
18
+ "setup-expo-linear-gradient",
19
+ "setup-expo-linear-gradient.cjs",
20
+ "setup-expo-ui-menu",
21
+ "setup-expo-ui-menu.cjs",
22
+ "setup-gesture-handler",
23
+ "setup-gesture-handler.cjs",
24
+ "setup-keyboard-controller",
25
+ "setup-keyboard-controller.cjs",
26
+ "setup-safe-area",
27
+ "setup-safe-area.cjs",
28
+ "setup-teleport",
29
+ "setup-teleport.cjs",
30
+ "setup-worklets",
31
+ "setup-worklets.cjs",
32
+ "setup-zeego",
33
+ "setup-zeego.cjs"
34
+ ],
10
35
  "main": "dist/cjs",
11
36
  "module": "dist/esm",
12
37
  "types": "./types/index.d.ts",
@@ -155,20 +180,72 @@
155
180
  "clean": "tamagui-build clean",
156
181
  "clean:build": "tamagui-build clean:build"
157
182
  },
183
+ "dependencies": {
184
+ "@tamagui/compose-refs": "3.0.0-beta.643.1"
185
+ },
158
186
  "devDependencies": {
159
- "@tamagui/build": "2.7.7",
160
- "burnt": "^0.12.2",
187
+ "@expo/ui": "~57.0.12",
188
+ "@tamagui/build": "3.0.0-beta.643.1",
189
+ "burnt": "^0.13.0",
190
+ "expo-linear-gradient": "~57.0.1",
161
191
  "react": ">=19",
162
- "react-native": "0.83.2",
163
- "react-native-gesture-handler": "~2.30.0",
164
- "react-native-keyboard-controller": "1.20.7",
192
+ "react-native": "0.86.2",
193
+ "react-native-gesture-handler": "~2.32.0",
194
+ "react-native-keyboard-controller": "1.21.9",
165
195
  "react-native-safe-area-context": "~5.7.0",
166
196
  "react-native-teleport": "^1.0.0",
197
+ "react-native-web": "^0.21.0",
167
198
  "react-native-worklets-core": "^1.0.0",
168
199
  "zeego": "^3.0.0"
169
200
  },
170
201
  "peerDependencies": {
171
- "react": "*"
202
+ "@expo/ui": "*",
203
+ "burnt": "*",
204
+ "expo-linear-gradient": "*",
205
+ "react": "*",
206
+ "react-native": "*",
207
+ "react-native-gesture-handler": "*",
208
+ "react-native-keyboard-controller": "*",
209
+ "react-native-safe-area-context": "*",
210
+ "react-native-teleport": "*",
211
+ "react-native-web": "*",
212
+ "react-native-worklets-core": "*",
213
+ "zeego": "*"
214
+ },
215
+ "peerDependenciesMeta": {
216
+ "@expo/ui": {
217
+ "optional": true
218
+ },
219
+ "burnt": {
220
+ "optional": true
221
+ },
222
+ "react-native": {
223
+ "optional": true
224
+ },
225
+ "react-native-web": {
226
+ "optional": true
227
+ },
228
+ "expo-linear-gradient": {
229
+ "optional": true
230
+ },
231
+ "react-native-gesture-handler": {
232
+ "optional": true
233
+ },
234
+ "react-native-keyboard-controller": {
235
+ "optional": true
236
+ },
237
+ "react-native-safe-area-context": {
238
+ "optional": true
239
+ },
240
+ "react-native-teleport": {
241
+ "optional": true
242
+ },
243
+ "react-native-worklets-core": {
244
+ "optional": true
245
+ },
246
+ "zeego": {
247
+ "optional": true
248
+ }
172
249
  },
173
250
  "repository": {
174
251
  "type": "git",
@@ -1,3 +1,4 @@
1
+ import { createRefComponent } from '@tamagui/compose-refs'
1
2
  import React from 'react'
2
3
  import { View, type ViewProps } from 'react-native'
3
4
  import {
@@ -30,52 +31,54 @@ function composeLast<T extends (...args: any[]) => void>(theirs: T | undefined,
30
31
  }
31
32
  }
32
33
 
33
- export const PressBoundary: React.ForwardRefExoticComponent<
34
- PressBoundaryProps & React.RefAttributes<View>
35
- > = React.forwardRef<View, PressBoundaryProps>(function PressBoundary(
36
- {
37
- enabled,
38
- stopPropagation,
39
- debugName,
40
- onTouchStart,
41
- onTouchEnd,
42
- onTouchCancel,
43
- onResponderGrant,
44
- onResponderRelease,
45
- onResponderTerminate,
46
- ...props
47
- },
48
- forwardedRef
49
- ) {
50
- const tokenRef = React.useRef<ExternalPressOwnershipToken | null>(null)
51
- const isEnabled = enabled ?? stopPropagation ?? true
34
+ export const PressBoundary: (
35
+ props: PressBoundaryProps & { ref?: React.Ref<View> }
36
+ ) => React.ReactNode = createRefComponent<View, PressBoundaryProps>(
37
+ function PressBoundary(
38
+ {
39
+ enabled,
40
+ stopPropagation,
41
+ debugName,
42
+ onTouchStart,
43
+ onTouchEnd,
44
+ onTouchCancel,
45
+ onResponderGrant,
46
+ onResponderRelease,
47
+ onResponderTerminate,
48
+ ...props
49
+ },
50
+ forwardedRef
51
+ ) {
52
+ const tokenRef = React.useRef<ExternalPressOwnershipToken | null>(null)
53
+ const isEnabled = enabled ?? stopPropagation ?? true
52
54
 
53
- const claim = React.useCallback((): void => {
54
- if (!isEnabled) return
55
- if (tokenRef.current) {
56
- releaseExternalPressOwnership(tokenRef.current, debugName)
57
- }
58
- tokenRef.current = claimExternalPressOwnership(debugName)
59
- }, [debugName, isEnabled])
55
+ const claim = React.useCallback((): void => {
56
+ if (!isEnabled) return
57
+ if (tokenRef.current) {
58
+ releaseExternalPressOwnership(tokenRef.current, debugName)
59
+ }
60
+ tokenRef.current = claimExternalPressOwnership(debugName)
61
+ }, [debugName, isEnabled])
60
62
 
61
- const release = React.useCallback((): void => {
62
- if (!tokenRef.current) return
63
- releaseExternalPressOwnership(tokenRef.current, debugName)
64
- tokenRef.current = null
65
- }, [debugName])
63
+ const release = React.useCallback((): void => {
64
+ if (!tokenRef.current) return
65
+ releaseExternalPressOwnership(tokenRef.current, debugName)
66
+ tokenRef.current = null
67
+ }, [debugName])
66
68
 
67
- React.useEffect(() => release, [release])
69
+ React.useEffect(() => release, [release])
68
70
 
69
- return (
70
- <View
71
- ref={forwardedRef}
72
- {...props}
73
- onTouchStart={composeFirst(claim, onTouchStart)}
74
- onTouchEnd={composeLast(onTouchEnd, release)}
75
- onTouchCancel={composeLast(onTouchCancel, release)}
76
- onResponderGrant={composeFirst(claim, onResponderGrant)}
77
- onResponderRelease={composeLast(onResponderRelease, release)}
78
- onResponderTerminate={composeLast(onResponderTerminate, release)}
79
- />
80
- )
81
- })
71
+ return (
72
+ <View
73
+ ref={forwardedRef}
74
+ {...props}
75
+ onTouchStart={composeFirst(claim, onTouchStart)}
76
+ onTouchEnd={composeLast(onTouchEnd, release)}
77
+ onTouchCancel={composeLast(onTouchCancel, release)}
78
+ onResponderGrant={composeFirst(claim, onResponderGrant)}
79
+ onResponderRelease={composeLast(onResponderRelease, release)}
80
+ onResponderTerminate={composeLast(onResponderTerminate, release)}
81
+ />
82
+ )
83
+ }
84
+ )
@@ -74,8 +74,7 @@ const warnedUnsupported = new Set<string>()
74
74
  // @expo/ui's MenuAction carries no subtitle, and its MenuView has no preview or
75
75
  // auxiliary view. these are decorations on a menu that is otherwise complete, so
76
76
  // drop them and warn once. throwing took down the whole app for an optional
77
- // embellishment, and an app that renders the same menu on zeego and on expo-ui
78
- // cannot avoid it without forking its markup per adapter.
77
+ // embellishment. apps cannot avoid this without forking their markup per adapter.
79
78
  function warnUnsupported(feature: string) {
80
79
  if (warnedUnsupported.has(feature) || process.env.NODE_ENV === 'production') {
81
80
  return
@@ -358,7 +358,7 @@ export function getGestureHandler(): GestureHandlerAccessor {
358
358
  const dy = touch.absoluteY - flags.moveStartY
359
359
  if (dx * dx + dy * dy <= PRESS_MOVE_CANCEL_DISTANCE_SQ) return
360
360
  // finger has travelled far enough to be a scroll/drag, not a tap.
361
- // release the pressStyle now (mid-scroll, so it doesn't stay stuck)
361
+ // release the press state now (mid-scroll, so it doesn't stay stuck)
362
362
  // and make onEnd skip onPress on finger lift.
363
363
  flags.cancelledByMove = true
364
364
  if (flags.pressInTimer) {
@@ -385,7 +385,7 @@ export function getGestureHandler(): GestureHandlerAccessor {
385
385
  // we already fired onPressIn but lost ownership before finalize
386
386
  // (e.g. finger dragged onto a sibling pressable and that one
387
387
  // claimed ownership). fire onPressOut so callers can clear their
388
- // press state - otherwise pressStyle stays stuck on this view.
388
+ // press state - otherwise the press clause stays stuck on this view.
389
389
  flags.didPressIn = false
390
390
  config.onPressOut?.(e)
391
391
  }
package/src/index.ts CHANGED
@@ -61,7 +61,7 @@ export { getWorklets } from './workletsState'
61
61
  export type { WorkletsAccessor } from './workletsState'
62
62
 
63
63
  // safe area
64
- export { getSafeArea } from './safeAreaState'
64
+ export { getSafeArea, hasSafeAreaSetup } from './safeAreaState'
65
65
  export type { SafeAreaAccessor } from './safeAreaState'
66
66
 
67
67
  // linear gradient
@@ -2,49 +2,81 @@ import { createGlobalState } from './globalState'
2
2
  import type { SafeAreaState, SafeAreaInsets, SafeAreaFrame } from './types'
3
3
 
4
4
  const state = createGlobalState<SafeAreaState>(`safe_area`, {
5
+ didSetup: false,
5
6
  enabled: false,
6
7
  useSafeAreaInsets: null,
7
8
  useSafeAreaFrame: null,
8
9
  initialMetrics: null,
9
10
  })
10
11
 
12
+ const subscriptions = createGlobalState(`safe_area_subscriptions`, {
13
+ enabled: true,
14
+ listeners: new Set<() => void>(),
15
+ })
16
+
11
17
  const defaultInsets: SafeAreaInsets = { top: 0, right: 0, bottom: 0, left: 0 }
12
18
  const defaultFrame: SafeAreaFrame = { x: 0, y: 0, width: 0, height: 0 }
13
19
 
14
20
  export interface SafeAreaAccessor {
21
+ readonly didSetup: boolean
15
22
  readonly isEnabled: boolean
16
23
  readonly state: SafeAreaState
17
24
  set(updates: Partial<SafeAreaState>): void
18
- /** Get initial insets (non-reactive, for style resolution) */
25
+ subscribe(listener: () => void): () => void
26
+ /** get the current insets snapshot */
19
27
  getInsets(): SafeAreaInsets
20
- /** Get initial frame (non-reactive) */
28
+ /** get the current frame snapshot */
21
29
  getFrame(): SafeAreaFrame
22
30
  }
23
31
 
32
+ const safeArea: SafeAreaAccessor = {
33
+ get didSetup() {
34
+ return state.get().didSetup
35
+ },
36
+ get isEnabled() {
37
+ return state.get().enabled
38
+ },
39
+ get state() {
40
+ return state.get()
41
+ },
42
+ set(updates) {
43
+ const current = state.get()
44
+ const previousMetrics = current.initialMetrics
45
+ Object.assign(current, updates)
46
+ if (
47
+ updates.initialMetrics !== undefined &&
48
+ current.initialMetrics !== previousMetrics
49
+ ) {
50
+ subscriptions.get().listeners.forEach((listener) => listener())
51
+ }
52
+ },
53
+ subscribe(listener) {
54
+ const listeners = subscriptions.get().listeners
55
+ listeners.add(listener)
56
+ return () => {
57
+ listeners.delete(listener)
58
+ }
59
+ },
60
+ getInsets() {
61
+ const current = state.get()
62
+ if (!current.enabled || !current.initialMetrics) {
63
+ return defaultInsets
64
+ }
65
+ return current.initialMetrics.insets
66
+ },
67
+ getFrame() {
68
+ const current = state.get()
69
+ if (!current.enabled || !current.initialMetrics) {
70
+ return defaultFrame
71
+ }
72
+ return current.initialMetrics.frame
73
+ },
74
+ }
75
+
24
76
  export function getSafeArea(): SafeAreaAccessor {
25
- return {
26
- get isEnabled() {
27
- return state.get().enabled
28
- },
29
- get state() {
30
- return state.get()
31
- },
32
- set(updates: Partial<SafeAreaState>) {
33
- Object.assign(state.get(), updates)
34
- },
35
- getInsets() {
36
- const s = state.get()
37
- if (!s.enabled || !s.initialMetrics) {
38
- return defaultInsets
39
- }
40
- return s.initialMetrics.insets
41
- },
42
- getFrame() {
43
- const s = state.get()
44
- if (!s.enabled || !s.initialMetrics) {
45
- return defaultFrame
46
- }
47
- return s.initialMetrics.frame
48
- },
49
- }
77
+ return safeArea
78
+ }
79
+
80
+ export function hasSafeAreaSetup(): boolean {
81
+ return state.get().didSetup
50
82
  }
@@ -22,35 +22,40 @@
22
22
  * On web, this is a no-op since CSS env(safe-area-inset-*) values work natively.
23
23
  */
24
24
 
25
+ import { useLayoutEffect } from 'react'
26
+ import {
27
+ initialWindowMetrics,
28
+ useSafeAreaFrame,
29
+ useSafeAreaInsets,
30
+ } from 'react-native-safe-area-context'
25
31
  import { getSafeArea } from './safeAreaState'
26
32
 
27
33
  function setup() {
28
- // only run on native
29
- if (process.env.TAMAGUI_TARGET !== 'native') {
30
- return
31
- }
32
-
33
- const g = globalThis as any
34
- if (g.__tamagui_native_safe_area_setup_complete) {
35
- return
36
- }
37
- g.__tamagui_native_safe_area_setup_complete = true
34
+ const safeArea = getSafeArea()
35
+ safeArea.set({ didSetup: true })
38
36
 
39
- try {
40
- const safeAreaContext = require('react-native-safe-area-context')
41
- const { useSafeAreaInsets, useSafeAreaFrame, initialWindowMetrics } = safeAreaContext
37
+ const useTrackedSafeAreaInsets = () => {
38
+ const insets = useSafeAreaInsets()
39
+ const frame = useSafeAreaFrame()
42
40
 
43
- if (useSafeAreaInsets) {
44
- getSafeArea().set({
45
- enabled: true,
46
- useSafeAreaInsets,
47
- useSafeAreaFrame: useSafeAreaFrame || null,
48
- initialMetrics: initialWindowMetrics || null,
41
+ useLayoutEffect(() => {
42
+ safeArea.set({
43
+ initialMetrics: {
44
+ insets,
45
+ frame,
46
+ },
49
47
  })
50
- }
51
- } catch {
52
- // react-native-safe-area-context not available
48
+ }, [insets, frame])
49
+
50
+ return insets
53
51
  }
52
+
53
+ safeArea.set({
54
+ enabled: true,
55
+ useSafeAreaInsets: useTrackedSafeAreaInsets,
56
+ useSafeAreaFrame,
57
+ initialMetrics: initialWindowMetrics,
58
+ })
54
59
  }
55
60
 
56
61
  setup()
@@ -0,0 +1,2 @@
1
+ // web resolves safe areas through CSS env() values.
2
+ export {}
package/src/types.ts CHANGED
@@ -44,6 +44,7 @@ export interface SafeAreaMetrics {
44
44
  }
45
45
 
46
46
  export interface SafeAreaState {
47
+ didSetup: boolean
47
48
  enabled: boolean
48
49
  useSafeAreaInsets: (() => SafeAreaInsets) | null
49
50
  useSafeAreaFrame: (() => SafeAreaFrame) | null
@@ -9,6 +9,8 @@ export interface PressBoundaryProps extends ViewProps {
9
9
  stopPropagation?: boolean;
10
10
  debugName?: string | null;
11
11
  }
12
- export declare const PressBoundary: React.ForwardRefExoticComponent<PressBoundaryProps & React.RefAttributes<View>>;
12
+ export declare const PressBoundary: (props: PressBoundaryProps & {
13
+ ref?: React.Ref<View>;
14
+ }) => React.ReactNode;
13
15
 
14
16
  //# sourceMappingURL=PressBoundary.d.ts.map
@@ -1,11 +1,11 @@
1
1
  {
2
- "mappings": "AAAA,OAAO,WAAW;AAClB,SAAS,WAAW,iBAAiB;AAOrC,iBAAiB,2BAA2B,UAAU;CACpD;;;;;CAKA;CACA;;AAiBF,OAAO,cAAM,eAAe,MAAM,0BAChC,qBAAqB,MAAM,cAAc",
2
+ "mappings": "AACA,OAAO,WAAW;AAClB,SAAS,WAAW,iBAAiB;AAOrC,iBAAiB,2BAA2B,UAAU;CACpD;;;;;CAKA;CACA;;AAiBF,OAAO,cAAM,gBACX,OAAO,qBAAqB;CAAE,MAAM,MAAM,IAAI;MAC3C,MAAM",
3
3
  "names": [],
4
4
  "sources": [
5
5
  "src/PressBoundary.tsx"
6
6
  ],
7
7
  "version": 3,
8
8
  "sourcesContent": [
9
- "import React from 'react'\nimport { View, type ViewProps } from 'react-native'\nimport {\n claimExternalPressOwnership,\n releaseExternalPressOwnership,\n type ExternalPressOwnershipToken,\n} from './gestureState'\n\nexport interface PressBoundaryProps extends ViewProps {\n enabled?: boolean\n /**\n * Alias for enabling the boundary. The behavior is limited to Tamagui's\n * shared press ownership and does not patch arbitrary RN bubbling.\n */\n stopPropagation?: boolean\n debugName?: string | null\n}\n\nfunction composeFirst<T extends (...args: any[]) => void>(ours: T, theirs?: T) {\n return (...args: Parameters<T>) => {\n ours(...args)\n theirs?.(...args)\n }\n}\n\nfunction composeLast<T extends (...args: any[]) => void>(theirs: T | undefined, ours: T) {\n return (...args: Parameters<T>) => {\n theirs?.(...args)\n ours(...args)\n }\n}\n\nexport const PressBoundary: React.ForwardRefExoticComponent<\n PressBoundaryProps & React.RefAttributes<View>\n> = React.forwardRef<View, PressBoundaryProps>(function PressBoundary(\n {\n enabled,\n stopPropagation,\n debugName,\n onTouchStart,\n onTouchEnd,\n onTouchCancel,\n onResponderGrant,\n onResponderRelease,\n onResponderTerminate,\n ...props\n },\n forwardedRef\n) {\n const tokenRef = React.useRef<ExternalPressOwnershipToken | null>(null)\n const isEnabled = enabled ?? stopPropagation ?? true\n\n const claim = React.useCallback((): void => {\n if (!isEnabled) return\n if (tokenRef.current) {\n releaseExternalPressOwnership(tokenRef.current, debugName)\n }\n tokenRef.current = claimExternalPressOwnership(debugName)\n }, [debugName, isEnabled])\n\n const release = React.useCallback((): void => {\n if (!tokenRef.current) return\n releaseExternalPressOwnership(tokenRef.current, debugName)\n tokenRef.current = null\n }, [debugName])\n\n React.useEffect(() => release, [release])\n\n return (\n <View\n ref={forwardedRef}\n {...props}\n onTouchStart={composeFirst(claim, onTouchStart)}\n onTouchEnd={composeLast(onTouchEnd, release)}\n onTouchCancel={composeLast(onTouchCancel, release)}\n onResponderGrant={composeFirst(claim, onResponderGrant)}\n onResponderRelease={composeLast(onResponderRelease, release)}\n onResponderTerminate={composeLast(onResponderTerminate, release)}\n />\n )\n})\n"
9
+ "import { createRefComponent } from '@tamagui/compose-refs'\nimport React from 'react'\nimport { View, type ViewProps } from 'react-native'\nimport {\n claimExternalPressOwnership,\n releaseExternalPressOwnership,\n type ExternalPressOwnershipToken,\n} from './gestureState'\n\nexport interface PressBoundaryProps extends ViewProps {\n enabled?: boolean\n /**\n * Alias for enabling the boundary. The behavior is limited to Tamagui's\n * shared press ownership and does not patch arbitrary RN bubbling.\n */\n stopPropagation?: boolean\n debugName?: string | null\n}\n\nfunction composeFirst<T extends (...args: any[]) => void>(ours: T, theirs?: T) {\n return (...args: Parameters<T>) => {\n ours(...args)\n theirs?.(...args)\n }\n}\n\nfunction composeLast<T extends (...args: any[]) => void>(theirs: T | undefined, ours: T) {\n return (...args: Parameters<T>) => {\n theirs?.(...args)\n ours(...args)\n }\n}\n\nexport const PressBoundary: (\n props: PressBoundaryProps & { ref?: React.Ref<View> }\n) => React.ReactNode = createRefComponent<View, PressBoundaryProps>(\n function PressBoundary(\n {\n enabled,\n stopPropagation,\n debugName,\n onTouchStart,\n onTouchEnd,\n onTouchCancel,\n onResponderGrant,\n onResponderRelease,\n onResponderTerminate,\n ...props\n },\n forwardedRef\n ) {\n const tokenRef = React.useRef<ExternalPressOwnershipToken | null>(null)\n const isEnabled = enabled ?? stopPropagation ?? true\n\n const claim = React.useCallback((): void => {\n if (!isEnabled) return\n if (tokenRef.current) {\n releaseExternalPressOwnership(tokenRef.current, debugName)\n }\n tokenRef.current = claimExternalPressOwnership(debugName)\n }, [debugName, isEnabled])\n\n const release = React.useCallback((): void => {\n if (!tokenRef.current) return\n releaseExternalPressOwnership(tokenRef.current, debugName)\n tokenRef.current = null\n }, [debugName])\n\n React.useEffect(() => release, [release])\n\n return (\n <View\n ref={forwardedRef}\n {...props}\n onTouchStart={composeFirst(claim, onTouchStart)}\n onTouchEnd={composeLast(onTouchEnd, release)}\n onTouchCancel={composeLast(onTouchCancel, release)}\n onResponderGrant={composeFirst(claim, onResponderGrant)}\n onResponderRelease={composeLast(onResponderRelease, release)}\n onResponderTerminate={composeLast(onResponderTerminate, release)}\n />\n )\n }\n)\n"
10
10
  ]
11
11
  }
@@ -1,11 +1,11 @@
1
1
  {
2
- "mappings": "AAAA,cAAc,qBAA8C;AAG5D,cAAc,yBAA2C;AA0BzD,YAAY,2BAA2B;CACrC,UAAU;;AAiEZ,OAAO,iBAAS,wBAAwB,EACtC,YACC,2BAA2B",
2
+ "mappings": "AAAA,cAAc,qBAA8C;AAG5D,cAAc,yBAA2C;AA0BzD,YAAY,2BAA2B;CACrC,UAAU;;AAgEZ,OAAO,iBAAS,wBAAwB,EACtC,YACC,2BAA2B",
3
3
  "names": [],
4
4
  "sources": [
5
5
  "src/expoUIMenuAdapter.tsx"
6
6
  ],
7
7
  "version": 3,
8
8
  "sourcesContent": [
9
- "import type { ComponentType, ReactElement, ReactNode } from 'react'\nimport React from 'react'\n\nimport type { NativeMenuAdapter, NativeMenuModule } from './nativeMenuState'\n\ntype ExpoMenuAction = {\n id: string\n title: string\n image?: unknown\n state?: 'on' | 'off'\n attributes?: {\n destructive?: boolean\n disabled?: boolean\n hidden?: boolean\n }\n subactions?: ExpoMenuAction[]\n displayInline?: boolean\n}\n\ntype ExpoMenuViewProps = {\n actions: ExpoMenuAction[]\n title?: string\n shouldOpenOnLongPress?: boolean\n onPressAction?: (event: { nativeEvent: { event: string } }) => void\n onOpenMenu?: () => void\n onCloseMenu?: () => void\n children?: ReactNode\n}\n\nexport type ExpoUIMenuAdapterOptions = {\n MenuView: ComponentType<any>\n}\n\ntype MarkerProps = {\n children?: ReactNode\n [key: string]: unknown\n}\n\nfunction marker(name: string): ComponentType<MarkerProps> {\n const Marker = () => null\n Marker.displayName = `ExpoUI${name}`\n return Marker\n}\n\nfunction isMarkerType(candidate: unknown, type: ComponentType<MarkerProps>): boolean {\n if (candidate === type) return true\n // createMenu runs every native component through withNativeMenu, which wraps\n // it in a new function component and copies the marker's displayName onto the\n // wrapper. that wrapper is a different reference, so an identity-only check\n // matches nothing and every item silently comes back with an empty title, no\n // icon, and no submenu. fall back to the (unique, ExpoUI-prefixed) name.\n const wanted = (type as { displayName?: string }).displayName\n if (!wanted) return false\n return (\n typeof candidate === 'function' &&\n (candidate as { displayName?: string }).displayName === wanted\n )\n}\n\nfunction elementOfType(\n children: ReactNode,\n type: ComponentType<MarkerProps>\n): ReactElement<MarkerProps> | null {\n for (const child of React.Children.toArray(children)) {\n if (React.isValidElement<MarkerProps>(child) && isMarkerType(child.type, type)) {\n return child\n }\n }\n return null\n}\n\nconst warnedUnsupported = new Set<string>()\n\n// @expo/ui's MenuAction carries no subtitle, and its MenuView has no preview or\n// auxiliary view. these are decorations on a menu that is otherwise complete, so\n// drop them and warn once. throwing took down the whole app for an optional\n// embellishment, and an app that renders the same menu on zeego and on expo-ui\n// cannot avoid it without forking its markup per adapter.\nfunction warnUnsupported(feature: string) {\n if (warnedUnsupported.has(feature) || process.env.NODE_ENV === 'production') {\n return\n }\n warnedUnsupported.add(feature)\n console.warn(\n `[Tamagui] @expo/ui/community/menu does not support ${feature}, so it will not render. ` +\n `Register a different native menu adapter if you need it.`\n )\n}\n\nfunction textFromNode(node: ReactNode): string {\n if (typeof node === 'string' || typeof node === 'number') return String(node)\n if (!React.isValidElement<MarkerProps>(node)) return ''\n return React.Children.toArray(node.props.children).map(textFromNode).join('')\n}\n\nexport function createExpoUIMenuAdapter({\n MenuView,\n}: ExpoUIMenuAdapterOptions): NativeMenuAdapter {\n const NativeMenuView: ComponentType<ExpoMenuViewProps> = MenuView\n\n function createModule(isContextMenu: boolean): NativeMenuModule {\n const Trigger = marker('Trigger')\n const Content = marker('Content')\n const Item = marker('Item')\n const ItemTitle = marker('ItemTitle')\n const ItemSubtitle = marker('ItemSubtitle')\n const ItemIcon = marker('ItemIcon')\n const ItemImage = marker('ItemImage')\n const ItemIndicator = marker('ItemIndicator')\n const Group = marker('Group')\n const Label = marker('Label')\n const Separator = marker('Separator')\n const Sub = marker('Sub')\n const SubTrigger = marker('SubTrigger')\n const SubContent = marker('SubContent')\n const CheckboxItem = marker('CheckboxItem')\n const Preview = marker('Preview')\n const Auxiliary = marker('Auxiliary')\n\n function Root({\n children,\n onOpenChange,\n onOpenWillChange,\n }: {\n children?: ReactNode\n onOpenChange?: (open: boolean) => void\n onOpenWillChange?: (open: boolean) => void\n }) {\n const trigger = elementOfType(children, Trigger)\n const content = elementOfType(children, Content)\n const preview = elementOfType(children, Preview)\n const auxiliary = elementOfType(children, Auxiliary)\n\n if (preview || auxiliary) {\n warnUnsupported('context-menu previews or auxiliary views')\n }\n if (!trigger || !content) {\n throw new Error('Tamagui native menus require one Trigger and one Content')\n }\n\n let nextId = 0\n const handlers = new Map<string, () => void>()\n\n function actionId(element: ReactElement<MarkerProps>): string {\n return element.key == null\n ? `tamagui-menu-${nextId++}`\n : String(element.key).replace(/^\\.\\$/, '')\n }\n\n function itemTitle(element: ReactElement<MarkerProps>): string {\n if (typeof element.props.textValue === 'string') return element.props.textValue\n const title = elementOfType(element.props.children, ItemTitle)\n return title ? textFromNode(title.props.children) : ''\n }\n\n function actionImage(element: ReactElement<MarkerProps>): unknown {\n const image = elementOfType(element.props.children, ItemImage)\n if (image?.props.source) return image.props.source\n const icon = elementOfType(element.props.children, ItemIcon)\n if (!icon) return undefined\n const ios = icon.props.ios\n if (\n typeof ios === 'object' &&\n ios &&\n 'name' in ios &&\n typeof ios.name === 'string'\n ) {\n return ios.name\n }\n return typeof icon.props.iosIconName === 'string'\n ? icon.props.iosIconName\n : undefined\n }\n\n function attributes(\n element: ReactElement<MarkerProps>\n ): ExpoMenuAction['attributes'] {\n const destructive = element.props.destructive === true\n const disabled = element.props.disabled === true\n const hidden = element.props.hidden === true\n return destructive || disabled || hidden\n ? { destructive, disabled, hidden }\n : undefined\n }\n\n function actionsFrom(childrenToParse: ReactNode): ExpoMenuAction[] {\n const actions: ExpoMenuAction[] = []\n let section: ExpoMenuAction[] | null = null\n\n function append(action: ExpoMenuAction) {\n if (section) {\n section.push(action)\n } else {\n actions.push(action)\n }\n }\n\n function flushSection() {\n if (!section?.length) {\n section = []\n return\n }\n actions.push({\n id: `tamagui-menu-section-${nextId++}`,\n title: '',\n subactions: section,\n displayInline: true,\n })\n section = []\n }\n\n for (const child of React.Children.toArray(childrenToParse)) {\n if (!React.isValidElement<MarkerProps>(child)) continue\n\n if (isMarkerType(child.type, Separator)) {\n if (!section) section = actions.splice(0)\n flushSection()\n continue\n }\n\n if (isMarkerType(child.type, Group)) {\n const label = elementOfType(child.props.children, Label)\n append({\n id: actionId(child),\n title: label ? textFromNode(label.props.children) : '',\n subactions: actionsFrom(child.props.children),\n displayInline: true,\n })\n continue\n }\n\n if (isMarkerType(child.type, Sub)) {\n const subTrigger = elementOfType(child.props.children, SubTrigger)\n const subContent = elementOfType(child.props.children, SubContent)\n if (!subTrigger || !subContent) {\n throw new Error(\n 'Tamagui native submenus require one SubTrigger and SubContent'\n )\n }\n append({\n id: actionId(child),\n title: itemTitle(subTrigger),\n image: actionImage(subTrigger),\n attributes: attributes(subTrigger),\n subactions: actionsFrom(subContent.props.children),\n })\n continue\n }\n\n if (!isMarkerType(child.type, Item) && !isMarkerType(child.type, CheckboxItem))\n continue\n\n if (elementOfType(child.props.children, ItemSubtitle)) {\n warnUnsupported('menu item subtitles')\n }\n\n const id = actionId(child)\n if (isMarkerType(child.type, CheckboxItem)) {\n const current =\n child.props.value === true ||\n child.props.value === 'on' ||\n child.props.checked === true\n const onValueChange = child.props.onValueChange\n const onCheckedChange = child.props.onCheckedChange\n handlers.set(id, () => {\n if (typeof onValueChange === 'function') {\n onValueChange(current ? 'off' : 'on', current ? 'on' : 'off')\n } else if (typeof onCheckedChange === 'function') {\n onCheckedChange(!current)\n }\n })\n append({\n id,\n title: itemTitle(child),\n image: actionImage(child),\n state: current ? 'on' : 'off',\n attributes: attributes(child),\n })\n continue\n }\n\n const onSelect = child.props.onSelect\n if (typeof onSelect === 'function') {\n handlers.set(id, () => onSelect())\n }\n append({\n id,\n title: itemTitle(child),\n image: actionImage(child),\n attributes: attributes(child),\n })\n }\n\n if (section) flushSection()\n return actions\n }\n\n const label = elementOfType(content.props.children, Label)\n const triggerAction = trigger.props.action\n\n return (\n <NativeMenuView\n actions={actionsFrom(content.props.children)}\n title={label ? textFromNode(label.props.children) : undefined}\n shouldOpenOnLongPress={\n triggerAction === 'longPress' || (triggerAction !== 'press' && isContextMenu)\n }\n onPressAction={(event) => handlers.get(event.nativeEvent.event)?.()}\n onOpenMenu={() => {\n onOpenWillChange?.(true)\n onOpenChange?.(true)\n }}\n onCloseMenu={() => {\n onOpenWillChange?.(false)\n onOpenChange?.(false)\n }}\n >\n {trigger.props.children}\n </NativeMenuView>\n )\n }\n\n return {\n Root,\n Trigger,\n Content,\n Item,\n ItemTitle,\n ItemSubtitle,\n ItemIcon,\n ItemImage,\n ItemIndicator,\n Group,\n Label,\n Separator,\n Sub,\n SubTrigger,\n SubContent,\n CheckboxItem,\n Preview,\n Auxiliary,\n }\n }\n\n return {\n name: 'expo-ui',\n Menu: createModule(false),\n ContextMenu: createModule(true),\n }\n}\n"
9
+ "import type { ComponentType, ReactElement, ReactNode } from 'react'\nimport React from 'react'\n\nimport type { NativeMenuAdapter, NativeMenuModule } from './nativeMenuState'\n\ntype ExpoMenuAction = {\n id: string\n title: string\n image?: unknown\n state?: 'on' | 'off'\n attributes?: {\n destructive?: boolean\n disabled?: boolean\n hidden?: boolean\n }\n subactions?: ExpoMenuAction[]\n displayInline?: boolean\n}\n\ntype ExpoMenuViewProps = {\n actions: ExpoMenuAction[]\n title?: string\n shouldOpenOnLongPress?: boolean\n onPressAction?: (event: { nativeEvent: { event: string } }) => void\n onOpenMenu?: () => void\n onCloseMenu?: () => void\n children?: ReactNode\n}\n\nexport type ExpoUIMenuAdapterOptions = {\n MenuView: ComponentType<any>\n}\n\ntype MarkerProps = {\n children?: ReactNode\n [key: string]: unknown\n}\n\nfunction marker(name: string): ComponentType<MarkerProps> {\n const Marker = () => null\n Marker.displayName = `ExpoUI${name}`\n return Marker\n}\n\nfunction isMarkerType(candidate: unknown, type: ComponentType<MarkerProps>): boolean {\n if (candidate === type) return true\n // createMenu runs every native component through withNativeMenu, which wraps\n // it in a new function component and copies the marker's displayName onto the\n // wrapper. that wrapper is a different reference, so an identity-only check\n // matches nothing and every item silently comes back with an empty title, no\n // icon, and no submenu. fall back to the (unique, ExpoUI-prefixed) name.\n const wanted = (type as { displayName?: string }).displayName\n if (!wanted) return false\n return (\n typeof candidate === 'function' &&\n (candidate as { displayName?: string }).displayName === wanted\n )\n}\n\nfunction elementOfType(\n children: ReactNode,\n type: ComponentType<MarkerProps>\n): ReactElement<MarkerProps> | null {\n for (const child of React.Children.toArray(children)) {\n if (React.isValidElement<MarkerProps>(child) && isMarkerType(child.type, type)) {\n return child\n }\n }\n return null\n}\n\nconst warnedUnsupported = new Set<string>()\n\n// @expo/ui's MenuAction carries no subtitle, and its MenuView has no preview or\n// auxiliary view. these are decorations on a menu that is otherwise complete, so\n// drop them and warn once. throwing took down the whole app for an optional\n// embellishment. apps cannot avoid this without forking their markup per adapter.\nfunction warnUnsupported(feature: string) {\n if (warnedUnsupported.has(feature) || process.env.NODE_ENV === 'production') {\n return\n }\n warnedUnsupported.add(feature)\n console.warn(\n `[Tamagui] @expo/ui/community/menu does not support ${feature}, so it will not render. ` +\n `Register a different native menu adapter if you need it.`\n )\n}\n\nfunction textFromNode(node: ReactNode): string {\n if (typeof node === 'string' || typeof node === 'number') return String(node)\n if (!React.isValidElement<MarkerProps>(node)) return ''\n return React.Children.toArray(node.props.children).map(textFromNode).join('')\n}\n\nexport function createExpoUIMenuAdapter({\n MenuView,\n}: ExpoUIMenuAdapterOptions): NativeMenuAdapter {\n const NativeMenuView: ComponentType<ExpoMenuViewProps> = MenuView\n\n function createModule(isContextMenu: boolean): NativeMenuModule {\n const Trigger = marker('Trigger')\n const Content = marker('Content')\n const Item = marker('Item')\n const ItemTitle = marker('ItemTitle')\n const ItemSubtitle = marker('ItemSubtitle')\n const ItemIcon = marker('ItemIcon')\n const ItemImage = marker('ItemImage')\n const ItemIndicator = marker('ItemIndicator')\n const Group = marker('Group')\n const Label = marker('Label')\n const Separator = marker('Separator')\n const Sub = marker('Sub')\n const SubTrigger = marker('SubTrigger')\n const SubContent = marker('SubContent')\n const CheckboxItem = marker('CheckboxItem')\n const Preview = marker('Preview')\n const Auxiliary = marker('Auxiliary')\n\n function Root({\n children,\n onOpenChange,\n onOpenWillChange,\n }: {\n children?: ReactNode\n onOpenChange?: (open: boolean) => void\n onOpenWillChange?: (open: boolean) => void\n }) {\n const trigger = elementOfType(children, Trigger)\n const content = elementOfType(children, Content)\n const preview = elementOfType(children, Preview)\n const auxiliary = elementOfType(children, Auxiliary)\n\n if (preview || auxiliary) {\n warnUnsupported('context-menu previews or auxiliary views')\n }\n if (!trigger || !content) {\n throw new Error('Tamagui native menus require one Trigger and one Content')\n }\n\n let nextId = 0\n const handlers = new Map<string, () => void>()\n\n function actionId(element: ReactElement<MarkerProps>): string {\n return element.key == null\n ? `tamagui-menu-${nextId++}`\n : String(element.key).replace(/^\\.\\$/, '')\n }\n\n function itemTitle(element: ReactElement<MarkerProps>): string {\n if (typeof element.props.textValue === 'string') return element.props.textValue\n const title = elementOfType(element.props.children, ItemTitle)\n return title ? textFromNode(title.props.children) : ''\n }\n\n function actionImage(element: ReactElement<MarkerProps>): unknown {\n const image = elementOfType(element.props.children, ItemImage)\n if (image?.props.source) return image.props.source\n const icon = elementOfType(element.props.children, ItemIcon)\n if (!icon) return undefined\n const ios = icon.props.ios\n if (\n typeof ios === 'object' &&\n ios &&\n 'name' in ios &&\n typeof ios.name === 'string'\n ) {\n return ios.name\n }\n return typeof icon.props.iosIconName === 'string'\n ? icon.props.iosIconName\n : undefined\n }\n\n function attributes(\n element: ReactElement<MarkerProps>\n ): ExpoMenuAction['attributes'] {\n const destructive = element.props.destructive === true\n const disabled = element.props.disabled === true\n const hidden = element.props.hidden === true\n return destructive || disabled || hidden\n ? { destructive, disabled, hidden }\n : undefined\n }\n\n function actionsFrom(childrenToParse: ReactNode): ExpoMenuAction[] {\n const actions: ExpoMenuAction[] = []\n let section: ExpoMenuAction[] | null = null\n\n function append(action: ExpoMenuAction) {\n if (section) {\n section.push(action)\n } else {\n actions.push(action)\n }\n }\n\n function flushSection() {\n if (!section?.length) {\n section = []\n return\n }\n actions.push({\n id: `tamagui-menu-section-${nextId++}`,\n title: '',\n subactions: section,\n displayInline: true,\n })\n section = []\n }\n\n for (const child of React.Children.toArray(childrenToParse)) {\n if (!React.isValidElement<MarkerProps>(child)) continue\n\n if (isMarkerType(child.type, Separator)) {\n if (!section) section = actions.splice(0)\n flushSection()\n continue\n }\n\n if (isMarkerType(child.type, Group)) {\n const label = elementOfType(child.props.children, Label)\n append({\n id: actionId(child),\n title: label ? textFromNode(label.props.children) : '',\n subactions: actionsFrom(child.props.children),\n displayInline: true,\n })\n continue\n }\n\n if (isMarkerType(child.type, Sub)) {\n const subTrigger = elementOfType(child.props.children, SubTrigger)\n const subContent = elementOfType(child.props.children, SubContent)\n if (!subTrigger || !subContent) {\n throw new Error(\n 'Tamagui native submenus require one SubTrigger and SubContent'\n )\n }\n append({\n id: actionId(child),\n title: itemTitle(subTrigger),\n image: actionImage(subTrigger),\n attributes: attributes(subTrigger),\n subactions: actionsFrom(subContent.props.children),\n })\n continue\n }\n\n if (!isMarkerType(child.type, Item) && !isMarkerType(child.type, CheckboxItem))\n continue\n\n if (elementOfType(child.props.children, ItemSubtitle)) {\n warnUnsupported('menu item subtitles')\n }\n\n const id = actionId(child)\n if (isMarkerType(child.type, CheckboxItem)) {\n const current =\n child.props.value === true ||\n child.props.value === 'on' ||\n child.props.checked === true\n const onValueChange = child.props.onValueChange\n const onCheckedChange = child.props.onCheckedChange\n handlers.set(id, () => {\n if (typeof onValueChange === 'function') {\n onValueChange(current ? 'off' : 'on', current ? 'on' : 'off')\n } else if (typeof onCheckedChange === 'function') {\n onCheckedChange(!current)\n }\n })\n append({\n id,\n title: itemTitle(child),\n image: actionImage(child),\n state: current ? 'on' : 'off',\n attributes: attributes(child),\n })\n continue\n }\n\n const onSelect = child.props.onSelect\n if (typeof onSelect === 'function') {\n handlers.set(id, () => onSelect())\n }\n append({\n id,\n title: itemTitle(child),\n image: actionImage(child),\n attributes: attributes(child),\n })\n }\n\n if (section) flushSection()\n return actions\n }\n\n const label = elementOfType(content.props.children, Label)\n const triggerAction = trigger.props.action\n\n return (\n <NativeMenuView\n actions={actionsFrom(content.props.children)}\n title={label ? textFromNode(label.props.children) : undefined}\n shouldOpenOnLongPress={\n triggerAction === 'longPress' || (triggerAction !== 'press' && isContextMenu)\n }\n onPressAction={(event) => handlers.get(event.nativeEvent.event)?.()}\n onOpenMenu={() => {\n onOpenWillChange?.(true)\n onOpenChange?.(true)\n }}\n onCloseMenu={() => {\n onOpenWillChange?.(false)\n onOpenChange?.(false)\n }}\n >\n {trigger.props.children}\n </NativeMenuView>\n )\n }\n\n return {\n Root,\n Trigger,\n Content,\n Item,\n ItemTitle,\n ItemSubtitle,\n ItemIcon,\n ItemImage,\n ItemIndicator,\n Group,\n Label,\n Separator,\n Sub,\n SubTrigger,\n SubContent,\n CheckboxItem,\n Preview,\n Auxiliary,\n }\n }\n\n return {\n name: 'expo-ui',\n Menu: createModule(false),\n ContextMenu: createModule(true),\n }\n}\n"
10
10
  ]
11
11
  }
@@ -6,6 +6,6 @@
6
6
  ],
7
7
  "version": 3,
8
8
  "sourcesContent": [
9
- "import { createGlobalState } from './globalState'\nimport type { GestureState } from './types'\n\nconst state = createGlobalState<GestureState>(`gesture`, {\n enabled: false,\n Gesture: null,\n GestureDetector: null,\n ScrollView: null,\n RootView: null,\n})\n\ntype GestureEnabledFreezeState = {\n frozen: boolean\n enabled: boolean\n warned: boolean\n}\n\nconst GESTURE_ENABLED_FREEZE_KEY = '__tamagui_gesture_enabled_freeze__'\n\nfunction getGestureEnabledFreezeState(): GestureEnabledFreezeState {\n const g = globalThis as typeof globalThis & {\n [GESTURE_ENABLED_FREEZE_KEY]?: GestureEnabledFreezeState\n }\n\n if (!g[GESTURE_ENABLED_FREEZE_KEY]) {\n g[GESTURE_ENABLED_FREEZE_KEY] = {\n frozen: false,\n enabled: false,\n warned: false,\n }\n }\n\n return g[GESTURE_ENABLED_FREEZE_KEY]!\n}\n\nfunction warnGestureEnabledMutationIgnored(source: string) {\n const freezeState = getGestureEnabledFreezeState()\n\n if (freezeState.warned || process.env.NODE_ENV === 'production') {\n return\n }\n\n freezeState.warned = true\n\n console.warn(\n `[Tamagui] Ignored ${source} because gesture handler press events were already ` +\n `${freezeState.enabled ? 'enabled' : 'disabled'} when TamaguiProvider mounted. ` +\n `Configure gesture handler mode before the first render.`\n )\n}\n\nexport function canChangeGestureHandlerEnabled(\n nextEnabled: boolean,\n source: string\n): boolean {\n const freezeState = getGestureEnabledFreezeState()\n\n if (!freezeState.frozen || freezeState.enabled === nextEnabled) {\n return true\n }\n\n warnGestureEnabledMutationIgnored(source)\n return false\n}\n\nlet pressGestureDebugId = 0\nlet externalPressDebugId = 0\n\n// distance in dp the finger may travel before a press is treated as a\n// scroll/drag and cancelled. tracked in absolute (screen) coordinates by the\n// Tap's onTouchesMove (see createPressGesture) so a view translating under a\n// stationary finger - a sheet sliding on keyboard open/close, a sheet open\n// animation - does not cancel the press; only genuine finger travel does.\n// this also makes scroll-cancels-press work for any scroll container (a plain\n// RN ScrollView/FlatList or an RNGH one), since the press cancels itself\n// rather than relying on a parent RNGH gesture to steal it.\nconst PRESS_MOVE_CANCEL_DISTANCE = 12\nconst PRESS_MOVE_CANCEL_DISTANCE_SQ =\n PRESS_MOVE_CANCEL_DISTANCE * PRESS_MOVE_CANCEL_DISTANCE\n\n// minimal shapes of the RNGH gesture events createPressGesture reads. RNGH is\n// injected at runtime (GestureState.Gesture is untyped), so this module keeps\n// no type dependency on react-native-gesture-handler - these name just the\n// fields the press gesture touches.\ntype GesturePoint = {\n absoluteX: number\n absoluteY: number\n}\ntype GestureBeginEvent = Partial<GesturePoint>\ntype GestureTouchEvent = {\n changedTouches?: GesturePoint[]\n allTouches?: GesturePoint[]\n}\n\ntype PressOwnerSource = 'internal' | 'external' | null\ntype PressOwnershipState = {\n owner: object | null\n ownerId: number | null\n ownerSource: PressOwnerSource\n ownerPointerId: number | null\n timestamp: number\n}\n\nconst PRESS_STATE_KEY = '__tamagui_press_state__'\n\nfunction getEventPointerId(e: any): number | null {\n const pointerId =\n e?.pointerId ??\n e?.pointer?.id ??\n e?.event?.pointerId ??\n e?.event?.pointer?.id ??\n e?.nativeEvent?.pointerId ??\n e?.nativeEvent?.id ??\n e?.event?.nativeEvent?.pointerId ??\n e?.event?.nativeEvent?.id ??\n null\n\n return pointerId == null || Number.isNaN(pointerId) ? null : Number(pointerId)\n}\n\n/**\n * Global press coordination - ensures only innermost pressable fires press events,\n * matching RN Pressable/responder system semantics where deepest component wins.\n * Uses a grace period to allow child gestures to steal ownership from parent,\n * since RNGH fires parent gestures before child gestures.\n */\nconst pressState = ((\n globalThis as typeof globalThis & {\n [PRESS_STATE_KEY]?: PressOwnershipState\n }\n)[PRESS_STATE_KEY] ??= {\n owner: null,\n ownerId: null,\n ownerSource: null,\n ownerPointerId: null,\n timestamp: 0,\n})\n\nexport interface Insets {\n top?: number\n left?: number\n bottom?: number\n right?: number\n}\n\nexport type PressGestureConfig = {\n debugName?: string | null\n onPressIn?: (e: any) => void\n onPressOut?: (e: any) => void\n onPress?: (e: any) => void\n onLongPress?: (e: any) => void\n delayLongPress?: number\n hitSlop?: number | Insets | null\n}\n\nexport interface GestureHandlerAccessor {\n readonly isEnabled: boolean\n readonly state: GestureState\n set(updates: Partial<GestureState>): void\n disable(): void\n createPressGesture(config: PressGestureConfig): any\n}\n\nexport type ExternalPressOwnershipToken = object\n\nfunction resetPressOwner() {\n pressState.owner = null\n pressState.ownerId = null\n pressState.ownerSource = null\n pressState.ownerPointerId = null\n pressState.timestamp = 0\n}\n\nfunction resetStaleOwner(now: number, debugName?: string | null) {\n if (now - pressState.timestamp > 2000) {\n resetPressOwner()\n }\n}\n\nexport function claimExternalPressOwnership(\n debugName?: string | null\n): ExternalPressOwnershipToken {\n const now = Date.now()\n resetStaleOwner(now, debugName)\n\n const token = {}\n const ownerId = ++externalPressDebugId\n\n pressState.owner = token\n pressState.ownerId = ownerId\n pressState.ownerSource = 'external'\n pressState.timestamp = now\n\n return token\n}\n\nexport function releaseExternalPressOwnership(\n token: ExternalPressOwnershipToken | null | undefined,\n debugName?: string | null\n): void {\n if (!token || pressState.owner !== token) {\n return\n }\n resetPressOwner()\n}\n\nexport function hasExternalPressOwnership(): boolean {\n resetStaleOwner(Date.now())\n return pressState.ownerSource === 'external'\n}\n\nexport function getGestureHandler(): GestureHandlerAccessor {\n return {\n get isEnabled(): boolean {\n return state.get().enabled\n },\n get state(): GestureState {\n return state.get()\n },\n set(updates: Partial<GestureState>): void {\n if (\n updates.enabled !== undefined &&\n !canChangeGestureHandlerEnabled(updates.enabled, 'getGestureHandler().set()')\n ) {\n return\n }\n\n Object.assign(state.get(), updates)\n },\n\n disable(): void {\n if (!canChangeGestureHandlerEnabled(false, 'getGestureHandler().disable()')) {\n return\n }\n\n state.get().enabled = false\n },\n\n createPressGesture(config: PressGestureConfig): any {\n const { Gesture } = state.get()\n if (!Gesture) return null\n\n const longPressDuration = config.delayLongPress ?? 500\n\n // unique token for this gesture instance - used to track ownership\n const myToken = {}\n const myDebugId = ++pressGestureDebugId\n // mutable gesture state kept on an object so handler bodies that get\n // workletized by react-native-worklets (>=0.7.4) see live values.\n // primitive `let`s get frozen into the worklet's serialized __closure\n // at factory time, so reassignments from workletized handlers never\n // propagate out. object property mutation goes through the captured\n // reference and is always observed.\n const flags = {\n didLongPress: false,\n didPressIn: false,\n pressInTimer: null as ReturnType<typeof setTimeout> | null,\n // absolute (screen) coords of the press start, and whether the finger\n // has since travelled far enough to be treated as a scroll/drag.\n moveStartX: null as number | null,\n moveStartY: 0,\n cancelledByMove: false,\n }\n\n // Grace period for child gestures to steal ownership from parent.\n // RNGH fires parent before child, but we want innermost to win.\n // Claims typically span a few ms, 24ms gives enough room to observe\n // ordering while debugging slower frames.\n const GRACE_PERIOD_MS = process.env.TAMAGUI_RNGH_PRESS_DELAY\n ? +process.env.TAMAGUI_RNGH_PRESS_DELAY\n : 24\n\n const tryClaimOwnership = (e: any) => {\n const now = Date.now()\n resetStaleOwner(now, config.debugName)\n\n const currentPointerId = getEventPointerId(e)\n const isSameTouchPointer =\n currentPointerId == null ||\n pressState.ownerPointerId == null ||\n pressState.ownerPointerId === currentPointerId\n\n if (\n pressState.owner === null ||\n (pressState.ownerSource === 'internal' && isSameTouchPointer)\n ) {\n pressState.owner = myToken\n pressState.ownerId = myDebugId\n pressState.ownerSource = 'internal'\n pressState.ownerPointerId = currentPointerId\n pressState.timestamp = now\n }\n return pressState.owner === myToken\n }\n\n const isOwner = () => pressState.owner === myToken\n\n const releaseOwnership = () => {\n if (flags.pressInTimer) {\n clearTimeout(flags.pressInTimer)\n flags.pressInTimer = null\n }\n if (pressState.owner === myToken) {\n resetPressOwner()\n }\n }\n\n const firePressIn = (e: any) => {\n if (!flags.didPressIn && isOwner()) {\n flags.didPressIn = true\n config.onPressIn?.(e)\n }\n }\n\n const schedulePressIn = (e: any) => {\n if (flags.pressInTimer) {\n clearTimeout(flags.pressInTimer)\n }\n flags.pressInTimer = setTimeout(() => {\n flags.pressInTimer = null\n if (isOwner()) {\n firePressIn(e)\n }\n }, GRACE_PERIOD_MS + 1)\n }\n\n // Tap gesture for regular presses.\n //\n // maxDuration is long so long-presses aren't cut off. We deliberately do\n // not use RNGH's .maxDistance(): it measures finger travel in the gesture\n // view's own coordinate space, so a view that translates under a\n // stationary finger (a sheet sliding on keyboard open/close, a sheet open\n // animation) reads as finger movement and silently cancels the press -\n // onPress never fires. Instead onTouchesMove tracks the finger in\n // absolute (screen) coordinates and cancels only on genuine finger\n // travel. That also makes scroll-cancels-press work for any scroll\n // container, since the press cancels itself instead of depending on a\n // parent RNGH gesture to steal the touch.\n const tap = Gesture.Tap()\n .runOnJS(true)\n .maxDuration(10000) // allow very long presses\n .onBegin((e: GestureBeginEvent) => {\n flags.didLongPress = false\n flags.didPressIn = false\n flags.cancelledByMove = false\n flags.moveStartX = typeof e.absoluteX === 'number' ? e.absoluteX : null\n flags.moveStartY = typeof e.absoluteY === 'number' ? e.absoluteY : 0\n tryClaimOwnership(e)\n // Defer onPressIn until after the grace window so child pressables\n // can steal ownership, but flush it on tap end for very fast taps.\n schedulePressIn(e)\n })\n .onTouchesMove((e: GestureTouchEvent) => {\n if (flags.cancelledByMove || flags.moveStartX === null) return\n const touch = e.changedTouches?.[0] ?? e.allTouches?.[0]\n if (!touch) return\n const dx = touch.absoluteX - flags.moveStartX\n const dy = touch.absoluteY - flags.moveStartY\n if (dx * dx + dy * dy <= PRESS_MOVE_CANCEL_DISTANCE_SQ) return\n // finger has travelled far enough to be a scroll/drag, not a tap.\n // release the pressStyle now (mid-scroll, so it doesn't stay stuck)\n // and make onEnd skip onPress on finger lift.\n flags.cancelledByMove = true\n if (flags.pressInTimer) {\n clearTimeout(flags.pressInTimer)\n flags.pressInTimer = null\n }\n if (flags.didPressIn) {\n flags.didPressIn = false\n config.onPressOut?.(e)\n }\n releaseOwnership()\n })\n .onEnd((e: unknown) => {\n if (isOwner() && !flags.didLongPress && !flags.cancelledByMove) {\n firePressIn(e)\n config.onPress?.(e)\n }\n })\n .onFinalize((e: unknown) => {\n if (isOwner()) {\n config.onPressOut?.(e)\n releaseOwnership()\n } else if (flags.didPressIn) {\n // we already fired onPressIn but lost ownership before finalize\n // (e.g. finger dragged onto a sibling pressable and that one\n // claimed ownership). fire onPressOut so callers can clear their\n // press state - otherwise pressStyle stays stuck on this view.\n flags.didPressIn = false\n config.onPressOut?.(e)\n }\n })\n\n if (config.hitSlop) tap.hitSlop(config.hitSlop)\n\n // if no long press handler, just use tap\n if (!config.onLongPress) return tap\n\n // LongPress gesture for long press handling\n const longPress = Gesture.LongPress()\n .runOnJS(true)\n .minDuration(longPressDuration)\n .onStart((e: any) => {\n flags.didLongPress = true\n if (isOwner()) {\n firePressIn(e)\n config.onLongPress?.(e)\n }\n })\n\n if (config.hitSlop) longPress.hitSlop(config.hitSlop)\n\n // exclusive: longPress has priority, tap is fallback for quick presses\n return Gesture.Exclusive(longPress, tap)\n },\n }\n}\n"
9
+ "import { createGlobalState } from './globalState'\nimport type { GestureState } from './types'\n\nconst state = createGlobalState<GestureState>(`gesture`, {\n enabled: false,\n Gesture: null,\n GestureDetector: null,\n ScrollView: null,\n RootView: null,\n})\n\ntype GestureEnabledFreezeState = {\n frozen: boolean\n enabled: boolean\n warned: boolean\n}\n\nconst GESTURE_ENABLED_FREEZE_KEY = '__tamagui_gesture_enabled_freeze__'\n\nfunction getGestureEnabledFreezeState(): GestureEnabledFreezeState {\n const g = globalThis as typeof globalThis & {\n [GESTURE_ENABLED_FREEZE_KEY]?: GestureEnabledFreezeState\n }\n\n if (!g[GESTURE_ENABLED_FREEZE_KEY]) {\n g[GESTURE_ENABLED_FREEZE_KEY] = {\n frozen: false,\n enabled: false,\n warned: false,\n }\n }\n\n return g[GESTURE_ENABLED_FREEZE_KEY]!\n}\n\nfunction warnGestureEnabledMutationIgnored(source: string) {\n const freezeState = getGestureEnabledFreezeState()\n\n if (freezeState.warned || process.env.NODE_ENV === 'production') {\n return\n }\n\n freezeState.warned = true\n\n console.warn(\n `[Tamagui] Ignored ${source} because gesture handler press events were already ` +\n `${freezeState.enabled ? 'enabled' : 'disabled'} when TamaguiProvider mounted. ` +\n `Configure gesture handler mode before the first render.`\n )\n}\n\nexport function canChangeGestureHandlerEnabled(\n nextEnabled: boolean,\n source: string\n): boolean {\n const freezeState = getGestureEnabledFreezeState()\n\n if (!freezeState.frozen || freezeState.enabled === nextEnabled) {\n return true\n }\n\n warnGestureEnabledMutationIgnored(source)\n return false\n}\n\nlet pressGestureDebugId = 0\nlet externalPressDebugId = 0\n\n// distance in dp the finger may travel before a press is treated as a\n// scroll/drag and cancelled. tracked in absolute (screen) coordinates by the\n// Tap's onTouchesMove (see createPressGesture) so a view translating under a\n// stationary finger - a sheet sliding on keyboard open/close, a sheet open\n// animation - does not cancel the press; only genuine finger travel does.\n// this also makes scroll-cancels-press work for any scroll container (a plain\n// RN ScrollView/FlatList or an RNGH one), since the press cancels itself\n// rather than relying on a parent RNGH gesture to steal it.\nconst PRESS_MOVE_CANCEL_DISTANCE = 12\nconst PRESS_MOVE_CANCEL_DISTANCE_SQ =\n PRESS_MOVE_CANCEL_DISTANCE * PRESS_MOVE_CANCEL_DISTANCE\n\n// minimal shapes of the RNGH gesture events createPressGesture reads. RNGH is\n// injected at runtime (GestureState.Gesture is untyped), so this module keeps\n// no type dependency on react-native-gesture-handler - these name just the\n// fields the press gesture touches.\ntype GesturePoint = {\n absoluteX: number\n absoluteY: number\n}\ntype GestureBeginEvent = Partial<GesturePoint>\ntype GestureTouchEvent = {\n changedTouches?: GesturePoint[]\n allTouches?: GesturePoint[]\n}\n\ntype PressOwnerSource = 'internal' | 'external' | null\ntype PressOwnershipState = {\n owner: object | null\n ownerId: number | null\n ownerSource: PressOwnerSource\n ownerPointerId: number | null\n timestamp: number\n}\n\nconst PRESS_STATE_KEY = '__tamagui_press_state__'\n\nfunction getEventPointerId(e: any): number | null {\n const pointerId =\n e?.pointerId ??\n e?.pointer?.id ??\n e?.event?.pointerId ??\n e?.event?.pointer?.id ??\n e?.nativeEvent?.pointerId ??\n e?.nativeEvent?.id ??\n e?.event?.nativeEvent?.pointerId ??\n e?.event?.nativeEvent?.id ??\n null\n\n return pointerId == null || Number.isNaN(pointerId) ? null : Number(pointerId)\n}\n\n/**\n * Global press coordination - ensures only innermost pressable fires press events,\n * matching RN Pressable/responder system semantics where deepest component wins.\n * Uses a grace period to allow child gestures to steal ownership from parent,\n * since RNGH fires parent gestures before child gestures.\n */\nconst pressState = ((\n globalThis as typeof globalThis & {\n [PRESS_STATE_KEY]?: PressOwnershipState\n }\n)[PRESS_STATE_KEY] ??= {\n owner: null,\n ownerId: null,\n ownerSource: null,\n ownerPointerId: null,\n timestamp: 0,\n})\n\nexport interface Insets {\n top?: number\n left?: number\n bottom?: number\n right?: number\n}\n\nexport type PressGestureConfig = {\n debugName?: string | null\n onPressIn?: (e: any) => void\n onPressOut?: (e: any) => void\n onPress?: (e: any) => void\n onLongPress?: (e: any) => void\n delayLongPress?: number\n hitSlop?: number | Insets | null\n}\n\nexport interface GestureHandlerAccessor {\n readonly isEnabled: boolean\n readonly state: GestureState\n set(updates: Partial<GestureState>): void\n disable(): void\n createPressGesture(config: PressGestureConfig): any\n}\n\nexport type ExternalPressOwnershipToken = object\n\nfunction resetPressOwner() {\n pressState.owner = null\n pressState.ownerId = null\n pressState.ownerSource = null\n pressState.ownerPointerId = null\n pressState.timestamp = 0\n}\n\nfunction resetStaleOwner(now: number, debugName?: string | null) {\n if (now - pressState.timestamp > 2000) {\n resetPressOwner()\n }\n}\n\nexport function claimExternalPressOwnership(\n debugName?: string | null\n): ExternalPressOwnershipToken {\n const now = Date.now()\n resetStaleOwner(now, debugName)\n\n const token = {}\n const ownerId = ++externalPressDebugId\n\n pressState.owner = token\n pressState.ownerId = ownerId\n pressState.ownerSource = 'external'\n pressState.timestamp = now\n\n return token\n}\n\nexport function releaseExternalPressOwnership(\n token: ExternalPressOwnershipToken | null | undefined,\n debugName?: string | null\n): void {\n if (!token || pressState.owner !== token) {\n return\n }\n resetPressOwner()\n}\n\nexport function hasExternalPressOwnership(): boolean {\n resetStaleOwner(Date.now())\n return pressState.ownerSource === 'external'\n}\n\nexport function getGestureHandler(): GestureHandlerAccessor {\n return {\n get isEnabled(): boolean {\n return state.get().enabled\n },\n get state(): GestureState {\n return state.get()\n },\n set(updates: Partial<GestureState>): void {\n if (\n updates.enabled !== undefined &&\n !canChangeGestureHandlerEnabled(updates.enabled, 'getGestureHandler().set()')\n ) {\n return\n }\n\n Object.assign(state.get(), updates)\n },\n\n disable(): void {\n if (!canChangeGestureHandlerEnabled(false, 'getGestureHandler().disable()')) {\n return\n }\n\n state.get().enabled = false\n },\n\n createPressGesture(config: PressGestureConfig): any {\n const { Gesture } = state.get()\n if (!Gesture) return null\n\n const longPressDuration = config.delayLongPress ?? 500\n\n // unique token for this gesture instance - used to track ownership\n const myToken = {}\n const myDebugId = ++pressGestureDebugId\n // mutable gesture state kept on an object so handler bodies that get\n // workletized by react-native-worklets (>=0.7.4) see live values.\n // primitive `let`s get frozen into the worklet's serialized __closure\n // at factory time, so reassignments from workletized handlers never\n // propagate out. object property mutation goes through the captured\n // reference and is always observed.\n const flags = {\n didLongPress: false,\n didPressIn: false,\n pressInTimer: null as ReturnType<typeof setTimeout> | null,\n // absolute (screen) coords of the press start, and whether the finger\n // has since travelled far enough to be treated as a scroll/drag.\n moveStartX: null as number | null,\n moveStartY: 0,\n cancelledByMove: false,\n }\n\n // Grace period for child gestures to steal ownership from parent.\n // RNGH fires parent before child, but we want innermost to win.\n // Claims typically span a few ms, 24ms gives enough room to observe\n // ordering while debugging slower frames.\n const GRACE_PERIOD_MS = process.env.TAMAGUI_RNGH_PRESS_DELAY\n ? +process.env.TAMAGUI_RNGH_PRESS_DELAY\n : 24\n\n const tryClaimOwnership = (e: any) => {\n const now = Date.now()\n resetStaleOwner(now, config.debugName)\n\n const currentPointerId = getEventPointerId(e)\n const isSameTouchPointer =\n currentPointerId == null ||\n pressState.ownerPointerId == null ||\n pressState.ownerPointerId === currentPointerId\n\n if (\n pressState.owner === null ||\n (pressState.ownerSource === 'internal' && isSameTouchPointer)\n ) {\n pressState.owner = myToken\n pressState.ownerId = myDebugId\n pressState.ownerSource = 'internal'\n pressState.ownerPointerId = currentPointerId\n pressState.timestamp = now\n }\n return pressState.owner === myToken\n }\n\n const isOwner = () => pressState.owner === myToken\n\n const releaseOwnership = () => {\n if (flags.pressInTimer) {\n clearTimeout(flags.pressInTimer)\n flags.pressInTimer = null\n }\n if (pressState.owner === myToken) {\n resetPressOwner()\n }\n }\n\n const firePressIn = (e: any) => {\n if (!flags.didPressIn && isOwner()) {\n flags.didPressIn = true\n config.onPressIn?.(e)\n }\n }\n\n const schedulePressIn = (e: any) => {\n if (flags.pressInTimer) {\n clearTimeout(flags.pressInTimer)\n }\n flags.pressInTimer = setTimeout(() => {\n flags.pressInTimer = null\n if (isOwner()) {\n firePressIn(e)\n }\n }, GRACE_PERIOD_MS + 1)\n }\n\n // Tap gesture for regular presses.\n //\n // maxDuration is long so long-presses aren't cut off. We deliberately do\n // not use RNGH's .maxDistance(): it measures finger travel in the gesture\n // view's own coordinate space, so a view that translates under a\n // stationary finger (a sheet sliding on keyboard open/close, a sheet open\n // animation) reads as finger movement and silently cancels the press -\n // onPress never fires. Instead onTouchesMove tracks the finger in\n // absolute (screen) coordinates and cancels only on genuine finger\n // travel. That also makes scroll-cancels-press work for any scroll\n // container, since the press cancels itself instead of depending on a\n // parent RNGH gesture to steal the touch.\n const tap = Gesture.Tap()\n .runOnJS(true)\n .maxDuration(10000) // allow very long presses\n .onBegin((e: GestureBeginEvent) => {\n flags.didLongPress = false\n flags.didPressIn = false\n flags.cancelledByMove = false\n flags.moveStartX = typeof e.absoluteX === 'number' ? e.absoluteX : null\n flags.moveStartY = typeof e.absoluteY === 'number' ? e.absoluteY : 0\n tryClaimOwnership(e)\n // Defer onPressIn until after the grace window so child pressables\n // can steal ownership, but flush it on tap end for very fast taps.\n schedulePressIn(e)\n })\n .onTouchesMove((e: GestureTouchEvent) => {\n if (flags.cancelledByMove || flags.moveStartX === null) return\n const touch = e.changedTouches?.[0] ?? e.allTouches?.[0]\n if (!touch) return\n const dx = touch.absoluteX - flags.moveStartX\n const dy = touch.absoluteY - flags.moveStartY\n if (dx * dx + dy * dy <= PRESS_MOVE_CANCEL_DISTANCE_SQ) return\n // finger has travelled far enough to be a scroll/drag, not a tap.\n // release the press state now (mid-scroll, so it doesn't stay stuck)\n // and make onEnd skip onPress on finger lift.\n flags.cancelledByMove = true\n if (flags.pressInTimer) {\n clearTimeout(flags.pressInTimer)\n flags.pressInTimer = null\n }\n if (flags.didPressIn) {\n flags.didPressIn = false\n config.onPressOut?.(e)\n }\n releaseOwnership()\n })\n .onEnd((e: unknown) => {\n if (isOwner() && !flags.didLongPress && !flags.cancelledByMove) {\n firePressIn(e)\n config.onPress?.(e)\n }\n })\n .onFinalize((e: unknown) => {\n if (isOwner()) {\n config.onPressOut?.(e)\n releaseOwnership()\n } else if (flags.didPressIn) {\n // we already fired onPressIn but lost ownership before finalize\n // (e.g. finger dragged onto a sibling pressable and that one\n // claimed ownership). fire onPressOut so callers can clear their\n // press state - otherwise the press clause stays stuck on this view.\n flags.didPressIn = false\n config.onPressOut?.(e)\n }\n })\n\n if (config.hitSlop) tap.hitSlop(config.hitSlop)\n\n // if no long press handler, just use tap\n if (!config.onLongPress) return tap\n\n // LongPress gesture for long press handling\n const longPress = Gesture.LongPress()\n .runOnJS(true)\n .minDuration(longPressDuration)\n .onStart((e: any) => {\n flags.didLongPress = true\n if (isOwner()) {\n firePressIn(e)\n config.onLongPress?.(e)\n }\n })\n\n if (config.hitSlop) longPress.hitSlop(config.hitSlop)\n\n // exclusive: longPress has priority, tap is fallback for quick presses\n return Gesture.Exclusive(longPress, tap)\n },\n }\n}\n"
10
10
  ]
11
11
  }
package/types/index.d.ts CHANGED
@@ -28,7 +28,7 @@ export type { ExternalPressOwnershipToken, GestureHandlerAccessor, PressGestureC
28
28
  export type { GestureHandlerConfig } from "./setup-gesture-handler";
29
29
  export { getWorklets } from "./workletsState";
30
30
  export type { WorkletsAccessor } from "./workletsState";
31
- export { getSafeArea } from "./safeAreaState";
31
+ export { getSafeArea, hasSafeAreaSetup } from "./safeAreaState";
32
32
  export type { SafeAreaAccessor } from "./safeAreaState";
33
33
  export { getLinearGradient } from "./linearGradientState";
34
34
  export type { LinearGradientAccessor } from "./linearGradientState";