@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.
- package/dist/cjs/PressBoundary.cjs +53 -71
- package/dist/cjs/PressBoundary.native.cjs +86 -0
- package/dist/cjs/PressBoundary.native.js +63 -80
- package/dist/cjs/PressBoundary.native.js.map +1 -1
- package/dist/cjs/burntState.cjs +29 -32
- package/dist/cjs/burntState.native.cjs +45 -0
- package/dist/cjs/burntState.native.js +29 -31
- package/dist/cjs/burntState.native.js.map +1 -1
- package/dist/cjs/components.cjs +35 -55
- package/dist/cjs/components.native.cjs +56 -0
- package/dist/cjs/components.native.js +35 -54
- package/dist/cjs/components.native.js.map +1 -1
- package/dist/cjs/expoUIMenuAdapter.cjs +240 -254
- package/dist/cjs/expoUIMenuAdapter.native.cjs +315 -0
- package/dist/cjs/expoUIMenuAdapter.native.js +285 -303
- package/dist/cjs/expoUIMenuAdapter.native.js.map +1 -1
- package/dist/cjs/gesture-handler.cjs +21 -22
- package/dist/cjs/gesture-handler.native.cjs +38 -0
- package/dist/cjs/gesture-handler.native.js +21 -21
- package/dist/cjs/gesture-handler.native.js.map +1 -1
- package/dist/cjs/gestureState.cjs +202 -207
- package/dist/cjs/gestureState.native.cjs +262 -0
- package/dist/cjs/gestureState.native.js +215 -219
- package/dist/cjs/gestureState.native.js.map +1 -1
- package/dist/cjs/globalState.cjs +31 -34
- package/dist/cjs/globalState.native.cjs +44 -0
- package/dist/cjs/globalState.native.js +31 -33
- package/dist/cjs/globalState.native.js.map +1 -1
- package/dist/cjs/index.cjs +34 -34
- package/dist/cjs/index.native.cjs +59 -0
- package/dist/cjs/index.native.js +34 -33
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/keyboardControllerState.cjs +28 -29
- package/dist/cjs/keyboardControllerState.native.cjs +50 -0
- package/dist/cjs/keyboardControllerState.native.js +28 -28
- package/dist/cjs/keyboardControllerState.native.js.map +1 -1
- package/dist/cjs/linearGradientState.cjs +28 -31
- package/dist/cjs/linearGradientState.native.cjs +44 -0
- package/dist/cjs/linearGradientState.native.js +28 -30
- package/dist/cjs/linearGradientState.native.js.map +1 -1
- package/dist/cjs/nativeMenuContext.cjs +15 -18
- package/dist/cjs/nativeMenuContext.native.cjs +28 -0
- package/dist/cjs/nativeMenuContext.native.js +15 -17
- package/dist/cjs/nativeMenuContext.native.js.map +1 -1
- package/dist/cjs/nativeMenuState.cjs +27 -28
- package/dist/cjs/nativeMenuState.native.cjs +47 -0
- package/dist/cjs/nativeMenuState.native.js +27 -27
- package/dist/cjs/nativeMenuState.native.js.map +1 -1
- package/dist/cjs/portalState.cjs +28 -31
- package/dist/cjs/portalState.native.cjs +44 -0
- package/dist/cjs/portalState.native.js +28 -30
- package/dist/cjs/portalState.native.js.map +1 -1
- package/dist/cjs/safeAreaState.cjs +77 -53
- package/dist/cjs/safeAreaState.native.cjs +101 -0
- package/dist/cjs/safeAreaState.native.js +79 -52
- package/dist/cjs/safeAreaState.native.js.map +1 -1
- package/dist/cjs/setup-burnt.cjs +15 -14
- package/dist/cjs/setup-burnt.native.cjs +20 -0
- package/dist/cjs/setup-burnt.native.js +15 -13
- package/dist/cjs/setup-burnt.native.js.map +1 -1
- package/dist/cjs/setup-expo-linear-gradient.cjs +14 -13
- package/dist/cjs/setup-expo-linear-gradient.native.cjs +19 -0
- package/dist/cjs/setup-expo-linear-gradient.native.js +14 -12
- package/dist/cjs/setup-expo-linear-gradient.native.js.map +1 -1
- package/dist/cjs/setup-expo-ui-menu.cjs +12 -13
- package/dist/cjs/setup-expo-ui-menu.native.cjs +18 -0
- package/dist/cjs/setup-expo-ui-menu.native.js +12 -12
- package/dist/cjs/setup-expo-ui-menu.native.js.map +1 -1
- package/dist/cjs/setup-gesture-handler.cjs +51 -57
- package/dist/cjs/setup-gesture-handler.native.cjs +72 -0
- package/dist/cjs/setup-gesture-handler.native.js +51 -56
- package/dist/cjs/setup-gesture-handler.native.js.map +1 -1
- package/dist/cjs/setup-keyboard-controller.native.cjs +28 -0
- package/dist/cjs/setup-keyboard-controller.native.js +23 -29
- package/dist/cjs/setup-keyboard-controller.native.js.map +1 -1
- package/dist/cjs/setup-safe-area.cjs +17 -3
- package/dist/cjs/setup-safe-area.native.cjs +28 -0
- package/dist/cjs/setup-safe-area.native.js +23 -16
- package/dist/cjs/setup-safe-area.native.js.map +1 -1
- package/dist/cjs/setup-teleport.cjs +16 -15
- package/dist/cjs/setup-teleport.native.cjs +21 -0
- package/dist/cjs/setup-teleport.native.js +16 -14
- package/dist/cjs/setup-teleport.native.js.map +1 -1
- package/dist/cjs/setup-worklets.cjs +19 -18
- package/dist/cjs/setup-worklets.native.cjs +24 -0
- package/dist/cjs/setup-worklets.native.js +19 -17
- package/dist/cjs/setup-worklets.native.js.map +1 -1
- package/dist/cjs/setup-zeego.cjs +18 -17
- package/dist/cjs/setup-zeego.native.cjs +23 -0
- package/dist/cjs/setup-zeego.native.js +18 -16
- package/dist/cjs/setup-zeego.native.js.map +1 -1
- package/dist/cjs/types.cjs +10 -11
- package/dist/cjs/types.native.cjs +19 -0
- package/dist/cjs/types.native.js +10 -10
- package/dist/cjs/types.native.js.map +1 -1
- package/dist/cjs/workletsState.cjs +31 -34
- package/dist/cjs/workletsState.native.cjs +47 -0
- package/dist/cjs/workletsState.native.js +31 -33
- package/dist/cjs/workletsState.native.js.map +1 -1
- package/dist/esm/PressBoundary.mjs +37 -45
- package/dist/esm/PressBoundary.mjs.map +1 -1
- package/dist/esm/PressBoundary.native.js +48 -56
- package/dist/esm/PressBoundary.native.js.map +1 -1
- package/dist/esm/burntState.mjs +16 -14
- package/dist/esm/burntState.mjs.map +1 -1
- package/dist/esm/burntState.native.js +16 -14
- package/dist/esm/burntState.native.js.map +1 -1
- package/dist/esm/components.mjs +20 -37
- package/dist/esm/components.mjs.map +1 -1
- package/dist/esm/components.native.js +21 -38
- package/dist/esm/components.native.js.map +1 -1
- package/dist/esm/expoUIMenuAdapter.mjs +224 -228
- package/dist/esm/expoUIMenuAdapter.mjs.map +1 -1
- package/dist/esm/expoUIMenuAdapter.native.js +270 -279
- package/dist/esm/expoUIMenuAdapter.native.js.map +1 -1
- package/dist/esm/gesture-handler.mjs +3 -4
- package/dist/esm/gesture-handler.native.js +3 -4
- package/dist/esm/gestureState.mjs +185 -187
- package/dist/esm/gestureState.mjs.map +1 -1
- package/dist/esm/gestureState.native.js +198 -200
- package/dist/esm/gestureState.native.js.map +1 -1
- package/dist/esm/globalState.mjs +17 -16
- package/dist/esm/globalState.mjs.map +1 -1
- package/dist/esm/globalState.native.js +17 -16
- package/dist/esm/globalState.native.js.map +1 -1
- package/dist/esm/index.js +5 -6
- package/dist/esm/index.mjs +5 -6
- package/dist/esm/index.native.js +5 -6
- package/dist/esm/keyboardControllerState.mjs +13 -11
- package/dist/esm/keyboardControllerState.mjs.map +1 -1
- package/dist/esm/keyboardControllerState.native.js +13 -11
- package/dist/esm/keyboardControllerState.native.js.map +1 -1
- package/dist/esm/linearGradientState.mjs +15 -13
- package/dist/esm/linearGradientState.mjs.map +1 -1
- package/dist/esm/linearGradientState.native.js +15 -13
- package/dist/esm/linearGradientState.native.js.map +1 -1
- package/dist/esm/nativeMenuContext.mjs +2 -0
- package/dist/esm/nativeMenuContext.mjs.map +1 -1
- package/dist/esm/nativeMenuContext.native.js +2 -0
- package/dist/esm/nativeMenuContext.native.js.map +1 -1
- package/dist/esm/nativeMenuState.mjs +13 -11
- package/dist/esm/nativeMenuState.mjs.map +1 -1
- package/dist/esm/nativeMenuState.native.js +13 -11
- package/dist/esm/nativeMenuState.native.js.map +1 -1
- package/dist/esm/portalState.mjs +15 -13
- package/dist/esm/portalState.mjs.map +1 -1
- package/dist/esm/portalState.native.js +15 -13
- package/dist/esm/portalState.native.js.map +1 -1
- package/dist/esm/safeAreaState.mjs +64 -38
- package/dist/esm/safeAreaState.mjs.map +1 -1
- package/dist/esm/safeAreaState.native.js +66 -38
- package/dist/esm/safeAreaState.native.js.map +1 -1
- package/dist/esm/setup-burnt.mjs +20 -13
- package/dist/esm/setup-burnt.mjs.map +1 -1
- package/dist/esm/setup-burnt.native.js +20 -13
- package/dist/esm/setup-burnt.native.js.map +1 -1
- package/dist/esm/setup-expo-linear-gradient.mjs +19 -12
- package/dist/esm/setup-expo-linear-gradient.mjs.map +1 -1
- package/dist/esm/setup-expo-linear-gradient.native.js +19 -12
- package/dist/esm/setup-expo-linear-gradient.native.js.map +1 -1
- package/dist/esm/setup-expo-ui-menu.mjs +17 -12
- package/dist/esm/setup-expo-ui-menu.mjs.map +1 -1
- package/dist/esm/setup-expo-ui-menu.native.js +17 -12
- package/dist/esm/setup-expo-ui-menu.native.js.map +1 -1
- package/dist/esm/setup-gesture-handler.mjs +42 -40
- package/dist/esm/setup-gesture-handler.mjs.map +1 -1
- package/dist/esm/setup-gesture-handler.native.js +42 -40
- package/dist/esm/setup-gesture-handler.native.js.map +1 -1
- package/dist/esm/setup-keyboard-controller.mjs +0 -2
- package/dist/esm/setup-keyboard-controller.native.js +28 -29
- package/dist/esm/setup-keyboard-controller.native.js.map +1 -1
- package/dist/esm/setup-safe-area.mjs +0 -4
- package/dist/esm/setup-safe-area.native.js +23 -22
- package/dist/esm/setup-safe-area.native.js.map +1 -1
- package/dist/esm/setup-teleport.mjs +21 -14
- package/dist/esm/setup-teleport.mjs.map +1 -1
- package/dist/esm/setup-teleport.native.js +21 -14
- package/dist/esm/setup-teleport.native.js.map +1 -1
- package/dist/esm/setup-worklets.mjs +24 -17
- package/dist/esm/setup-worklets.mjs.map +1 -1
- package/dist/esm/setup-worklets.native.js +24 -17
- package/dist/esm/setup-worklets.native.js.map +1 -1
- package/dist/esm/setup-zeego.mjs +23 -16
- package/dist/esm/setup-zeego.mjs.map +1 -1
- package/dist/esm/setup-zeego.native.js +23 -16
- package/dist/esm/setup-zeego.native.js.map +1 -1
- package/dist/esm/types.mjs +0 -2
- package/dist/esm/types.native.js +0 -2
- package/dist/esm/workletsState.mjs +18 -16
- package/dist/esm/workletsState.mjs.map +1 -1
- package/dist/esm/workletsState.native.js +18 -16
- package/dist/esm/workletsState.native.js.map +1 -1
- package/package.json +84 -7
- package/src/PressBoundary.tsx +48 -45
- package/src/expoUIMenuAdapter.tsx +1 -2
- package/src/gestureState.ts +2 -2
- package/src/index.ts +1 -1
- package/src/safeAreaState.ts +59 -27
- package/src/setup-safe-area.ts +27 -22
- package/src/setup-safe-area.web.ts +2 -0
- package/src/types.ts +1 -0
- package/types/PressBoundary.d.ts +3 -1
- package/types/PressBoundary.d.ts.map +2 -2
- package/types/expoUIMenuAdapter.d.ts.map +2 -2
- package/types/gestureState.d.ts.map +1 -1
- package/types/index.d.ts +1 -1
- package/types/index.d.ts.map +2 -2
- package/types/safeAreaState.d.ts +5 -2
- package/types/safeAreaState.d.ts.map +2 -2
- package/types/setup-safe-area.d.ts.map +1 -1
- package/types/setup-safe-area.web.d.ts +3 -0
- package/types/setup-safe-area.web.d.ts.map +11 -0
- package/types/types.d.ts +1 -0
- package/types/types.d.ts.map +2 -2
- package/.turbo/turbo-build.log +0 -2
- package/dist/esm/gesture-handler.mjs.map +0 -1
- package/dist/esm/gesture-handler.native.js.map +0 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/index.mjs.map +0 -1
- package/dist/esm/index.native.js.map +0 -1
- package/dist/esm/setup-keyboard-controller.mjs.map +0 -1
- package/dist/esm/setup-safe-area.mjs.map +0 -1
- package/dist/esm/types.mjs.map +0 -1
- package/dist/esm/types.native.js.map +0 -1
- package/tsconfig.json +0 -12
package/package.json
CHANGED
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tamagui/native",
|
|
3
|
-
"version": "
|
|
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
|
-
"@
|
|
160
|
-
"
|
|
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.
|
|
163
|
-
"react-native-gesture-handler": "~2.
|
|
164
|
-
"react-native-keyboard-controller": "1.
|
|
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
|
-
"
|
|
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",
|
package/src/PressBoundary.tsx
CHANGED
|
@@ -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:
|
|
34
|
-
PressBoundaryProps & React.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
+
React.useEffect(() => release, [release])
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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
|
package/src/gestureState.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
package/src/safeAreaState.ts
CHANGED
|
@@ -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
|
-
|
|
25
|
+
subscribe(listener: () => void): () => void
|
|
26
|
+
/** get the current insets snapshot */
|
|
19
27
|
getInsets(): SafeAreaInsets
|
|
20
|
-
/**
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
}
|
package/src/setup-safe-area.ts
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
40
|
-
const
|
|
41
|
-
const
|
|
37
|
+
const useTrackedSafeAreaInsets = () => {
|
|
38
|
+
const insets = useSafeAreaInsets()
|
|
39
|
+
const frame = useSafeAreaFrame()
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
useLayoutEffect(() => {
|
|
42
|
+
safeArea.set({
|
|
43
|
+
initialMetrics: {
|
|
44
|
+
insets,
|
|
45
|
+
frame,
|
|
46
|
+
},
|
|
49
47
|
})
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
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()
|
package/src/types.ts
CHANGED
package/types/PressBoundary.d.ts
CHANGED
|
@@ -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:
|
|
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": "
|
|
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:
|
|
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;;
|
|
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";
|