@tamagui/sheet 2.0.0-1769467800680 → 2.0.0-1769536410877
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/SheetImplementationCustom.cjs +66 -31
- package/dist/cjs/SheetImplementationCustom.js +70 -40
- package/dist/cjs/SheetImplementationCustom.js.map +1 -1
- package/dist/cjs/SheetImplementationCustom.native.js +73 -33
- package/dist/cjs/SheetImplementationCustom.native.js.map +1 -1
- package/dist/cjs/SheetScrollView.cjs +2 -0
- package/dist/cjs/SheetScrollView.js +2 -0
- package/dist/cjs/SheetScrollView.js.map +1 -1
- package/dist/cjs/SheetScrollView.native.js +2 -0
- package/dist/cjs/SheetScrollView.native.js.map +1 -1
- package/dist/cjs/useGestureHandlerPan.cjs +21 -11
- package/dist/cjs/useGestureHandlerPan.js +20 -11
- package/dist/cjs/useGestureHandlerPan.js.map +1 -1
- package/dist/cjs/useGestureHandlerPan.native.js +21 -11
- package/dist/cjs/useGestureHandlerPan.native.js.map +1 -1
- package/dist/cjs/useKeyboardControllerSheet.cjs +38 -0
- package/dist/cjs/useKeyboardControllerSheet.js +34 -0
- package/dist/cjs/useKeyboardControllerSheet.js.map +6 -0
- package/dist/cjs/useKeyboardControllerSheet.native.js +114 -0
- package/dist/cjs/useKeyboardControllerSheet.native.js.map +1 -0
- package/dist/esm/SheetImplementationCustom.js +73 -41
- package/dist/esm/SheetImplementationCustom.js.map +1 -1
- package/dist/esm/SheetImplementationCustom.mjs +68 -33
- package/dist/esm/SheetImplementationCustom.mjs.map +1 -1
- package/dist/esm/SheetImplementationCustom.native.js +75 -35
- package/dist/esm/SheetImplementationCustom.native.js.map +1 -1
- package/dist/esm/SheetScrollView.js +2 -0
- package/dist/esm/SheetScrollView.js.map +1 -1
- package/dist/esm/SheetScrollView.mjs +2 -0
- package/dist/esm/SheetScrollView.mjs.map +1 -1
- package/dist/esm/SheetScrollView.native.js +2 -0
- package/dist/esm/SheetScrollView.native.js.map +1 -1
- package/dist/esm/useGestureHandlerPan.js +20 -11
- package/dist/esm/useGestureHandlerPan.js.map +1 -1
- package/dist/esm/useGestureHandlerPan.mjs +21 -11
- package/dist/esm/useGestureHandlerPan.mjs.map +1 -1
- package/dist/esm/useGestureHandlerPan.native.js +21 -11
- package/dist/esm/useGestureHandlerPan.native.js.map +1 -1
- package/dist/esm/useKeyboardControllerSheet.js +18 -0
- package/dist/esm/useKeyboardControllerSheet.js.map +6 -0
- package/dist/esm/useKeyboardControllerSheet.mjs +15 -0
- package/dist/esm/useKeyboardControllerSheet.mjs.map +1 -0
- package/dist/esm/useKeyboardControllerSheet.native.js +88 -0
- package/dist/esm/useKeyboardControllerSheet.native.js.map +1 -0
- package/dist/jsx/SheetImplementationCustom.js +73 -41
- package/dist/jsx/SheetImplementationCustom.js.map +1 -1
- package/dist/jsx/SheetImplementationCustom.mjs +68 -33
- package/dist/jsx/SheetImplementationCustom.mjs.map +1 -1
- package/dist/jsx/SheetImplementationCustom.native.js +73 -33
- package/dist/jsx/SheetImplementationCustom.native.js.map +1 -1
- package/dist/jsx/SheetScrollView.js +2 -0
- package/dist/jsx/SheetScrollView.js.map +1 -1
- package/dist/jsx/SheetScrollView.mjs +2 -0
- package/dist/jsx/SheetScrollView.mjs.map +1 -1
- package/dist/jsx/SheetScrollView.native.js +2 -0
- package/dist/jsx/SheetScrollView.native.js.map +1 -1
- package/dist/jsx/useGestureHandlerPan.js +20 -11
- package/dist/jsx/useGestureHandlerPan.js.map +1 -1
- package/dist/jsx/useGestureHandlerPan.mjs +21 -11
- package/dist/jsx/useGestureHandlerPan.mjs.map +1 -1
- package/dist/jsx/useGestureHandlerPan.native.js +21 -11
- package/dist/jsx/useGestureHandlerPan.native.js.map +1 -1
- package/dist/jsx/useKeyboardControllerSheet.js +18 -0
- package/dist/jsx/useKeyboardControllerSheet.js.map +6 -0
- package/dist/jsx/useKeyboardControllerSheet.mjs +15 -0
- package/dist/jsx/useKeyboardControllerSheet.mjs.map +1 -0
- package/dist/jsx/useKeyboardControllerSheet.native.js +114 -0
- package/dist/jsx/useKeyboardControllerSheet.native.js.map +1 -0
- package/package.json +20 -20
- package/src/SheetImplementationCustom.tsx +138 -46
- package/src/SheetScrollView.tsx +2 -0
- package/src/types.tsx +47 -0
- package/src/useGestureHandlerPan.tsx +57 -10
- package/src/useKeyboardControllerSheet.native.ts +136 -0
- package/src/useKeyboardControllerSheet.ts +26 -0
- package/types/SheetImplementationCustom.d.ts.map +1 -1
- package/types/SheetScrollView.d.ts.map +1 -1
- package/types/types.d.ts +38 -0
- package/types/types.d.ts.map +1 -1
- package/types/useGestureHandlerPan.d.ts +2 -1
- package/types/useGestureHandlerPan.d.ts.map +1 -1
- package/types/useKeyboardControllerSheet.d.ts +7 -0
- package/types/useKeyboardControllerSheet.d.ts.map +1 -0
- package/types/useKeyboardControllerSheet.native.d.ts +13 -0
- package/types/useKeyboardControllerSheet.native.d.ts.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tamagui/sheet",
|
|
3
|
-
"version": "2.0.0-
|
|
3
|
+
"version": "2.0.0-1769536410877",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"*.css"
|
|
@@ -72,24 +72,24 @@
|
|
|
72
72
|
}
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@tamagui/adapt": "2.0.0-
|
|
76
|
-
"@tamagui/animate-presence": "2.0.0-
|
|
77
|
-
"@tamagui/animations-react-native": "2.0.0-
|
|
78
|
-
"@tamagui/compose-refs": "2.0.0-
|
|
79
|
-
"@tamagui/constants": "2.0.0-
|
|
80
|
-
"@tamagui/core": "2.0.0-
|
|
81
|
-
"@tamagui/create-context": "2.0.0-
|
|
82
|
-
"@tamagui/helpers": "2.0.0-
|
|
83
|
-
"@tamagui/native": "2.0.0-
|
|
84
|
-
"@tamagui/portal": "2.0.0-
|
|
85
|
-
"@tamagui/remove-scroll": "2.0.0-
|
|
86
|
-
"@tamagui/scroll-view": "2.0.0-
|
|
87
|
-
"@tamagui/stacks": "2.0.0-
|
|
88
|
-
"@tamagui/use-constant": "2.0.0-
|
|
89
|
-
"@tamagui/use-controllable-state": "2.0.0-
|
|
90
|
-
"@tamagui/use-did-finish-ssr": "2.0.0-
|
|
91
|
-
"@tamagui/use-keyboard-visible": "2.0.0-
|
|
92
|
-
"@tamagui/z-index-stack": "2.0.0-
|
|
75
|
+
"@tamagui/adapt": "2.0.0-1769536410877",
|
|
76
|
+
"@tamagui/animate-presence": "2.0.0-1769536410877",
|
|
77
|
+
"@tamagui/animations-react-native": "2.0.0-1769536410877",
|
|
78
|
+
"@tamagui/compose-refs": "2.0.0-1769536410877",
|
|
79
|
+
"@tamagui/constants": "2.0.0-1769536410877",
|
|
80
|
+
"@tamagui/core": "2.0.0-1769536410877",
|
|
81
|
+
"@tamagui/create-context": "2.0.0-1769536410877",
|
|
82
|
+
"@tamagui/helpers": "2.0.0-1769536410877",
|
|
83
|
+
"@tamagui/native": "2.0.0-1769536410877",
|
|
84
|
+
"@tamagui/portal": "2.0.0-1769536410877",
|
|
85
|
+
"@tamagui/remove-scroll": "2.0.0-1769536410877",
|
|
86
|
+
"@tamagui/scroll-view": "2.0.0-1769536410877",
|
|
87
|
+
"@tamagui/stacks": "2.0.0-1769536410877",
|
|
88
|
+
"@tamagui/use-constant": "2.0.0-1769536410877",
|
|
89
|
+
"@tamagui/use-controllable-state": "2.0.0-1769536410877",
|
|
90
|
+
"@tamagui/use-did-finish-ssr": "2.0.0-1769536410877",
|
|
91
|
+
"@tamagui/use-keyboard-visible": "2.0.0-1769536410877",
|
|
92
|
+
"@tamagui/z-index-stack": "2.0.0-1769536410877"
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
95
|
"react": "*",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
}
|
|
103
103
|
},
|
|
104
104
|
"devDependencies": {
|
|
105
|
-
"@tamagui/build": "2.0.0-
|
|
105
|
+
"@tamagui/build": "2.0.0-1769536410877",
|
|
106
106
|
"react": "*",
|
|
107
107
|
"react-native": "0.81.5",
|
|
108
108
|
"react-native-gesture-handler": "~2.28.0"
|
|
@@ -19,19 +19,41 @@ import type {
|
|
|
19
19
|
LayoutChangeEvent,
|
|
20
20
|
PanResponderGestureState,
|
|
21
21
|
} from 'react-native'
|
|
22
|
-
import { Dimensions,
|
|
22
|
+
import { Dimensions, PanResponder, View } from 'react-native'
|
|
23
23
|
import { ParentSheetContext, SheetInsideSheetContext } from './contexts'
|
|
24
24
|
import { GestureDetectorWrapper } from './GestureDetectorWrapper'
|
|
25
25
|
import { GestureSheetProvider } from './GestureSheetContext'
|
|
26
|
+
import { getSafeArea } from '@tamagui/native'
|
|
26
27
|
import { resisted } from './helpers'
|
|
27
28
|
import { SheetProvider } from './SheetContext'
|
|
28
29
|
import type { SheetProps, SnapPointsMode } from './types'
|
|
29
30
|
import { useGestureHandlerPan } from './useGestureHandlerPan'
|
|
31
|
+
import { useKeyboardControllerSheet } from './useKeyboardControllerSheet'
|
|
30
32
|
import { useSheetOpenState } from './useSheetOpenState'
|
|
31
33
|
import { useSheetProviderProps } from './useSheetProviderProps'
|
|
32
34
|
|
|
33
35
|
const hiddenSize = 10_000.1
|
|
34
36
|
|
|
37
|
+
// safe area top inset, cached per-session (device-constant value)
|
|
38
|
+
let _cachedSafeAreaTop: number | undefined
|
|
39
|
+
function getSafeAreaTopInset(): number {
|
|
40
|
+
if (_cachedSafeAreaTop !== undefined) return _cachedSafeAreaTop
|
|
41
|
+
// try tamagui native safe area state first
|
|
42
|
+
const sa = getSafeArea()
|
|
43
|
+
if (sa.isEnabled) {
|
|
44
|
+
_cachedSafeAreaTop = sa.getInsets().top
|
|
45
|
+
return _cachedSafeAreaTop
|
|
46
|
+
}
|
|
47
|
+
// fallback: react-native-safe-area-context initialWindowMetrics (no provider needed)
|
|
48
|
+
try {
|
|
49
|
+
const sac = require('react-native-safe-area-context')
|
|
50
|
+
_cachedSafeAreaTop = sac.initialWindowMetrics?.insets?.top ?? 0
|
|
51
|
+
} catch {
|
|
52
|
+
_cachedSafeAreaTop = 0
|
|
53
|
+
}
|
|
54
|
+
return _cachedSafeAreaTop ?? 0
|
|
55
|
+
}
|
|
56
|
+
|
|
35
57
|
let sheetHiddenStyleSheet: HTMLStyleElement | null = null
|
|
36
58
|
|
|
37
59
|
// on web we are always relative to window, on to screen
|
|
@@ -141,6 +163,74 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
141
163
|
[screenSize, effectiveFrameSize, snapPoints, snapPointsMode]
|
|
142
164
|
)
|
|
143
165
|
|
|
166
|
+
// keyboard state tracking — just tracks height/visibility, no position animation.
|
|
167
|
+
// Position animation is handled via keyboard-adjusted positions below,
|
|
168
|
+
// matching the react-native-actions-sheet pattern.
|
|
169
|
+
const {
|
|
170
|
+
keyboardHeight,
|
|
171
|
+
isKeyboardVisible,
|
|
172
|
+
dismissKeyboard,
|
|
173
|
+
pauseKeyboardHandler,
|
|
174
|
+
flushPendingHide,
|
|
175
|
+
} = useKeyboardControllerSheet({
|
|
176
|
+
enabled: !isWeb && Boolean(moveOnKeyboardChange),
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
const [isDragging, setIsDragging_] = React.useState(false)
|
|
180
|
+
|
|
181
|
+
// synchronous dragging ref — set BEFORE async state commits.
|
|
182
|
+
// RNGH onBegin fires before keyboard hide event reaches JS,
|
|
183
|
+
// so the ref is true by the time activePositions memo re-evaluates.
|
|
184
|
+
// Also controls pauseKeyboardHandler to freeze keyboard state during drag.
|
|
185
|
+
const isDraggingRef = React.useRef(false)
|
|
186
|
+
const setIsDragging = React.useCallback(
|
|
187
|
+
(val: boolean) => {
|
|
188
|
+
isDraggingRef.current = val
|
|
189
|
+
pauseKeyboardHandler.current = val
|
|
190
|
+
setIsDragging_(val)
|
|
191
|
+
// when drag ends, flush any keyboard hide that was suppressed during drag
|
|
192
|
+
// so isKeyboardVisible/keyboardHeight reflect actual state
|
|
193
|
+
if (!val) {
|
|
194
|
+
flushPendingHide()
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
[pauseKeyboardHandler, flushPendingHide]
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
// keyboard-adjusted positions: shift snap points up by keyboard height
|
|
201
|
+
// when keyboard is visible. This drives both gesture snap calculation
|
|
202
|
+
// and animation targets — keyboard never dismissed during drag.
|
|
203
|
+
// Capped at safe area top inset so the sheet never goes above the notch/status bar
|
|
204
|
+
// (matching the react-native-actions-sheet pattern).
|
|
205
|
+
//
|
|
206
|
+
// IMPORTANT: frozen during drag to prevent gesture handler recreation.
|
|
207
|
+
// When user drags, TextInput may blur → keyboard dismisses → positions would revert,
|
|
208
|
+
// causing the gesture useMemo to recreate and cancel the active drag.
|
|
209
|
+
// The post-drag reconciliation effect handles animating to correct position after drag ends.
|
|
210
|
+
const activePositionsRef = React.useRef(positions)
|
|
211
|
+
const activePositions = React.useMemo(() => {
|
|
212
|
+
// during drag, return frozen positions to prevent gesture handler recreation.
|
|
213
|
+
// check both state (for re-render trigger) and ref (for synchronous check
|
|
214
|
+
// when keyboard hide event fires before isDragging state commits)
|
|
215
|
+
if (isDragging || isDraggingRef.current) return activePositionsRef.current
|
|
216
|
+
|
|
217
|
+
let result: number[]
|
|
218
|
+
if (!isKeyboardVisible || keyboardHeight <= 0) {
|
|
219
|
+
result = positions
|
|
220
|
+
} else {
|
|
221
|
+
const safeAreaTop = isWeb ? 0 : getSafeAreaTopInset()
|
|
222
|
+
result = positions.map((p) => {
|
|
223
|
+
// don't adjust the off-screen/close position (from dismissOnSnapToBottom's 0% snap)
|
|
224
|
+
// — it must stay at screenSize so the user can drag between real snap points
|
|
225
|
+
// without accidentally closing the sheet
|
|
226
|
+
if (screenSize && p >= screenSize) return p
|
|
227
|
+
return Math.max(safeAreaTop, p - keyboardHeight)
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
activePositionsRef.current = result
|
|
231
|
+
return result
|
|
232
|
+
}, [positions, isKeyboardVisible, keyboardHeight, screenSize, isDragging])
|
|
233
|
+
|
|
144
234
|
const { useAnimatedNumber, useAnimatedNumberStyle, useAnimatedNumberReaction } =
|
|
145
235
|
animationDriver
|
|
146
236
|
const AnimatedView = (animationDriver.View ?? TamaguiView) as typeof Animated.View
|
|
@@ -179,8 +269,8 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
179
269
|
at.current = value
|
|
180
270
|
scrollBridge.paneY = value
|
|
181
271
|
// update isAtTop for scroll enable/disable
|
|
182
|
-
//
|
|
183
|
-
const minY =
|
|
272
|
+
// activePositions[0] is the top snap point (keyboard-adjusted minY)
|
|
273
|
+
const minY = activePositions[0]
|
|
184
274
|
const wasAtTop = scrollBridge.isAtTop
|
|
185
275
|
const nowAtTop = value <= minY + 5
|
|
186
276
|
if (wasAtTop !== nowAtTop) {
|
|
@@ -196,7 +286,7 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
196
286
|
}
|
|
197
287
|
}
|
|
198
288
|
},
|
|
199
|
-
[animationDriver,
|
|
289
|
+
[animationDriver, activePositions]
|
|
200
290
|
)
|
|
201
291
|
)
|
|
202
292
|
|
|
@@ -208,20 +298,23 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
208
298
|
}
|
|
209
299
|
}
|
|
210
300
|
|
|
211
|
-
const animateTo = useEvent((position: number) => {
|
|
301
|
+
const animateTo = useEvent((position: number, animationOverride?: any) => {
|
|
212
302
|
if (frameSize === 0) return
|
|
213
303
|
|
|
214
|
-
let toValue = isHidden || position === -1 ? screenSize :
|
|
304
|
+
let toValue = isHidden || position === -1 ? screenSize : activePositions[position]
|
|
215
305
|
|
|
216
306
|
if (at.current === toValue) return
|
|
217
307
|
|
|
218
308
|
at.current = toValue
|
|
219
309
|
stopSpring()
|
|
220
310
|
|
|
221
|
-
animatedNumber.setValue(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
311
|
+
animatedNumber.setValue(
|
|
312
|
+
toValue,
|
|
313
|
+
animationOverride || {
|
|
314
|
+
type: 'spring',
|
|
315
|
+
...transitionConfig,
|
|
316
|
+
}
|
|
317
|
+
)
|
|
225
318
|
})
|
|
226
319
|
|
|
227
320
|
useIsomorphicLayoutEffect(() => {
|
|
@@ -282,7 +375,6 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
282
375
|
|
|
283
376
|
const disableDrag = props.disableDrag ?? controller?.disableDrag
|
|
284
377
|
const themeName = useThemeName()
|
|
285
|
-
const [isDragging, setIsDragging] = React.useState(false)
|
|
286
378
|
const [blockPan, setBlockPan] = React.useState(false)
|
|
287
379
|
|
|
288
380
|
const panResponder = React.useMemo(() => {
|
|
@@ -442,9 +534,41 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
442
534
|
})
|
|
443
535
|
}, [disableDrag, isShowingInnerSheet, animateTo, frameSize, positions, setPosition])
|
|
444
536
|
|
|
537
|
+
// animate to keyboard-adjusted position when keyboard state changes
|
|
538
|
+
React.useEffect(() => {
|
|
539
|
+
if (isDragging || isHidden || !open || disableAnimation) return
|
|
540
|
+
if (!frameSize || !screenSize) return
|
|
541
|
+
// use timing animation to match iOS keyboard animation (~250ms)
|
|
542
|
+
animateTo(position, { type: 'timing', duration: 250 })
|
|
543
|
+
}, [isKeyboardVisible, keyboardHeight])
|
|
544
|
+
|
|
545
|
+
// reconcile position after drag ends — if keyboard dismissed during drag
|
|
546
|
+
// (e.g., input blur), activePositions reverted but onEnd used frozen positions
|
|
547
|
+
// for snap index. This effect ensures the sheet animates to the correct
|
|
548
|
+
// non-keyboard-adjusted position for the chosen snap index.
|
|
549
|
+
const wasDragging = React.useRef(false)
|
|
550
|
+
React.useEffect(() => {
|
|
551
|
+
if (isDragging) {
|
|
552
|
+
wasDragging.current = true
|
|
553
|
+
return
|
|
554
|
+
}
|
|
555
|
+
if (!wasDragging.current) return
|
|
556
|
+
wasDragging.current = false
|
|
557
|
+
// drag just ended — reconcile position with latest activePositions
|
|
558
|
+
if (!frameSize || !screenSize || isHidden || !open) return
|
|
559
|
+
animateTo(position)
|
|
560
|
+
}, [isDragging])
|
|
561
|
+
|
|
562
|
+
// dismiss keyboard when sheet closes
|
|
563
|
+
React.useEffect(() => {
|
|
564
|
+
if (!open && isKeyboardVisible) {
|
|
565
|
+
dismissKeyboard()
|
|
566
|
+
}
|
|
567
|
+
}, [open])
|
|
568
|
+
|
|
445
569
|
// gesture handler hook for RNGH-based gesture coordination
|
|
446
570
|
const { panGesture, panGestureRef, gestureHandlerEnabled } = useGestureHandlerPan({
|
|
447
|
-
positions,
|
|
571
|
+
positions: activePositions,
|
|
448
572
|
frameSize,
|
|
449
573
|
setPosition,
|
|
450
574
|
animateTo,
|
|
@@ -457,9 +581,10 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
457
581
|
isShowingInnerSheet,
|
|
458
582
|
setAnimatedPosition: (val: number) => {
|
|
459
583
|
// directly set the animated value for smooth dragging
|
|
460
|
-
|
|
584
|
+
at.current = val
|
|
461
585
|
animatedNumber.setValue(val, { type: 'direct' })
|
|
462
586
|
},
|
|
587
|
+
pauseKeyboardHandler,
|
|
463
588
|
})
|
|
464
589
|
|
|
465
590
|
const handleAnimationViewLayout = React.useCallback(
|
|
@@ -500,39 +625,6 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
500
625
|
}
|
|
501
626
|
})
|
|
502
627
|
|
|
503
|
-
const sizeBeforeKeyboard = React.useRef<number | null>(null)
|
|
504
|
-
React.useEffect(() => {
|
|
505
|
-
if (isWeb || !moveOnKeyboardChange) return
|
|
506
|
-
const keyboardShowListener = Keyboard.addListener(
|
|
507
|
-
currentPlatform === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow',
|
|
508
|
-
(e) => {
|
|
509
|
-
if (sizeBeforeKeyboard.current !== null) return
|
|
510
|
-
sizeBeforeKeyboard.current =
|
|
511
|
-
isHidden || position === -1 ? screenSize : positions[position]
|
|
512
|
-
animatedNumber.setValue(
|
|
513
|
-
Math.max(sizeBeforeKeyboard.current - e.endCoordinates.height, 0),
|
|
514
|
-
{
|
|
515
|
-
type: 'timing',
|
|
516
|
-
duration: 250,
|
|
517
|
-
}
|
|
518
|
-
)
|
|
519
|
-
}
|
|
520
|
-
)
|
|
521
|
-
const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
|
|
522
|
-
if (sizeBeforeKeyboard.current === null) return
|
|
523
|
-
animatedNumber.setValue(sizeBeforeKeyboard.current, {
|
|
524
|
-
type: 'timing',
|
|
525
|
-
duration: 250,
|
|
526
|
-
})
|
|
527
|
-
sizeBeforeKeyboard.current = null
|
|
528
|
-
})
|
|
529
|
-
|
|
530
|
-
return () => {
|
|
531
|
-
keyboardDidHideListener.remove()
|
|
532
|
-
keyboardShowListener.remove()
|
|
533
|
-
}
|
|
534
|
-
}, [moveOnKeyboardChange, positions, position, isHidden])
|
|
535
|
-
|
|
536
628
|
// we need to set this *after* fully closed to 0, to avoid it overlapping
|
|
537
629
|
// the page when resizing quickly on web for example
|
|
538
630
|
const [opacity, setOpacity] = React.useState(open ? 1 : 0)
|
package/src/SheetScrollView.tsx
CHANGED
|
@@ -279,6 +279,8 @@ export const SheetScrollView = React.forwardRef<
|
|
|
279
279
|
}}
|
|
280
280
|
contentContainerStyle={{ minHeight: '100%' }}
|
|
281
281
|
bounces={false}
|
|
282
|
+
keyboardShouldPersistTaps="always"
|
|
283
|
+
keyboardDismissMode="none"
|
|
282
284
|
{...props}
|
|
283
285
|
>
|
|
284
286
|
{/* wrapper to measure actual content height (not min-height expanded) */}
|
package/src/types.tsx
CHANGED
|
@@ -125,3 +125,50 @@ export type ScrollBridge = {
|
|
|
125
125
|
// whether sheet is at top position (for scroll enable/disable)
|
|
126
126
|
isAtTop?: boolean
|
|
127
127
|
}
|
|
128
|
+
|
|
129
|
+
// keyboard controller sheet types
|
|
130
|
+
|
|
131
|
+
export interface KeyboardControllerSheetOptions {
|
|
132
|
+
/**
|
|
133
|
+
* Whether keyboard handling is enabled.
|
|
134
|
+
* When false, the hook is a no-op.
|
|
135
|
+
*/
|
|
136
|
+
enabled: boolean
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface KeyboardControllerSheetResult {
|
|
140
|
+
/**
|
|
141
|
+
* Whether keyboard-controller is available and enabled.
|
|
142
|
+
*/
|
|
143
|
+
keyboardControllerEnabled: boolean
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Current keyboard height (0 when hidden).
|
|
147
|
+
* On web or when keyboard-controller is not available, always 0.
|
|
148
|
+
*/
|
|
149
|
+
keyboardHeight: number
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Whether the keyboard is currently visible.
|
|
153
|
+
*/
|
|
154
|
+
isKeyboardVisible: boolean
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Dismiss the keyboard programmatically.
|
|
158
|
+
* Called when sheet closes to dismiss the keyboard.
|
|
159
|
+
*/
|
|
160
|
+
dismissKeyboard: () => void
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Ref to pause keyboard hide state updates (action-sheet pattern).
|
|
164
|
+
* When true, keyboard hide events are ignored — keeps isKeyboardVisible=true
|
|
165
|
+
* and keyboardHeight at their last values during drag.
|
|
166
|
+
*/
|
|
167
|
+
pauseKeyboardHandler: React.RefObject<boolean>
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Flush any keyboard hide event that was suppressed while paused.
|
|
171
|
+
* Call after drag ends to reconcile actual keyboard state.
|
|
172
|
+
*/
|
|
173
|
+
flushPendingHide: () => void
|
|
174
|
+
}
|
|
@@ -10,7 +10,7 @@ interface GesturePanConfig {
|
|
|
10
10
|
positions: number[]
|
|
11
11
|
frameSize: number
|
|
12
12
|
setPosition: (pos: number) => void
|
|
13
|
-
animateTo: (pos: number) => void
|
|
13
|
+
animateTo: (pos: number, animationOverride?: any) => void
|
|
14
14
|
stopSpring: () => void
|
|
15
15
|
scrollBridge: ScrollBridge
|
|
16
16
|
setIsDragging: (val: boolean) => void
|
|
@@ -22,6 +22,8 @@ interface GesturePanConfig {
|
|
|
22
22
|
setAnimatedPosition: (val: number) => void
|
|
23
23
|
// ref to scroll gesture for simultaneousWithExternalGesture
|
|
24
24
|
scrollGestureRef?: RefObject<any> | null
|
|
25
|
+
// ref to pause keyboard hide events during gesture (action-sheet pattern)
|
|
26
|
+
pauseKeyboardHandler?: RefObject<boolean>
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
interface GesturePanResult {
|
|
@@ -80,21 +82,26 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
80
82
|
prevTranslationY: 0,
|
|
81
83
|
// track if scroll was engaged (scrollY > 0) at some point
|
|
82
84
|
scrollEngaged: false,
|
|
85
|
+
// positions frozen at gesture start — keyboard may dismiss during drag (input blur),
|
|
86
|
+
// causing positions to revert. Frozen positions ensure stable snap calculation.
|
|
87
|
+
frozenPositions: [] as number[],
|
|
88
|
+
frozenMinY: 0,
|
|
89
|
+
// whether pan gesture actually started (vs just a tap in onBegin)
|
|
90
|
+
panStarted: false,
|
|
83
91
|
})
|
|
84
92
|
|
|
85
93
|
const onStart = useCallback(() => {
|
|
86
94
|
stopSpring()
|
|
87
|
-
|
|
88
|
-
}, [stopSpring, setIsDragging])
|
|
95
|
+
}, [stopSpring])
|
|
89
96
|
|
|
90
97
|
const onEnd = useCallback(
|
|
91
|
-
(closestPoint: number) => {
|
|
98
|
+
(closestPoint: number, animationOverride?: any) => {
|
|
92
99
|
setIsDragging(false)
|
|
93
100
|
scrollBridge.setParentDragging(false)
|
|
94
101
|
// re-enable scroll when gesture ends
|
|
95
102
|
scrollBridge.setScrollEnabled?.(true)
|
|
96
103
|
setPosition(closestPoint)
|
|
97
|
-
animateTo(closestPoint)
|
|
104
|
+
animateTo(closestPoint, animationOverride)
|
|
98
105
|
},
|
|
99
106
|
[setIsDragging, scrollBridge, setPosition, animateTo]
|
|
100
107
|
)
|
|
@@ -122,6 +129,18 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
122
129
|
.failOffsetX([-20, 20])
|
|
123
130
|
.shouldCancelWhenOutside(false)
|
|
124
131
|
.onBegin(() => {
|
|
132
|
+
// onBegin fires on ANY touch (including taps to focus inputs).
|
|
133
|
+
// We do NOT set isDragging here — that would block keyboard animation.
|
|
134
|
+
// Instead, isDragging is set in onStart (actual pan gesture recognized).
|
|
135
|
+
gs.panStarted = false
|
|
136
|
+
|
|
137
|
+
// lightweight: pause keyboard handler to suppress hide events during gesture.
|
|
138
|
+
// This prevents keyboard flicker if input blurs before onStart.
|
|
139
|
+
// If this is just a tap, onFinalize un-pauses without setting isDragging.
|
|
140
|
+
if (config.pauseKeyboardHandler) {
|
|
141
|
+
config.pauseKeyboardHandler.current = true
|
|
142
|
+
}
|
|
143
|
+
|
|
125
144
|
// check position at gesture begin - before direction is known
|
|
126
145
|
const pos = getCurrentPosition()
|
|
127
146
|
const atTop = pos <= minY + AT_TOP_THRESHOLD
|
|
@@ -132,6 +151,10 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
132
151
|
gs.accumulatedOffset = 0
|
|
133
152
|
gs.prevTranslationY = 0
|
|
134
153
|
gs.scrollEngaged = currentScrollY > 0 // track if scroll is already engaged
|
|
154
|
+
// freeze positions at gesture start so keyboard dismiss during drag
|
|
155
|
+
// doesn't change snap targets mid-gesture
|
|
156
|
+
gs.frozenPositions = [...positions]
|
|
157
|
+
gs.frozenMinY = minY
|
|
135
158
|
|
|
136
159
|
// if sheet not at top, DISABLE SCROLL immediately and lock to 0
|
|
137
160
|
// this prevents scroll from firing before pan takes over
|
|
@@ -140,6 +163,11 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
140
163
|
}
|
|
141
164
|
})
|
|
142
165
|
.onStart(() => {
|
|
166
|
+
// onStart fires only when pan gesture is recognized (finger moved enough).
|
|
167
|
+
// Safe to set isDragging here — this won't fire for taps to focus inputs.
|
|
168
|
+
gs.panStarted = true
|
|
169
|
+
setIsDragging(true)
|
|
170
|
+
|
|
143
171
|
// console.warn('[RNGH-Pan] onStart', { startY: gs.startY, minY })
|
|
144
172
|
scrollBridge.initialPosition = gs.startY
|
|
145
173
|
onStart()
|
|
@@ -250,21 +278,30 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
250
278
|
// clear scroll lock
|
|
251
279
|
scrollBridge.scrollLockY = undefined
|
|
252
280
|
|
|
281
|
+
// use frozen positions from gesture start — keyboard may have dismissed
|
|
282
|
+
// during drag (input blur), reverting activePositions. Frozen positions
|
|
283
|
+
// ensure the snap index reflects the user's intent at drag start.
|
|
284
|
+
// The onEnd callback's animateTo uses latest activePositions for the
|
|
285
|
+
// actual animation target, so the sheet ends up at the right place.
|
|
286
|
+
const snapPositions =
|
|
287
|
+
gs.frozenPositions.length > 0 ? gs.frozenPositions : positions
|
|
288
|
+
const snapMinY = gs.frozenPositions.length > 0 ? gs.frozenMinY : minY
|
|
289
|
+
|
|
253
290
|
// if sheet is at top and scroll is engaged, just stay at top
|
|
254
|
-
if (currentPos <=
|
|
291
|
+
if (currentPos <= snapMinY + AT_TOP_THRESHOLD && scrollBridge.y > 0) {
|
|
255
292
|
onEnd(0)
|
|
256
293
|
return
|
|
257
294
|
}
|
|
258
295
|
|
|
259
|
-
//
|
|
296
|
+
// snap calculation using frozen positions
|
|
260
297
|
const velocity = velocityY / 1000
|
|
261
298
|
const projectedEnd = currentPos + frameSize * velocity * 0.2
|
|
262
299
|
|
|
263
300
|
let closestPoint = 0
|
|
264
301
|
let minDist = Number.POSITIVE_INFINITY
|
|
265
302
|
|
|
266
|
-
for (let i = 0; i <
|
|
267
|
-
const pos =
|
|
303
|
+
for (let i = 0; i < snapPositions.length; i++) {
|
|
304
|
+
const pos = snapPositions[i]
|
|
268
305
|
const dist = Math.abs(projectedEnd - pos)
|
|
269
306
|
if (dist < minDist) {
|
|
270
307
|
minDist = dist
|
|
@@ -275,9 +312,19 @@ export function useGestureHandlerPan(config: GesturePanConfig): GesturePanResult
|
|
|
275
312
|
onEnd(closestPoint)
|
|
276
313
|
})
|
|
277
314
|
.onFinalize(() => {
|
|
278
|
-
// console.warn('[RNGH-Pan] onFinalize')
|
|
315
|
+
// console.warn('[RNGH-Pan] onFinalize', { panStarted: gs.panStarted })
|
|
279
316
|
// clear scroll lock on finalize too (safety)
|
|
280
317
|
scrollBridge.scrollLockY = undefined
|
|
318
|
+
if (gs.panStarted) {
|
|
319
|
+
// real pan gesture — reset isDragging (also un-pauses keyboard handler + flushes)
|
|
320
|
+
setIsDragging(false)
|
|
321
|
+
} else {
|
|
322
|
+
// just a tap (onBegin fired but onStart never did) — un-pause keyboard handler
|
|
323
|
+
// without setting isDragging (which would block keyboard animation effect)
|
|
324
|
+
if (config.pauseKeyboardHandler) {
|
|
325
|
+
config.pauseKeyboardHandler.current = false
|
|
326
|
+
}
|
|
327
|
+
}
|
|
281
328
|
})
|
|
282
329
|
.runOnJS(true)
|
|
283
330
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native implementation of keyboard controller sheet hook.
|
|
3
|
+
*
|
|
4
|
+
* Simplified to just track keyboard state (height, visibility).
|
|
5
|
+
* Position animation is handled by SheetImplementationCustom via
|
|
6
|
+
* keyboard-adjusted positions — matching the react-native-actions-sheet pattern.
|
|
7
|
+
*
|
|
8
|
+
* Uses react-native-keyboard-controller events when available,
|
|
9
|
+
* falls back to basic Keyboard API otherwise.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
13
|
+
import { Keyboard, Platform } from 'react-native'
|
|
14
|
+
import type {
|
|
15
|
+
KeyboardControllerSheetOptions,
|
|
16
|
+
KeyboardControllerSheetResult,
|
|
17
|
+
} from './types'
|
|
18
|
+
|
|
19
|
+
// lazy import state accessors
|
|
20
|
+
let isKeyboardControllerEnabled: () => boolean = () => false
|
|
21
|
+
let getKeyboardControllerState: () => any = () => ({})
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const nativeModule = require('@tamagui/native')
|
|
25
|
+
isKeyboardControllerEnabled = nativeModule.isKeyboardControllerEnabled
|
|
26
|
+
getKeyboardControllerState = nativeModule.getKeyboardControllerState
|
|
27
|
+
} catch {
|
|
28
|
+
// @tamagui/native not available
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useKeyboardControllerSheet(
|
|
32
|
+
options: KeyboardControllerSheetOptions
|
|
33
|
+
): KeyboardControllerSheetResult {
|
|
34
|
+
const { enabled } = options
|
|
35
|
+
|
|
36
|
+
const [keyboardHeight, setKeyboardHeight] = useState(0)
|
|
37
|
+
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false)
|
|
38
|
+
const keyboardControllerEnabled = isKeyboardControllerEnabled()
|
|
39
|
+
|
|
40
|
+
// action-sheet pattern: pause keyboard hide events during drag
|
|
41
|
+
// when true, keyboard hide events are ignored so isKeyboardVisible stays true
|
|
42
|
+
// and activePositions don't revert mid-gesture
|
|
43
|
+
const pauseKeyboardHandler = useRef(false)
|
|
44
|
+
// tracks if a keyboard hide event was suppressed while paused
|
|
45
|
+
const pendingHide = useRef(false)
|
|
46
|
+
|
|
47
|
+
// dismiss keyboard helper
|
|
48
|
+
const dismissKeyboard = useCallback(() => {
|
|
49
|
+
Keyboard.dismiss()
|
|
50
|
+
if (keyboardControllerEnabled) {
|
|
51
|
+
try {
|
|
52
|
+
const { KeyboardController } = getKeyboardControllerState()
|
|
53
|
+
KeyboardController?.dismiss?.()
|
|
54
|
+
} catch {
|
|
55
|
+
// ignore errors from keyboard-controller
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}, [keyboardControllerEnabled])
|
|
59
|
+
|
|
60
|
+
// flush any keyboard hide that was suppressed during drag.
|
|
61
|
+
// called when drag ends to reconcile actual keyboard state.
|
|
62
|
+
const flushPendingHide = useCallback(() => {
|
|
63
|
+
if (pendingHide.current) {
|
|
64
|
+
pendingHide.current = false
|
|
65
|
+
setIsKeyboardVisible(false)
|
|
66
|
+
setKeyboardHeight(0)
|
|
67
|
+
}
|
|
68
|
+
}, [])
|
|
69
|
+
|
|
70
|
+
// keyboard-controller event listeners (preferred when available)
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!enabled || !keyboardControllerEnabled) return
|
|
73
|
+
|
|
74
|
+
const { KeyboardEvents } = getKeyboardControllerState()
|
|
75
|
+
if (!KeyboardEvents?.addListener) return
|
|
76
|
+
|
|
77
|
+
const showSub = KeyboardEvents.addListener('keyboardWillShow', (e: any) => {
|
|
78
|
+
const height = e?.height ?? 0
|
|
79
|
+
if (height > 0) {
|
|
80
|
+
setKeyboardHeight(height)
|
|
81
|
+
}
|
|
82
|
+
setIsKeyboardVisible(true)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const hideSub = KeyboardEvents.addListener('keyboardWillHide', () => {
|
|
86
|
+
if (pauseKeyboardHandler.current) {
|
|
87
|
+
pendingHide.current = true
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
setIsKeyboardVisible(false)
|
|
91
|
+
setKeyboardHeight(0)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
showSub?.remove?.()
|
|
96
|
+
hideSub?.remove?.()
|
|
97
|
+
}
|
|
98
|
+
}, [enabled, keyboardControllerEnabled])
|
|
99
|
+
|
|
100
|
+
// fallback to basic Keyboard API when keyboard-controller not available
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!enabled) return
|
|
103
|
+
if (keyboardControllerEnabled) return
|
|
104
|
+
|
|
105
|
+
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'
|
|
106
|
+
const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide'
|
|
107
|
+
|
|
108
|
+
const showListener = Keyboard.addListener(showEvent, (e) => {
|
|
109
|
+
setKeyboardHeight(e.endCoordinates.height)
|
|
110
|
+
setIsKeyboardVisible(true)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const hideListener = Keyboard.addListener(hideEvent, () => {
|
|
114
|
+
if (pauseKeyboardHandler.current) {
|
|
115
|
+
pendingHide.current = true
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
setIsKeyboardVisible(false)
|
|
119
|
+
setKeyboardHeight(0)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return () => {
|
|
123
|
+
showListener.remove()
|
|
124
|
+
hideListener.remove()
|
|
125
|
+
}
|
|
126
|
+
}, [enabled, keyboardControllerEnabled])
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
keyboardControllerEnabled,
|
|
130
|
+
keyboardHeight,
|
|
131
|
+
isKeyboardVisible,
|
|
132
|
+
dismissKeyboard,
|
|
133
|
+
pauseKeyboardHandler,
|
|
134
|
+
flushPendingHide,
|
|
135
|
+
}
|
|
136
|
+
}
|