@hanzogui/sheet 7.3.0 → 8.0.0
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/.turbo/turbo-build.log +2 -0
- package/controller/index.cjs +2 -0
- package/controller/index.d.ts +5 -0
- package/controller/index.js +2 -0
- package/controller/index.native.cjs +2 -0
- package/controller/index.native.js +2 -0
- package/dist/cjs/GestureDetectorWrapper.native.js.map +1 -1
- package/dist/cjs/SheetImplementationCustom.cjs +137 -74
- package/dist/cjs/SheetImplementationCustom.native.js +162 -83
- package/dist/cjs/SheetImplementationCustom.native.js.map +1 -1
- package/dist/cjs/SheetScrollView.cjs +155 -34
- package/dist/cjs/SheetScrollView.native.js +160 -35
- package/dist/cjs/SheetScrollView.native.js.map +1 -1
- package/dist/cjs/createSheet.cjs +7 -3
- package/dist/cjs/createSheet.native.js +7 -3
- package/dist/cjs/createSheet.native.js.map +1 -1
- package/dist/cjs/gestureState.cjs +13 -2
- package/dist/cjs/gestureState.native.js +14 -2
- package/dist/cjs/gestureState.native.js.map +1 -1
- package/dist/cjs/index.cjs +12 -1
- package/dist/cjs/index.native.js +11 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/keyboardAvoidance.cjs +93 -0
- package/dist/cjs/keyboardAvoidance.native.js +99 -0
- package/dist/cjs/keyboardAvoidance.native.js.map +1 -0
- package/dist/cjs/nativeSheet.cjs +3 -0
- package/dist/cjs/nativeSheet.native.js +3 -0
- package/dist/cjs/nativeSheet.native.js.map +1 -1
- package/dist/cjs/setupGestureHandler.cjs +9 -6
- package/dist/cjs/setupGestureHandler.native.js +11 -6
- package/dist/cjs/setupGestureHandler.native.js.map +1 -1
- package/dist/cjs/useGestureHandlerPan.cjs +86 -18
- package/dist/cjs/useGestureHandlerPan.native.js +91 -18
- package/dist/cjs/useGestureHandlerPan.native.js.map +1 -1
- package/dist/cjs/useKeyboardControllerSheet.cjs +68 -6
- package/dist/cjs/useSafeAreaInsets.cjs +44 -0
- package/dist/cjs/useSafeAreaInsets.native.js +48 -0
- package/dist/cjs/useSafeAreaInsets.native.js.map +1 -0
- package/dist/cjs/useSheetProviderProps.cjs +1 -1
- package/dist/cjs/useSheetProviderProps.native.js +1 -1
- package/dist/cjs/useSheetProviderProps.native.js.map +1 -1
- package/dist/cjs/useSheetScrollViewGestures.cjs +30 -5
- package/dist/cjs/useSheetScrollViewGestures.native.js +4 -0
- package/dist/cjs/useSheetScrollViewGestures.native.js.map +1 -1
- package/dist/cjs/webViewport.cjs +75 -0
- package/dist/cjs/webViewport.native.js +81 -0
- package/dist/cjs/webViewport.native.js.map +1 -0
- package/dist/esm/GestureDetectorWrapper.mjs.map +1 -1
- package/dist/esm/GestureDetectorWrapper.native.js.map +1 -1
- package/dist/esm/SheetImplementationCustom.mjs +143 -84
- package/dist/esm/SheetImplementationCustom.mjs.map +1 -1
- package/dist/esm/SheetImplementationCustom.native.js +173 -102
- package/dist/esm/SheetImplementationCustom.native.js.map +1 -1
- package/dist/esm/SheetScrollView.mjs +157 -36
- package/dist/esm/SheetScrollView.mjs.map +1 -1
- package/dist/esm/SheetScrollView.native.js +162 -37
- package/dist/esm/SheetScrollView.native.js.map +1 -1
- package/dist/esm/createSheet.mjs +9 -6
- package/dist/esm/createSheet.mjs.map +1 -1
- package/dist/esm/createSheet.native.js +19 -15
- package/dist/esm/createSheet.native.js.map +1 -1
- package/dist/esm/gestureState.mjs +13 -2
- package/dist/esm/gestureState.mjs.map +1 -1
- package/dist/esm/gestureState.native.js +14 -2
- package/dist/esm/gestureState.native.js.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index.mjs +2 -0
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +2 -0
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/keyboardAvoidance.mjs +66 -0
- package/dist/esm/keyboardAvoidance.mjs.map +1 -0
- package/dist/esm/keyboardAvoidance.native.js +69 -0
- package/dist/esm/keyboardAvoidance.native.js.map +1 -0
- package/dist/esm/nativeSheet.mjs +3 -0
- package/dist/esm/nativeSheet.mjs.map +1 -1
- package/dist/esm/nativeSheet.native.js +3 -0
- package/dist/esm/nativeSheet.native.js.map +1 -1
- package/dist/esm/setupGestureHandler.mjs +9 -6
- package/dist/esm/setupGestureHandler.mjs.map +1 -1
- package/dist/esm/setupGestureHandler.native.js +11 -6
- package/dist/esm/setupGestureHandler.native.js.map +1 -1
- package/dist/esm/useGestureHandlerPan.mjs +87 -19
- package/dist/esm/useGestureHandlerPan.mjs.map +1 -1
- package/dist/esm/useGestureHandlerPan.native.js +92 -19
- package/dist/esm/useGestureHandlerPan.native.js.map +1 -1
- package/dist/esm/useKeyboardControllerSheet.mjs +69 -7
- package/dist/esm/useKeyboardControllerSheet.mjs.map +1 -1
- package/dist/esm/useSafeAreaInsets.mjs +7 -0
- package/dist/esm/useSafeAreaInsets.mjs.map +1 -0
- package/dist/esm/useSafeAreaInsets.native.js +8 -0
- package/dist/esm/useSafeAreaInsets.native.js.map +1 -0
- package/dist/esm/useSheetProviderProps.mjs +1 -1
- package/dist/esm/useSheetProviderProps.mjs.map +1 -1
- package/dist/esm/useSheetProviderProps.native.js +1 -1
- package/dist/esm/useSheetProviderProps.native.js.map +1 -1
- package/dist/esm/useSheetScrollViewGestures.mjs +30 -5
- package/dist/esm/useSheetScrollViewGestures.mjs.map +1 -1
- package/dist/esm/useSheetScrollViewGestures.native.js +4 -0
- package/dist/esm/useSheetScrollViewGestures.native.js.map +1 -1
- package/dist/esm/webViewport.mjs +43 -0
- package/dist/esm/webViewport.mjs.map +1 -0
- package/dist/esm/webViewport.native.js +46 -0
- package/dist/esm/webViewport.native.js.map +1 -0
- package/dist/jsx/GestureDetectorWrapper.mjs.map +1 -1
- package/dist/jsx/GestureDetectorWrapper.native.js.map +1 -1
- package/dist/jsx/SheetImplementationCustom.mjs +143 -84
- package/dist/jsx/SheetImplementationCustom.mjs.map +1 -1
- package/dist/jsx/SheetImplementationCustom.native.js +162 -83
- package/dist/jsx/SheetImplementationCustom.native.js.map +1 -1
- package/dist/jsx/SheetScrollView.mjs +157 -36
- package/dist/jsx/SheetScrollView.mjs.map +1 -1
- package/dist/jsx/SheetScrollView.native.js +160 -35
- package/dist/jsx/SheetScrollView.native.js.map +1 -1
- package/dist/jsx/createSheet.mjs +9 -6
- package/dist/jsx/createSheet.mjs.map +1 -1
- package/dist/jsx/createSheet.native.js +7 -3
- package/dist/jsx/createSheet.native.js.map +1 -1
- package/dist/jsx/gestureState.mjs +13 -2
- package/dist/jsx/gestureState.mjs.map +1 -1
- package/dist/jsx/gestureState.native.js +14 -2
- package/dist/jsx/gestureState.native.js.map +1 -1
- package/dist/jsx/index.js +2 -0
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +2 -0
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +11 -0
- package/dist/jsx/index.native.js.map +1 -1
- package/dist/jsx/keyboardAvoidance.mjs +66 -0
- package/dist/jsx/keyboardAvoidance.mjs.map +1 -0
- package/dist/jsx/keyboardAvoidance.native.js +99 -0
- package/dist/jsx/keyboardAvoidance.native.js.map +1 -0
- package/dist/jsx/nativeSheet.mjs +3 -0
- package/dist/jsx/nativeSheet.mjs.map +1 -1
- package/dist/jsx/nativeSheet.native.js +3 -0
- package/dist/jsx/nativeSheet.native.js.map +1 -1
- package/dist/jsx/setupGestureHandler.mjs +9 -6
- package/dist/jsx/setupGestureHandler.mjs.map +1 -1
- package/dist/jsx/setupGestureHandler.native.js +11 -6
- package/dist/jsx/setupGestureHandler.native.js.map +1 -1
- package/dist/jsx/useGestureHandlerPan.mjs +87 -19
- package/dist/jsx/useGestureHandlerPan.mjs.map +1 -1
- package/dist/jsx/useGestureHandlerPan.native.js +91 -18
- package/dist/jsx/useGestureHandlerPan.native.js.map +1 -1
- package/dist/jsx/useKeyboardControllerSheet.mjs +69 -7
- package/dist/jsx/useKeyboardControllerSheet.mjs.map +1 -1
- package/dist/jsx/useSafeAreaInsets.mjs +7 -0
- package/dist/jsx/useSafeAreaInsets.mjs.map +1 -0
- package/dist/jsx/useSafeAreaInsets.native.js +48 -0
- package/dist/jsx/useSafeAreaInsets.native.js.map +1 -0
- package/dist/jsx/useSheetProviderProps.mjs +1 -1
- package/dist/jsx/useSheetProviderProps.mjs.map +1 -1
- package/dist/jsx/useSheetProviderProps.native.js +1 -1
- package/dist/jsx/useSheetProviderProps.native.js.map +1 -1
- package/dist/jsx/useSheetScrollViewGestures.mjs +30 -5
- package/dist/jsx/useSheetScrollViewGestures.mjs.map +1 -1
- package/dist/jsx/useSheetScrollViewGestures.native.js +4 -0
- package/dist/jsx/useSheetScrollViewGestures.native.js.map +1 -1
- package/dist/jsx/webViewport.mjs +43 -0
- package/dist/jsx/webViewport.mjs.map +1 -0
- package/dist/jsx/webViewport.native.js +81 -0
- package/dist/jsx/webViewport.native.js.map +1 -0
- package/next.md +78 -0
- package/package.json +30 -48
- package/setup-gesture-handler/index.cjs +2 -0
- package/setup-gesture-handler/index.js +2 -0
- package/setup-gesture-handler/index.native.cjs +2 -0
- package/setup-gesture-handler/index.native.js +2 -0
- package/src/GestureDetectorWrapper.tsx +0 -3
- package/src/SheetImplementationCustom.tsx +387 -127
- package/src/SheetScrollView.tsx +195 -22
- package/src/createSheet.tsx +20 -7
- package/src/gestureState.ts +22 -2
- package/src/index.ts +4 -0
- package/src/keyboardAvoidance.ts +121 -0
- package/src/nativeSheet.tsx +8 -1
- package/src/setupGestureHandler.ts +14 -5
- package/src/types.tsx +20 -1
- package/src/useGestureHandlerPan.tsx +94 -35
- package/src/useKeyboardControllerSheet.ts +127 -10
- package/src/useSafeAreaInsets.native.ts +29 -0
- package/src/useSafeAreaInsets.ts +19 -0
- package/src/useSheetProviderProps.tsx +12 -1
- package/src/useSheetScrollViewGestures.native.ts +4 -0
- package/src/useSheetScrollViewGestures.ts +52 -5
- package/src/webViewport.ts +100 -0
- package/test/keyboardAvoidance.test.ts +269 -0
- package/tsconfig.json +57 -0
- package/types/GestureDetectorWrapper.d.ts +1 -1
- package/types/GestureDetectorWrapper.d.ts.map +1 -1
- package/types/GestureSheetContext.d.ts +1 -1
- package/types/GestureSheetContext.d.ts.map +1 -1
- package/types/Sheet.d.ts +6 -5
- package/types/Sheet.d.ts.map +1 -1
- package/types/SheetContext.d.ts +5 -2
- package/types/SheetContext.d.ts.map +1 -1
- package/types/SheetController.d.ts +1 -2
- package/types/SheetController.d.ts.map +1 -1
- package/types/SheetImplementationCustom.d.ts +1 -0
- package/types/SheetImplementationCustom.d.ts.map +1 -1
- package/types/SheetScrollView.d.ts.map +1 -1
- package/types/createSheet.d.ts +13 -12
- package/types/createSheet.d.ts.map +1 -1
- package/types/gestureState.d.ts.map +1 -1
- package/types/index.d.ts +1 -0
- package/types/index.d.ts.map +1 -1
- package/types/keyboardAvoidance.d.ts +26 -0
- package/types/keyboardAvoidance.d.ts.map +1 -0
- package/types/nativeSheet.d.ts.map +1 -1
- package/types/setupGestureHandler.d.ts.map +1 -1
- package/types/types.d.ts +10 -1
- package/types/types.d.ts.map +1 -1
- package/types/useGestureHandlerPan.d.ts +4 -1
- package/types/useGestureHandlerPan.d.ts.map +1 -1
- package/types/useKeyboardControllerSheet.d.ts +14 -3
- package/types/useKeyboardControllerSheet.d.ts.map +1 -1
- package/types/useSafeAreaInsets.d.ts +10 -0
- package/types/useSafeAreaInsets.d.ts.map +1 -0
- package/types/useSafeAreaInsets.native.d.ts +20 -0
- package/types/useSafeAreaInsets.native.d.ts.map +1 -0
- package/types/useSheetProviderProps.d.ts +3 -0
- package/types/useSheetProviderProps.d.ts.map +1 -1
- package/types/useSheetScrollViewGestures.d.ts +2 -1
- package/types/useSheetScrollViewGestures.d.ts.map +1 -1
- package/types/useSheetScrollViewGestures.native.d.ts.map +1 -1
- package/types/webViewport.d.ts +36 -0
- package/types/webViewport.d.ts.map +1 -0
- package/LICENSE +0 -42
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isWeb } from '@hanzogui/constants'
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, type RefObject } from 'react'
|
|
2
3
|
import { getGestureHandlerState, isGestureHandlerEnabled } from './gestureState'
|
|
3
|
-
import
|
|
4
|
+
import { getSheetReleasePosition } from './keyboardAvoidance'
|
|
5
|
+
import type { ScrollBridge, SnapPointsMode } from './types'
|
|
4
6
|
|
|
5
7
|
// threshold in pixels for considering sheet "at top" position
|
|
6
8
|
// allows for small measurement variations
|
|
7
9
|
const AT_TOP_THRESHOLD = 5
|
|
10
|
+
const SCROLL_HANDOFF_THRESHOLD = 160
|
|
8
11
|
|
|
9
12
|
interface GesturePanConfig {
|
|
10
13
|
positions: number[]
|
|
@@ -18,6 +21,9 @@ interface GesturePanConfig {
|
|
|
18
21
|
resisted: (val: number, minY: number) => number
|
|
19
22
|
disableDrag?: boolean
|
|
20
23
|
isShowingInnerSheet?: boolean
|
|
24
|
+
dismissOnSnapToBottom?: boolean
|
|
25
|
+
snapPointsMode?: SnapPointsMode
|
|
26
|
+
isKeyboardVisible?: boolean
|
|
21
27
|
// set the animated position directly (for smooth dragging)
|
|
22
28
|
setAnimatedPosition: (val: number) => void
|
|
23
29
|
// ref to scroll gesture for simultaneousWithExternalGesture
|
|
@@ -69,6 +75,16 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
69
75
|
|
|
70
76
|
const gestureHandlerEnabled = isGestureHandlerEnabled()
|
|
71
77
|
const panGestureRef = useRef<any>(null)
|
|
78
|
+
const releaseConfigRef = useRef({
|
|
79
|
+
dismissOnSnapToBottom: config.dismissOnSnapToBottom === true,
|
|
80
|
+
snapPointsMode: config.snapPointsMode ?? 'percent',
|
|
81
|
+
isKeyboardVisible: config.isKeyboardVisible === true,
|
|
82
|
+
})
|
|
83
|
+
releaseConfigRef.current = {
|
|
84
|
+
dismissOnSnapToBottom: config.dismissOnSnapToBottom === true,
|
|
85
|
+
snapPointsMode: config.snapPointsMode ?? 'percent',
|
|
86
|
+
isKeyboardVisible: config.isKeyboardVisible === true,
|
|
87
|
+
}
|
|
72
88
|
|
|
73
89
|
// use refs for values that need to persist across gesture lifecycle
|
|
74
90
|
// (useMemo closure variables get reset when gesture is recreated)
|
|
@@ -86,20 +102,48 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
86
102
|
// causing positions to revert. Frozen positions ensure stable snap calculation.
|
|
87
103
|
frozenPositions: [] as number[],
|
|
88
104
|
frozenMinY: 0,
|
|
105
|
+
frozenIsKeyboardVisible: false,
|
|
89
106
|
// whether pan gesture actually started (vs just a tap in onBegin)
|
|
90
107
|
panStarted: false,
|
|
108
|
+
startedBelowTop: false,
|
|
109
|
+
upwardDragAfterTop: 0,
|
|
110
|
+
scrollUnlockedAfterTop: false,
|
|
111
|
+
keepScrollLockedAfterEnd: false,
|
|
91
112
|
})
|
|
113
|
+
const unlockScrollTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
return () => {
|
|
117
|
+
if (unlockScrollTimer.current) {
|
|
118
|
+
clearTimeout(unlockScrollTimer.current)
|
|
119
|
+
unlockScrollTimer.current = null
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}, [])
|
|
92
123
|
|
|
93
124
|
const onStart = useCallback(() => {
|
|
94
125
|
stopSpring()
|
|
95
126
|
}, [stopSpring])
|
|
96
127
|
|
|
97
128
|
const onEnd = useCallback(
|
|
98
|
-
(closestPoint: number, animationOverride?: any) => {
|
|
129
|
+
(closestPoint: number, animationOverride?: any, keepScrollLocked = false) => {
|
|
130
|
+
if (unlockScrollTimer.current) {
|
|
131
|
+
clearTimeout(unlockScrollTimer.current)
|
|
132
|
+
unlockScrollTimer.current = null
|
|
133
|
+
}
|
|
99
134
|
setIsDragging(false)
|
|
100
135
|
scrollBridge.setParentDragging(false)
|
|
101
|
-
|
|
102
|
-
|
|
136
|
+
scrollBridge.lockScrollAtTop = false
|
|
137
|
+
if (keepScrollLocked) {
|
|
138
|
+
scrollBridge.setScrollEnabled?.(false, 0)
|
|
139
|
+
unlockScrollTimer.current = setTimeout(() => {
|
|
140
|
+
unlockScrollTimer.current = null
|
|
141
|
+
scrollBridge.setScrollEnabled?.(true)
|
|
142
|
+
}, 160)
|
|
143
|
+
} else {
|
|
144
|
+
// re-enable scroll when gesture ends
|
|
145
|
+
scrollBridge.setScrollEnabled?.(true)
|
|
146
|
+
}
|
|
103
147
|
setPosition(closestPoint)
|
|
104
148
|
animateTo(closestPoint, animationOverride)
|
|
105
149
|
},
|
|
@@ -122,7 +166,6 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
122
166
|
|
|
123
167
|
// simultaneousHandlers pattern from react-native-actions-sheet
|
|
124
168
|
// both gestures run simultaneously, we use blockPan to decide who handles
|
|
125
|
-
// console.warn('[RNGH-Pan] CREATING gesture, minY:', minY, 'frameSize:', frameSize)
|
|
126
169
|
const gesture = Gesture.Pan()
|
|
127
170
|
.withRef(panGestureRef)
|
|
128
171
|
// NO manualActivation - let both gestures run via simultaneousHandlers
|
|
@@ -148,7 +191,6 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
148
191
|
const pos = getCurrentPosition()
|
|
149
192
|
const atTop = pos <= minY + AT_TOP_THRESHOLD
|
|
150
193
|
const currentScrollY = scrollBridge.y
|
|
151
|
-
// console.warn('[RNGH-Pan] onBegin', { pos, minY, atTop, currentScrollY })
|
|
152
194
|
gs.startY = pos
|
|
153
195
|
gs.lastPanTranslationY = 0
|
|
154
196
|
gs.accumulatedOffset = 0
|
|
@@ -158,6 +200,16 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
158
200
|
// doesn't change snap targets mid-gesture
|
|
159
201
|
gs.frozenPositions = [...positions]
|
|
160
202
|
gs.frozenMinY = minY
|
|
203
|
+
gs.frozenIsKeyboardVisible = releaseConfigRef.current.isKeyboardVisible
|
|
204
|
+
gs.startedBelowTop = !atTop
|
|
205
|
+
gs.upwardDragAfterTop = 0
|
|
206
|
+
gs.scrollUnlockedAfterTop = false
|
|
207
|
+
gs.keepScrollLockedAfterEnd = false
|
|
208
|
+
scrollBridge.lockScrollAtTop = false
|
|
209
|
+
if (unlockScrollTimer.current) {
|
|
210
|
+
clearTimeout(unlockScrollTimer.current)
|
|
211
|
+
unlockScrollTimer.current = null
|
|
212
|
+
}
|
|
161
213
|
|
|
162
214
|
// if sheet not at top, DISABLE SCROLL immediately and lock to 0
|
|
163
215
|
// this prevents scroll from firing before pan takes over
|
|
@@ -171,7 +223,6 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
171
223
|
gs.panStarted = true
|
|
172
224
|
setIsDragging(true)
|
|
173
225
|
|
|
174
|
-
// console.warn('[RNGH-Pan] onStart', { startY: gs.startY, minY })
|
|
175
226
|
scrollBridge.initialPosition = gs.startY
|
|
176
227
|
onStart()
|
|
177
228
|
})
|
|
@@ -195,8 +246,6 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
195
246
|
const isCurrentlyAtTop = currentPos <= minY + AT_TOP_THRESHOLD
|
|
196
247
|
const nodeIsScrolling = scrollY > 0
|
|
197
248
|
|
|
198
|
-
// console.warn('[RNGH-Pan] onChange', { translationY: translationY.toFixed(1), deltaY: deltaY.toFixed(1), currentPos: currentPos.toFixed(1), minY, isCurrentlyAtTop, isSwipingDown, scrollY, scrollEngaged: gs.scrollEngaged })
|
|
199
|
-
|
|
200
249
|
// decision matrix (from react-native-actions-sheet pattern)
|
|
201
250
|
// each frame, decide who handles the movement based on current state
|
|
202
251
|
//
|
|
@@ -229,7 +278,6 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
229
278
|
} else if (gs.scrollEngaged && hasScrollableContent) {
|
|
230
279
|
// scroll WAS > 0 but now is 0 -> handoff from scroll to pan
|
|
231
280
|
// pan takes over to drag sheet down
|
|
232
|
-
// console.warn('[RNGH-Pan] *** HANDOFF FROM SCROLL TO PAN ***')
|
|
233
281
|
panHandles = true
|
|
234
282
|
} else {
|
|
235
283
|
// scroll never engaged OR content not scrollable, just drag sheet down
|
|
@@ -240,8 +288,18 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
240
288
|
// if there's scrollable content, let scroll handle so user can scroll into content
|
|
241
289
|
// resistance only applies when there's NO scrollable content
|
|
242
290
|
if (hasScrollableContent) {
|
|
243
|
-
|
|
244
|
-
|
|
291
|
+
if (gs.startedBelowTop && !gs.scrollUnlockedAfterTop) {
|
|
292
|
+
gs.upwardDragAfterTop += Math.max(0, -deltaY)
|
|
293
|
+
if (gs.upwardDragAfterTop >= SCROLL_HANDOFF_THRESHOLD) {
|
|
294
|
+
gs.scrollUnlockedAfterTop = true
|
|
295
|
+
panHandles = false
|
|
296
|
+
} else {
|
|
297
|
+
panHandles = true
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
// content is scrollable - let scroll handle (user wants to scroll down into content)
|
|
301
|
+
panHandles = false
|
|
302
|
+
}
|
|
245
303
|
} else {
|
|
246
304
|
// no scrollable content -> pan handles for resistance effect
|
|
247
305
|
panHandles = true
|
|
@@ -249,9 +307,8 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
249
307
|
}
|
|
250
308
|
}
|
|
251
309
|
|
|
252
|
-
// console.warn('[RNGH-Pan] decision', { panHandles, isCurrentlyAtTop, isSwipingDown, nodeIsScrolling, scrollEngaged: gs.scrollEngaged, hasScrollableContent, currentPos: currentPos.toFixed(1), minY })
|
|
253
|
-
|
|
254
310
|
if (panHandles) {
|
|
311
|
+
scrollBridge.lockScrollAtTop = gs.startedBelowTop && !gs.scrollUnlockedAfterTop
|
|
255
312
|
// pan handles - disable scroll and move sheet
|
|
256
313
|
// when swiping down at top after scroll was engaged: lock at current scroll position
|
|
257
314
|
// (handoff from scroll to pan — preserve scroll offset)
|
|
@@ -269,19 +326,22 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
269
326
|
setAnimatedPosition(newPosition)
|
|
270
327
|
scrollBridge.setParentDragging(newPosition > minY)
|
|
271
328
|
} else {
|
|
329
|
+
scrollBridge.lockScrollAtTop = false
|
|
272
330
|
// scroll handles - enable scroll so it can move freely
|
|
273
331
|
scrollBridge.setScrollEnabled?.(true)
|
|
332
|
+
scrollBridge.setParentDragging(false)
|
|
274
333
|
// don't accumulate offset when scroll is handling
|
|
275
334
|
}
|
|
276
335
|
})
|
|
277
336
|
.onEnd((event: { velocityY: number }) => {
|
|
278
337
|
const { velocityY } = event
|
|
279
338
|
const currentPos = gs.startY + gs.accumulatedOffset
|
|
280
|
-
|
|
281
|
-
// console.warn('[RNGH-Pan] onEnd', { velocityY, currentPos, accumulatedOffset: gs.accumulatedOffset, scrollY: scrollBridge.y })
|
|
339
|
+
const keepScrollLockedAfterEnd = gs.startedBelowTop && !gs.scrollUnlockedAfterTop
|
|
282
340
|
|
|
283
341
|
// clear scroll lock
|
|
284
|
-
scrollBridge.
|
|
342
|
+
scrollBridge.lockScrollAtTop = false
|
|
343
|
+
scrollBridge.scrollLockY = keepScrollLockedAfterEnd ? 0 : undefined
|
|
344
|
+
gs.keepScrollLockedAfterEnd = keepScrollLockedAfterEnd
|
|
285
345
|
|
|
286
346
|
// use frozen positions from gesture start — keyboard may have dismissed
|
|
287
347
|
// during drag (input blur), reverting activePositions. Frozen positions
|
|
@@ -294,7 +354,7 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
294
354
|
|
|
295
355
|
// if sheet is at top and scroll is engaged, just stay at top
|
|
296
356
|
if (currentPos <= snapMinY + AT_TOP_THRESHOLD && scrollBridge.y > 0) {
|
|
297
|
-
onEnd(0)
|
|
357
|
+
onEnd(0, undefined, keepScrollLockedAfterEnd)
|
|
298
358
|
return
|
|
299
359
|
}
|
|
300
360
|
|
|
@@ -302,24 +362,24 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
302
362
|
const velocity = velocityY / 1000
|
|
303
363
|
const projectedEnd = currentPos + frameSize * velocity * 0.2
|
|
304
364
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
onEnd(closestPoint)
|
|
365
|
+
const releaseConfig = releaseConfigRef.current
|
|
366
|
+
const closestPoint = getSheetReleasePosition({
|
|
367
|
+
positions: snapPositions,
|
|
368
|
+
projectedEnd,
|
|
369
|
+
currentPosition: currentPos,
|
|
370
|
+
frameSize,
|
|
371
|
+
dismissOnSnapToBottom: releaseConfig.dismissOnSnapToBottom,
|
|
372
|
+
snapPointsMode: releaseConfig.snapPointsMode,
|
|
373
|
+
isKeyboardVisible: gs.frozenIsKeyboardVisible,
|
|
374
|
+
isWeb,
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
onEnd(closestPoint, undefined, keepScrollLockedAfterEnd)
|
|
318
378
|
})
|
|
319
379
|
.onFinalize(() => {
|
|
320
|
-
// console.warn('[RNGH-Pan] onFinalize', { panStarted: gs.panStarted })
|
|
321
380
|
// clear scroll lock on finalize too (safety)
|
|
322
|
-
scrollBridge.
|
|
381
|
+
scrollBridge.lockScrollAtTop = false
|
|
382
|
+
scrollBridge.scrollLockY = gs.keepScrollLockedAfterEnd ? 0 : undefined
|
|
323
383
|
if (gs.panStarted) {
|
|
324
384
|
// real pan gesture — reset isDragging (also un-pauses keyboard handler + flushes)
|
|
325
385
|
setIsDragging(false)
|
|
@@ -336,7 +396,6 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
336
396
|
// if we have a scroll gesture ref, make pan simultaneous with it
|
|
337
397
|
// this allows both gestures to run and we decide in onChange who handles it
|
|
338
398
|
if (scrollGestureRef?.current) {
|
|
339
|
-
// console.warn('[RNGH-Pan] adding simultaneousWithExternalGesture for scroll')
|
|
340
399
|
return gesture.simultaneousWithExternalGesture(scrollGestureRef.current)
|
|
341
400
|
}
|
|
342
401
|
|
|
@@ -1,26 +1,143 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Web
|
|
3
|
-
*
|
|
2
|
+
* Web implementation of the keyboard controller sheet hook.
|
|
3
|
+
*
|
|
4
|
+
* Mobile browsers don't expose a keyboard API, but they do resize the
|
|
5
|
+
* VisualViewport when the soft keyboard opens. We use the viewport shrink
|
|
6
|
+
* (`clientHeight - visualViewport.height`) to detect the keyboard, then feed the
|
|
7
|
+
* bottom layout inset (`clientHeight - (offsetTop + height)`) into
|
|
8
|
+
* SheetImplementationCustom. The bottom inset accounts for iOS Safari panning
|
|
9
|
+
* the visual viewport during focus, so the sheet doesn't over-lift.
|
|
10
|
+
*
|
|
11
|
+
* Without this, a bottom sheet on mobile web stays pinned behind the keyboard:
|
|
12
|
+
* react-native-web's Dimensions tracks the (shrinking) VisualViewport, so any
|
|
13
|
+
* content sized off window dimensions collapses while the sheet's bottom stays
|
|
14
|
+
* occluded. See SheetImplementationCustom's activePositions / keyboardOccludedHeight.
|
|
4
15
|
*/
|
|
5
16
|
|
|
6
|
-
import { useRef } from 'react'
|
|
17
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
18
|
+
import { isWeb } from '@hanzogui/constants'
|
|
7
19
|
import type {
|
|
8
20
|
KeyboardControllerSheetOptions,
|
|
9
21
|
KeyboardControllerSheetResult,
|
|
10
22
|
} from './types'
|
|
11
|
-
|
|
12
|
-
|
|
23
|
+
import {
|
|
24
|
+
getWebKeyboardBottomInset,
|
|
25
|
+
getWebKeyboardResizeHeight,
|
|
26
|
+
isEditableElement,
|
|
27
|
+
MIN_KEYBOARD_HEIGHT,
|
|
28
|
+
} from './webViewport'
|
|
13
29
|
|
|
14
30
|
export function useKeyboardControllerSheet(
|
|
15
|
-
|
|
31
|
+
options: KeyboardControllerSheetOptions
|
|
16
32
|
): KeyboardControllerSheetResult {
|
|
33
|
+
const { enabled } = options
|
|
34
|
+
|
|
35
|
+
// initialize from the CURRENT viewport, not blindly from 0/false. when the
|
|
36
|
+
// sheet opens with the soft keyboard already up (e.g. it was raised by a field
|
|
37
|
+
// on the page behind, or by an autofocus whose keyboard wins the race against
|
|
38
|
+
// the first layout), a false initial value makes the sheet's very first render
|
|
39
|
+
// believe the keyboard is down — it captures a keyboard-free baseline from a
|
|
40
|
+
// keyboard-shrunk layout and the anchor/seed machinery has to recover. reading
|
|
41
|
+
// the viewport synchronously here removes that first-render race: the sheet
|
|
42
|
+
// knows the keyboard is up on render 1 and takes the seed path deterministically.
|
|
43
|
+
const [keyboardHeight, setKeyboardHeight] = useState(() =>
|
|
44
|
+
isWeb && enabled && typeof window !== 'undefined' && window.visualViewport
|
|
45
|
+
? (() => {
|
|
46
|
+
const resizeHeight = getWebKeyboardResizeHeight()
|
|
47
|
+
return resizeHeight >= MIN_KEYBOARD_HEIGHT &&
|
|
48
|
+
isEditableElement(document.activeElement)
|
|
49
|
+
? getWebKeyboardBottomInset()
|
|
50
|
+
: 0
|
|
51
|
+
})()
|
|
52
|
+
: 0
|
|
53
|
+
)
|
|
54
|
+
const [isKeyboardVisible, setIsKeyboardVisible] = useState(() => keyboardHeight > 0)
|
|
55
|
+
|
|
56
|
+
// action-sheet pattern: pause keyboard hide events during drag so the sheet
|
|
57
|
+
// position doesn't revert mid-gesture when a TextInput blurs.
|
|
17
58
|
const pauseKeyboardHandler = useRef(false)
|
|
59
|
+
const pendingHide = useRef(false)
|
|
60
|
+
// live mirror of isKeyboardVisible so the listener can read it without
|
|
61
|
+
// re-subscribing on every toggle.
|
|
62
|
+
const isVisibleRef = useRef(false)
|
|
63
|
+
isVisibleRef.current = isKeyboardVisible
|
|
64
|
+
|
|
65
|
+
const dismissKeyboard = useCallback(() => {
|
|
66
|
+
if (typeof document === 'undefined') return
|
|
67
|
+
const active = document.activeElement as HTMLElement | null
|
|
68
|
+
if (isEditableElement(active)) {
|
|
69
|
+
active?.blur?.()
|
|
70
|
+
}
|
|
71
|
+
}, [])
|
|
72
|
+
|
|
73
|
+
const flushPendingHide = useCallback(() => {
|
|
74
|
+
if (pendingHide.current) {
|
|
75
|
+
pendingHide.current = false
|
|
76
|
+
setIsKeyboardVisible(false)
|
|
77
|
+
setKeyboardHeight(0)
|
|
78
|
+
}
|
|
79
|
+
}, [])
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!isWeb || !enabled) return
|
|
83
|
+
if (typeof window === 'undefined') return
|
|
84
|
+
const vv = window.visualViewport
|
|
85
|
+
if (!vv) return
|
|
86
|
+
|
|
87
|
+
const update = () => {
|
|
88
|
+
const resizeHeight = getWebKeyboardResizeHeight()
|
|
89
|
+
const height = getWebKeyboardBottomInset()
|
|
90
|
+
const tall = resizeHeight >= MIN_KEYBOARD_HEIGHT
|
|
91
|
+
// require an editable element focused to *show* — this rules out URL-bar
|
|
92
|
+
// collapse and other viewport changes that aren't a keyboard. but stay
|
|
93
|
+
// visible while the viewport remains shrunk even if focus momentarily
|
|
94
|
+
// moves to a non-editable element (e.g. tapping between two inputs fires
|
|
95
|
+
// focusout→focusin), so the sheet doesn't flicker mid-transition.
|
|
96
|
+
const editableFocused = isEditableElement(document.activeElement)
|
|
97
|
+
const visible = tall && (editableFocused || isVisibleRef.current)
|
|
98
|
+
|
|
99
|
+
if (!visible) {
|
|
100
|
+
// suppress hide during an active drag so positions stay frozen; the
|
|
101
|
+
// drag-end flush reconciles the real state.
|
|
102
|
+
if (pauseKeyboardHandler.current && isVisibleRef.current) {
|
|
103
|
+
pendingHide.current = true
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
setIsKeyboardVisible(false)
|
|
107
|
+
setKeyboardHeight(0)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
pendingHide.current = false
|
|
112
|
+
setIsKeyboardVisible(true)
|
|
113
|
+
setKeyboardHeight((prev) => (prev === height ? prev : height))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// resize tracks keyboard open/close. scroll tracks iOS Safari's focus pan:
|
|
117
|
+
// visualViewport.offsetTop can change after the height has settled, and the
|
|
118
|
+
// sheet cap must move in that same layout coordinate space.
|
|
119
|
+
vv.addEventListener('resize', update)
|
|
120
|
+
vv.addEventListener('scroll', update)
|
|
121
|
+
// focus changes flip editable state without necessarily resizing the viewport
|
|
122
|
+
window.addEventListener('focusin', update)
|
|
123
|
+
window.addEventListener('focusout', update)
|
|
124
|
+
|
|
125
|
+
update()
|
|
126
|
+
|
|
127
|
+
return () => {
|
|
128
|
+
vv.removeEventListener('resize', update)
|
|
129
|
+
vv.removeEventListener('scroll', update)
|
|
130
|
+
window.removeEventListener('focusin', update)
|
|
131
|
+
window.removeEventListener('focusout', update)
|
|
132
|
+
}
|
|
133
|
+
}, [enabled])
|
|
134
|
+
|
|
18
135
|
return {
|
|
19
136
|
keyboardControllerEnabled: false,
|
|
20
|
-
keyboardHeight
|
|
21
|
-
isKeyboardVisible
|
|
22
|
-
dismissKeyboard
|
|
137
|
+
keyboardHeight,
|
|
138
|
+
isKeyboardVisible,
|
|
139
|
+
dismissKeyboard,
|
|
23
140
|
pauseKeyboardHandler,
|
|
24
|
-
flushPendingHide
|
|
141
|
+
flushPendingHide,
|
|
25
142
|
}
|
|
26
143
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { SafeAreaInsetsContext as RNSafeAreaInsetsContext } from 'react-native-safe-area-context'
|
|
3
|
+
|
|
4
|
+
import type { SafeAreaInsets } from './useSafeAreaInsets'
|
|
5
|
+
|
|
6
|
+
// the real react-native-safe-area-context insets context, re-typed to the local
|
|
7
|
+
// shape so consumers don't depend on the package's types. exported so the Sheet
|
|
8
|
+
// can re-propagate it across the portal.
|
|
9
|
+
export const SafeAreaInsetsContext =
|
|
10
|
+
RNSafeAreaInsetsContext as unknown as React.Context<SafeAreaInsets | null>
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Live safe-area insets (notch / status bar / home indicator) read from the
|
|
14
|
+
* context the app's SafeAreaProvider provides.
|
|
15
|
+
*
|
|
16
|
+
* Read this in a component BODY that renders inside the provider. The Sheet does
|
|
17
|
+
* exactly that — only its modal CONTENT is teleported out through the portal, so
|
|
18
|
+
* the body still sees the real insets. This is the one read that works in real
|
|
19
|
+
* native bundles:
|
|
20
|
+
* - `getSafeArea()` (the @hanzogui/native abstraction) is frequently NOT enabled
|
|
21
|
+
* against the instance a component reads, so it returns 0.
|
|
22
|
+
* - a dynamic `require('react-native-safe-area-context')` throws "Unknown named
|
|
23
|
+
* module" in metro/rolldown bundles where the package isn't in the importing
|
|
24
|
+
* module's graph.
|
|
25
|
+
* A static import + context read avoids both.
|
|
26
|
+
*/
|
|
27
|
+
export function useSafeAreaInsets(): SafeAreaInsets | null {
|
|
28
|
+
return React.useContext(SafeAreaInsetsContext)
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
export interface SafeAreaInsets {
|
|
4
|
+
top: number
|
|
5
|
+
right: number
|
|
6
|
+
bottom: number
|
|
7
|
+
left: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// web uses CSS env(safe-area-inset-*) for safe areas, so there is no native
|
|
11
|
+
// safe-area context to read here — the keyboard-avoidance path uses the visual
|
|
12
|
+
// viewport offset instead. this stub keeps the import resolvable on web (where
|
|
13
|
+
// react-native-safe-area-context isn't a dependency) and is the canonical type
|
|
14
|
+
// source for both platform variants.
|
|
15
|
+
export const SafeAreaInsetsContext = React.createContext<SafeAreaInsets | null>(null)
|
|
16
|
+
|
|
17
|
+
export function useSafeAreaInsets(): SafeAreaInsets | null {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
@@ -8,6 +8,17 @@ import type { ScrollBridge, SheetProps } from './types'
|
|
|
8
8
|
import type { SheetOpenState } from './useSheetOpenState'
|
|
9
9
|
|
|
10
10
|
export type SheetContextValue = ReturnType<typeof useSheetProviderProps> & {
|
|
11
|
+
keyboardOccludedHeight: number
|
|
12
|
+
// whether the soft keyboard is currently open. distinct from
|
|
13
|
+
// keyboardOccludedHeight (which is 0 once the sheet fits above the keyboard):
|
|
14
|
+
// the fit-mode height freeze must persist for the whole time the keyboard is
|
|
15
|
+
// open, not just while occluded, or it releases and the sheet collapses.
|
|
16
|
+
isKeyboardVisible: boolean
|
|
17
|
+
// the sheet's pre-keyboard frame height (web). SheetScrollView pins its height
|
|
18
|
+
// to this while the keyboard is open so the frame translates at full size
|
|
19
|
+
// instead of collapsing to a consumer maxHeight that shrank with the viewport.
|
|
20
|
+
// 0 when not applicable.
|
|
21
|
+
keyboardStableFrameHeight: number
|
|
11
22
|
setHasScrollView: (val: boolean) => void
|
|
12
23
|
}
|
|
13
24
|
|
|
@@ -133,7 +144,7 @@ export function useSheetProviderProps(
|
|
|
133
144
|
throw new Error(
|
|
134
145
|
process.env.NODE_ENV === 'production'
|
|
135
146
|
? `❌ 008`
|
|
136
|
-
: 'Must set animations in
|
|
147
|
+
: 'Must set animations in hanzogui.config.ts'
|
|
137
148
|
)
|
|
138
149
|
}
|
|
139
150
|
|
|
@@ -131,6 +131,10 @@ export function useSheetScrollViewGestures({
|
|
|
131
131
|
} else {
|
|
132
132
|
panHandles = !hasScrollableContent
|
|
133
133
|
if (!panHandles) {
|
|
134
|
+
s.handoffOccurred = false
|
|
135
|
+
s.handoffDragOffset = 0
|
|
136
|
+
s.scrollEngaged = currentScrollY > 0
|
|
137
|
+
s.prevScrollY = currentScrollY
|
|
134
138
|
s.isScrolling = true
|
|
135
139
|
scrollBridge.scrollLock = true
|
|
136
140
|
setScrollEnabled(true)
|
|
@@ -8,6 +8,7 @@ interface GestureState {
|
|
|
8
8
|
startY: number
|
|
9
9
|
lastY: number
|
|
10
10
|
owner: GestureOwner
|
|
11
|
+
hadPanOwner: boolean
|
|
11
12
|
panDragOffset: number
|
|
12
13
|
dys: number[]
|
|
13
14
|
scrollYAtGestureStart: number
|
|
@@ -19,21 +20,29 @@ interface UseSheetScrollViewGesturesProps {
|
|
|
19
20
|
hasScrollableContent: boolean
|
|
20
21
|
scrollEnabled: boolean
|
|
21
22
|
setScrollEnabled: (enabled: boolean, lockTo?: number) => void
|
|
23
|
+
onManualScroll?: (node: HTMLElement, y: number) => void
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export function useSheetScrollViewGestures({
|
|
25
27
|
scrollRef,
|
|
26
28
|
scrollBridge,
|
|
27
29
|
hasScrollableContent,
|
|
30
|
+
onManualScroll,
|
|
28
31
|
}: UseSheetScrollViewGesturesProps) {
|
|
29
32
|
const state = useRef<GestureState>({
|
|
30
33
|
startY: 0,
|
|
31
34
|
lastY: 0,
|
|
32
35
|
owner: 'none',
|
|
36
|
+
hadPanOwner: false,
|
|
33
37
|
panDragOffset: 0,
|
|
34
38
|
dys: [],
|
|
35
39
|
scrollYAtGestureStart: 0,
|
|
36
40
|
})
|
|
41
|
+
const onManualScrollRef = useRef(onManualScroll)
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
onManualScrollRef.current = onManualScroll
|
|
45
|
+
}, [onManualScroll])
|
|
37
46
|
|
|
38
47
|
useEffect(() => {
|
|
39
48
|
if (!scrollRef.current) return
|
|
@@ -59,6 +68,13 @@ export function useSheetScrollViewGestures({
|
|
|
59
68
|
node.style.overflowY = originalOverflow
|
|
60
69
|
}
|
|
61
70
|
|
|
71
|
+
const setScrollTop = (nextScrollY: number) => {
|
|
72
|
+
if (node.scrollTop === nextScrollY) return
|
|
73
|
+
|
|
74
|
+
node.scrollTop = nextScrollY
|
|
75
|
+
onManualScrollRef.current?.(node, nextScrollY)
|
|
76
|
+
}
|
|
77
|
+
|
|
62
78
|
const handleTouchStart = (e: TouchEvent) => {
|
|
63
79
|
const touch = e.touches[0]
|
|
64
80
|
if (!touch) return
|
|
@@ -69,12 +85,17 @@ export function useSheetScrollViewGestures({
|
|
|
69
85
|
startY: touch.pageY,
|
|
70
86
|
lastY: touch.pageY,
|
|
71
87
|
owner: 'none',
|
|
88
|
+
hadPanOwner: false,
|
|
72
89
|
panDragOffset: 0,
|
|
73
90
|
dys: [],
|
|
74
91
|
scrollYAtGestureStart: currentScrollY,
|
|
75
92
|
}
|
|
76
93
|
|
|
77
94
|
scrollBridge.scrollStartY = touch.pageY
|
|
95
|
+
// claim this touch for the scroll-view gesture hook so the PanResponder
|
|
96
|
+
// (which also negotiates this touch via RNW's responder system) defers
|
|
97
|
+
// and doesn't double-drive the sheet position. cleared on touchend.
|
|
98
|
+
scrollBridge.scrollNodeTouched = true
|
|
78
99
|
}
|
|
79
100
|
|
|
80
101
|
const handleTouchMove = (e: TouchEvent) => {
|
|
@@ -90,7 +111,8 @@ export function useSheetScrollViewGestures({
|
|
|
90
111
|
|
|
91
112
|
const isDraggingDown = dy > 0
|
|
92
113
|
const currentScrollY = node.scrollTop
|
|
93
|
-
const isPaneAtTop =
|
|
114
|
+
const isPaneAtTop =
|
|
115
|
+
scrollBridge.isAtTop ?? scrollBridge.paneY <= scrollBridge.paneMinY + 5
|
|
94
116
|
|
|
95
117
|
scrollBridge.y = currentScrollY
|
|
96
118
|
|
|
@@ -113,14 +135,23 @@ export function useSheetScrollViewGestures({
|
|
|
113
135
|
// handle transitions
|
|
114
136
|
if (newOwner !== s.owner) {
|
|
115
137
|
if (newOwner === 'pan') {
|
|
138
|
+
s.hadPanOwner = true
|
|
116
139
|
s.panDragOffset = 0
|
|
117
140
|
s.dys = []
|
|
141
|
+
// re-baseline the pan origin to the sheet's CURRENT position so the
|
|
142
|
+
// offset (reset to 0 here) maps to where it actually is — required for
|
|
143
|
+
// a correct scroll→pan handoff now that the PanResponder defers to us.
|
|
144
|
+
scrollBridge.startPanDrag?.()
|
|
118
145
|
scrollBridge.setParentDragging(true)
|
|
119
146
|
disableScroll()
|
|
120
147
|
} else {
|
|
121
148
|
scrollBridge.setParentDragging(false)
|
|
122
149
|
scrollBridge.scrollLock = false
|
|
123
|
-
|
|
150
|
+
if (s.hadPanOwner) {
|
|
151
|
+
disableScroll()
|
|
152
|
+
} else {
|
|
153
|
+
enableScroll()
|
|
154
|
+
}
|
|
124
155
|
}
|
|
125
156
|
s.owner = newOwner
|
|
126
157
|
}
|
|
@@ -133,14 +164,21 @@ export function useSheetScrollViewGestures({
|
|
|
133
164
|
|
|
134
165
|
s.dys.push(dy)
|
|
135
166
|
if (s.dys.length > 100) s.dys = s.dys.slice(-10)
|
|
136
|
-
} else if (s.owner === 'scroll') {
|
|
137
|
-
//
|
|
167
|
+
} else if (s.owner === 'scroll' && (!e.isTrusted || s.hadPanOwner)) {
|
|
168
|
+
// synthetic events don't trigger native overflow scroll, so tests need us
|
|
169
|
+
// to move scrollTop directly. real mixed gestures need the same direct
|
|
170
|
+
// path after pan ownership: ios safari may not resume native scrolling
|
|
171
|
+
// mid-touch after an earlier prevented touchmove. pure real scrolls still
|
|
172
|
+
// use native scrolling; mixed pan↔scroll gestures keep overflow hidden
|
|
173
|
+
// and are driven here until touchend.
|
|
174
|
+
if (e.cancelable) e.preventDefault()
|
|
138
175
|
const scrollDelta = -dy
|
|
139
176
|
const maxScrollY = node.scrollHeight - node.clientHeight
|
|
140
177
|
const newScrollY = Math.max(0, Math.min(maxScrollY, currentScrollY + scrollDelta))
|
|
141
178
|
if (newScrollY !== currentScrollY) {
|
|
142
|
-
|
|
179
|
+
setScrollTop(newScrollY)
|
|
143
180
|
scrollBridge.y = newScrollY
|
|
181
|
+
if (newScrollY > 0) scrollBridge.scrollStartY = -1
|
|
144
182
|
}
|
|
145
183
|
}
|
|
146
184
|
}
|
|
@@ -159,12 +197,21 @@ export function useSheetScrollViewGestures({
|
|
|
159
197
|
}
|
|
160
198
|
|
|
161
199
|
scrollBridge.release({ dragAt: s.panDragOffset, vy })
|
|
200
|
+
} else if (s.owner === 'scroll') {
|
|
201
|
+
// gesture ended while scrolling. a pan→scroll handoff only happens once
|
|
202
|
+
// the pane reached the top, so commit the top snap (index 0) and clear
|
|
203
|
+
// the dragging state HERE, on touchend — never mid-gesture (that would
|
|
204
|
+
// fight the live gesture). this replaces the PanResponder's release,
|
|
205
|
+
// which used to fire on touchend before it deferred to this hook.
|
|
206
|
+
scrollBridge.snapToPosition?.(0)
|
|
162
207
|
}
|
|
163
208
|
|
|
164
209
|
enableScroll()
|
|
165
210
|
s.owner = 'none'
|
|
211
|
+
s.hadPanOwner = false
|
|
166
212
|
s.panDragOffset = 0
|
|
167
213
|
s.dys = []
|
|
214
|
+
scrollBridge.scrollNodeTouched = false
|
|
168
215
|
}
|
|
169
216
|
|
|
170
217
|
node.addEventListener('touchstart', handleTouchStart, {
|