@tamagui/sheet 2.0.0-1769233344020 → 2.0.0-1769288527117
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/GestureDetectorWrapper.cjs +48 -0
- package/dist/cjs/GestureDetectorWrapper.js +29 -0
- package/dist/cjs/GestureDetectorWrapper.js.map +6 -0
- package/dist/cjs/GestureDetectorWrapper.native.js +53 -0
- package/dist/cjs/GestureDetectorWrapper.native.js.map +1 -0
- package/dist/cjs/GestureSheetContext.cjs +52 -0
- package/dist/cjs/GestureSheetContext.js +43 -0
- package/dist/cjs/GestureSheetContext.js.map +6 -0
- package/dist/cjs/GestureSheetContext.native.js +56 -0
- package/dist/cjs/GestureSheetContext.native.js.map +1 -0
- package/dist/cjs/SheetImplementationCustom.cjs +85 -40
- package/dist/cjs/SheetImplementationCustom.js +81 -58
- package/dist/cjs/SheetImplementationCustom.js.map +1 -1
- package/dist/cjs/SheetImplementationCustom.native.js +105 -50
- package/dist/cjs/SheetImplementationCustom.native.js.map +1 -1
- package/dist/cjs/SheetScrollView.cjs +88 -17
- package/dist/cjs/SheetScrollView.js +85 -26
- package/dist/cjs/SheetScrollView.js.map +1 -1
- package/dist/cjs/SheetScrollView.native.js +102 -19
- package/dist/cjs/SheetScrollView.native.js.map +1 -1
- package/dist/cjs/gestureState.cjs +39 -0
- package/dist/cjs/gestureState.js +34 -0
- package/dist/cjs/gestureState.js.map +6 -0
- package/dist/cjs/gestureState.native.js +42 -0
- package/dist/cjs/gestureState.native.js.map +1 -0
- package/dist/cjs/setupGestureHandler.cjs +46 -0
- package/dist/cjs/setupGestureHandler.js +38 -0
- package/dist/cjs/setupGestureHandler.js.map +6 -0
- package/dist/cjs/setupGestureHandler.native.js +50 -0
- package/dist/cjs/setupGestureHandler.native.js.map +1 -0
- package/dist/cjs/useGestureHandlerPan.cjs +126 -0
- package/dist/cjs/useGestureHandlerPan.js +117 -0
- package/dist/cjs/useGestureHandlerPan.js.map +6 -0
- package/dist/cjs/useGestureHandlerPan.native.js +135 -0
- package/dist/cjs/useGestureHandlerPan.native.js.map +1 -0
- package/dist/esm/GestureDetectorWrapper.js +15 -0
- package/dist/esm/GestureDetectorWrapper.js.map +6 -0
- package/dist/esm/GestureDetectorWrapper.mjs +25 -0
- package/dist/esm/GestureDetectorWrapper.mjs.map +1 -0
- package/dist/esm/GestureDetectorWrapper.native.js +27 -0
- package/dist/esm/GestureDetectorWrapper.native.js.map +1 -0
- package/dist/esm/GestureSheetContext.js +28 -0
- package/dist/esm/GestureSheetContext.js.map +6 -0
- package/dist/esm/GestureSheetContext.mjs +28 -0
- package/dist/esm/GestureSheetContext.mjs.map +1 -0
- package/dist/esm/GestureSheetContext.native.js +29 -0
- package/dist/esm/GestureSheetContext.native.js.map +1 -0
- package/dist/esm/SheetImplementationCustom.js +83 -57
- package/dist/esm/SheetImplementationCustom.js.map +1 -1
- package/dist/esm/SheetImplementationCustom.mjs +85 -40
- package/dist/esm/SheetImplementationCustom.mjs.map +1 -1
- package/dist/esm/SheetImplementationCustom.native.js +105 -50
- package/dist/esm/SheetImplementationCustom.native.js.map +1 -1
- package/dist/esm/SheetScrollView.js +87 -26
- package/dist/esm/SheetScrollView.js.map +1 -1
- package/dist/esm/SheetScrollView.mjs +89 -18
- package/dist/esm/SheetScrollView.mjs.map +1 -1
- package/dist/esm/SheetScrollView.native.js +103 -20
- package/dist/esm/SheetScrollView.native.js.map +1 -1
- package/dist/esm/gestureState.js +18 -0
- package/dist/esm/gestureState.js.map +6 -0
- package/dist/esm/gestureState.mjs +13 -0
- package/dist/esm/gestureState.mjs.map +1 -0
- package/dist/esm/gestureState.native.js +13 -0
- package/dist/esm/gestureState.native.js.map +1 -0
- package/dist/esm/setupGestureHandler.js +22 -0
- package/dist/esm/setupGestureHandler.js.map +6 -0
- package/dist/esm/setupGestureHandler.mjs +22 -0
- package/dist/esm/setupGestureHandler.mjs.map +1 -0
- package/dist/esm/setupGestureHandler.native.js +23 -0
- package/dist/esm/setupGestureHandler.native.js.map +1 -0
- package/dist/esm/useGestureHandlerPan.js +102 -0
- package/dist/esm/useGestureHandlerPan.js.map +6 -0
- package/dist/esm/useGestureHandlerPan.mjs +103 -0
- package/dist/esm/useGestureHandlerPan.mjs.map +1 -0
- package/dist/esm/useGestureHandlerPan.native.js +109 -0
- package/dist/esm/useGestureHandlerPan.native.js.map +1 -0
- package/dist/jsx/GestureDetectorWrapper.js +15 -0
- package/dist/jsx/GestureDetectorWrapper.js.map +6 -0
- package/dist/jsx/GestureDetectorWrapper.mjs +25 -0
- package/dist/jsx/GestureDetectorWrapper.mjs.map +1 -0
- package/dist/jsx/GestureDetectorWrapper.native.js +53 -0
- package/dist/jsx/GestureDetectorWrapper.native.js.map +1 -0
- package/dist/jsx/GestureSheetContext.js +28 -0
- package/dist/jsx/GestureSheetContext.js.map +6 -0
- package/dist/jsx/GestureSheetContext.mjs +28 -0
- package/dist/jsx/GestureSheetContext.mjs.map +1 -0
- package/dist/jsx/GestureSheetContext.native.js +56 -0
- package/dist/jsx/GestureSheetContext.native.js.map +1 -0
- package/dist/jsx/SheetImplementationCustom.js +83 -57
- package/dist/jsx/SheetImplementationCustom.js.map +1 -1
- package/dist/jsx/SheetImplementationCustom.mjs +85 -40
- package/dist/jsx/SheetImplementationCustom.mjs.map +1 -1
- package/dist/jsx/SheetImplementationCustom.native.js +105 -50
- package/dist/jsx/SheetImplementationCustom.native.js.map +1 -1
- package/dist/jsx/SheetScrollView.js +87 -26
- package/dist/jsx/SheetScrollView.js.map +1 -1
- package/dist/jsx/SheetScrollView.mjs +89 -18
- package/dist/jsx/SheetScrollView.mjs.map +1 -1
- package/dist/jsx/SheetScrollView.native.js +102 -19
- package/dist/jsx/SheetScrollView.native.js.map +1 -1
- package/dist/jsx/gestureState.js +18 -0
- package/dist/jsx/gestureState.js.map +6 -0
- package/dist/jsx/gestureState.mjs +13 -0
- package/dist/jsx/gestureState.mjs.map +1 -0
- package/dist/jsx/gestureState.native.js +42 -0
- package/dist/jsx/gestureState.native.js.map +1 -0
- package/dist/jsx/setupGestureHandler.js +22 -0
- package/dist/jsx/setupGestureHandler.js.map +6 -0
- package/dist/jsx/setupGestureHandler.mjs +22 -0
- package/dist/jsx/setupGestureHandler.mjs.map +1 -0
- package/dist/jsx/setupGestureHandler.native.js +50 -0
- package/dist/jsx/setupGestureHandler.native.js.map +1 -0
- package/dist/jsx/useGestureHandlerPan.js +102 -0
- package/dist/jsx/useGestureHandlerPan.js.map +6 -0
- package/dist/jsx/useGestureHandlerPan.mjs +103 -0
- package/dist/jsx/useGestureHandlerPan.mjs.map +1 -0
- package/dist/jsx/useGestureHandlerPan.native.js +135 -0
- package/dist/jsx/useGestureHandlerPan.native.js.map +1 -0
- package/package.json +48 -21
- package/src/GestureDetectorWrapper.tsx +41 -0
- package/src/GestureSheetContext.tsx +62 -0
- package/src/SheetImplementationCustom.tsx +124 -57
- package/src/SheetScrollView.tsx +169 -20
- package/src/gestureState.ts +24 -0
- package/src/setupGestureHandler.ts +34 -0
- package/src/types.tsx +15 -0
- package/src/useGestureHandlerPan.tsx +312 -0
- package/types/GestureDetectorWrapper.d.ts +14 -0
- package/types/GestureDetectorWrapper.d.ts.map +1 -0
- package/types/GestureDetectorWrapper.native.d.ts +14 -0
- package/types/GestureSheetContext.d.ts +36 -0
- package/types/GestureSheetContext.d.ts.map +1 -0
- package/types/SheetImplementationCustom.d.ts.map +1 -1
- package/types/SheetScrollView.d.ts.map +1 -1
- package/types/gestureState.d.ts +11 -0
- package/types/gestureState.d.ts.map +1 -0
- package/types/gestureState.native.d.ts +12 -0
- package/types/setupGestureHandler.d.ts +11 -0
- package/types/setupGestureHandler.d.ts.map +1 -0
- package/types/setupGestureHandler.native.d.ts +41 -0
- package/types/types.d.ts +8 -0
- package/types/types.d.ts.map +1 -1
- package/types/useGestureHandlerPan.d.ts +43 -0
- package/types/useGestureHandlerPan.d.ts.map +1 -0
- package/types/useGestureHandlerPan.native.d.ts +33 -0
|
@@ -26,9 +26,12 @@ import type {
|
|
|
26
26
|
} from 'react-native'
|
|
27
27
|
import { Dimensions, Keyboard, PanResponder, View } from 'react-native'
|
|
28
28
|
import { ParentSheetContext, SheetInsideSheetContext } from './contexts'
|
|
29
|
+
import { GestureDetectorWrapper } from './GestureDetectorWrapper'
|
|
30
|
+
import { GestureSheetProvider } from './GestureSheetContext'
|
|
29
31
|
import { resisted } from './helpers'
|
|
30
32
|
import { SheetProvider } from './SheetContext'
|
|
31
33
|
import type { SheetProps, SnapPointsMode } from './types'
|
|
34
|
+
import { useGestureHandlerPan } from './useGestureHandlerPan'
|
|
32
35
|
import { useSheetOpenState } from './useSheetOpenState'
|
|
33
36
|
import { useSheetProviderProps } from './useSheetProviderProps'
|
|
34
37
|
|
|
@@ -131,18 +134,16 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
131
134
|
}
|
|
132
135
|
}, [open, frameSize])
|
|
133
136
|
|
|
137
|
+
// use stableFrameSize when closing to prevent position jumps during exit animation
|
|
138
|
+
// but when opening, always use the current frameSize so positions update correctly
|
|
139
|
+
const effectiveFrameSize = open ? frameSize : stableFrameSize.current || frameSize
|
|
140
|
+
|
|
134
141
|
const positions = React.useMemo(
|
|
135
142
|
() =>
|
|
136
143
|
snapPoints.map((point) =>
|
|
137
|
-
|
|
138
|
-
getYPositions(
|
|
139
|
-
snapPointsMode,
|
|
140
|
-
point,
|
|
141
|
-
screenSize,
|
|
142
|
-
open ? frameSize : stableFrameSize.current
|
|
143
|
-
)
|
|
144
|
+
getYPositions(snapPointsMode, point, screenSize, effectiveFrameSize)
|
|
144
145
|
),
|
|
145
|
-
[screenSize,
|
|
146
|
+
[screenSize, effectiveFrameSize, snapPoints, snapPointsMode]
|
|
146
147
|
)
|
|
147
148
|
|
|
148
149
|
const { useAnimatedNumber, useAnimatedNumberStyle, useAnimatedNumberReaction } =
|
|
@@ -182,8 +183,25 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
182
183
|
(value) => {
|
|
183
184
|
at.current = value
|
|
184
185
|
scrollBridge.paneY = value
|
|
186
|
+
// update isAtTop for scroll enable/disable
|
|
187
|
+
// positions[0] is the top snap point (minY)
|
|
188
|
+
const minY = positions[0]
|
|
189
|
+
const wasAtTop = scrollBridge.isAtTop
|
|
190
|
+
const nowAtTop = value <= minY + 5
|
|
191
|
+
if (wasAtTop !== nowAtTop) {
|
|
192
|
+
scrollBridge.isAtTop = nowAtTop
|
|
193
|
+
// when reaching top, enable scroll; when leaving top, disable scroll
|
|
194
|
+
// this preemptively sets scroll state before any gestures start
|
|
195
|
+
if (nowAtTop) {
|
|
196
|
+
scrollBridge.scrollLockY = undefined
|
|
197
|
+
scrollBridge.setScrollEnabled?.(true)
|
|
198
|
+
} else {
|
|
199
|
+
scrollBridge.scrollLockY = 0
|
|
200
|
+
scrollBridge.setScrollEnabled?.(false)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
185
203
|
},
|
|
186
|
-
[animationDriver]
|
|
204
|
+
[animationDriver, positions]
|
|
187
205
|
)
|
|
188
206
|
)
|
|
189
207
|
|
|
@@ -251,11 +269,26 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
251
269
|
scrollBridge.scrollLock = false
|
|
252
270
|
scrollBridge.scrollStartY = -1
|
|
253
271
|
}
|
|
272
|
+
|
|
273
|
+
// set initial isAtTop state when sheet opens
|
|
274
|
+
// position 0 = top snap point, so isAtTop = true
|
|
275
|
+
if (open && position >= 0) {
|
|
276
|
+
const isTopPosition = position === 0
|
|
277
|
+
scrollBridge.isAtTop = isTopPosition
|
|
278
|
+
if (isTopPosition) {
|
|
279
|
+
scrollBridge.scrollLockY = undefined
|
|
280
|
+
scrollBridge.setScrollEnabled?.(true)
|
|
281
|
+
} else {
|
|
282
|
+
scrollBridge.scrollLockY = 0
|
|
283
|
+
scrollBridge.setScrollEnabled?.(false)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
254
286
|
}, [hasntMeasured, disableAnimation, isHidden, frameSize, screenSize, open, position])
|
|
255
287
|
|
|
256
288
|
const disableDrag = props.disableDrag ?? controller?.disableDrag
|
|
257
289
|
const themeName = useThemeName()
|
|
258
290
|
const [isDragging, setIsDragging] = React.useState(false)
|
|
291
|
+
const [blockPan, setBlockPan] = React.useState(false)
|
|
259
292
|
|
|
260
293
|
const panResponder = React.useMemo(() => {
|
|
261
294
|
if (disableDrag) return
|
|
@@ -414,6 +447,26 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
414
447
|
})
|
|
415
448
|
}, [disableDrag, isShowingInnerSheet, animateTo, frameSize, positions, setPosition])
|
|
416
449
|
|
|
450
|
+
// gesture handler hook for RNGH-based gesture coordination
|
|
451
|
+
const { panGesture, panGestureRef, gestureHandlerEnabled } = useGestureHandlerPan({
|
|
452
|
+
positions,
|
|
453
|
+
frameSize,
|
|
454
|
+
setPosition,
|
|
455
|
+
animateTo,
|
|
456
|
+
stopSpring,
|
|
457
|
+
scrollBridge,
|
|
458
|
+
setIsDragging,
|
|
459
|
+
getCurrentPosition: () => at.current,
|
|
460
|
+
resisted,
|
|
461
|
+
disableDrag,
|
|
462
|
+
isShowingInnerSheet,
|
|
463
|
+
setAnimatedPosition: (val: number) => {
|
|
464
|
+
// directly set the animated value for smooth dragging
|
|
465
|
+
// console.warn('[RNGH-Sheet] setAnimatedPosition:', val.toFixed(1))
|
|
466
|
+
animatedNumber.setValue(val, { type: 'direct' })
|
|
467
|
+
},
|
|
468
|
+
})
|
|
469
|
+
|
|
417
470
|
const handleAnimationViewLayout = React.useCallback(
|
|
418
471
|
(e: LayoutChangeEvent) => {
|
|
419
472
|
// FIX: Don't update frameSize during exit animation to prevent position jumps
|
|
@@ -523,52 +576,65 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
523
576
|
<LayoutMeasurementController disable={!open}>
|
|
524
577
|
<ParentSheetContext.Provider value={nextParentContext}>
|
|
525
578
|
<SheetProvider {...providerProps} setHasScrollView={setHasScrollView}>
|
|
526
|
-
<
|
|
527
|
-
{
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
style={{
|
|
533
|
-
opacity: 0,
|
|
534
|
-
position: 'absolute',
|
|
535
|
-
top: 0,
|
|
536
|
-
left: 0,
|
|
537
|
-
right: 0,
|
|
538
|
-
bottom: 0,
|
|
539
|
-
pointerEvents: 'none',
|
|
540
|
-
}}
|
|
541
|
-
onLayout={handleMaxContentViewLayout}
|
|
542
|
-
/>
|
|
543
|
-
)}
|
|
544
|
-
|
|
545
|
-
<AnimatedView
|
|
546
|
-
ref={ref}
|
|
547
|
-
{...panResponder?.panHandlers}
|
|
548
|
-
onLayout={handleAnimationViewLayout}
|
|
549
|
-
// @ts-ignore for CSS driver this is necessary to attach the transition
|
|
550
|
-
// also motion driver at least though i suspect all drivers?
|
|
551
|
-
transition={isDragging || disableAnimation ? null : transition}
|
|
552
|
-
// @ts-ignore
|
|
553
|
-
disableClassName
|
|
554
|
-
style={[
|
|
555
|
-
{
|
|
556
|
-
position: 'absolute',
|
|
557
|
-
zIndex,
|
|
558
|
-
width: '100%',
|
|
559
|
-
height: forcedContentHeight,
|
|
560
|
-
minHeight: forcedContentHeight,
|
|
561
|
-
opacity: !shouldHideParentSheet ? opacity : 0,
|
|
562
|
-
...((shouldHideParentSheet || !open) && {
|
|
563
|
-
pointerEvents: 'none',
|
|
564
|
-
}),
|
|
565
|
-
},
|
|
566
|
-
animatedStyle,
|
|
567
|
-
]}
|
|
579
|
+
<GestureSheetProvider
|
|
580
|
+
isDragging={isDragging}
|
|
581
|
+
blockPan={blockPan}
|
|
582
|
+
setBlockPan={setBlockPan}
|
|
583
|
+
panGesture={panGesture}
|
|
584
|
+
panGestureRef={panGestureRef}
|
|
568
585
|
>
|
|
569
|
-
{
|
|
570
|
-
|
|
571
|
-
|
|
586
|
+
<AnimatePresence custom={{ open }}>
|
|
587
|
+
{shouldHideParentSheet || !open ? null : overlayComponent}
|
|
588
|
+
</AnimatePresence>
|
|
589
|
+
|
|
590
|
+
{snapPointsMode !== 'percent' && (
|
|
591
|
+
<View
|
|
592
|
+
style={{
|
|
593
|
+
opacity: 0,
|
|
594
|
+
position: 'absolute',
|
|
595
|
+
top: 0,
|
|
596
|
+
left: 0,
|
|
597
|
+
right: 0,
|
|
598
|
+
bottom: 0,
|
|
599
|
+
pointerEvents: 'none',
|
|
600
|
+
}}
|
|
601
|
+
onLayout={handleMaxContentViewLayout}
|
|
602
|
+
/>
|
|
603
|
+
)}
|
|
604
|
+
|
|
605
|
+
<AnimatedView
|
|
606
|
+
ref={ref}
|
|
607
|
+
{...(!gestureHandlerEnabled && panResponder?.panHandlers)}
|
|
608
|
+
onLayout={handleAnimationViewLayout}
|
|
609
|
+
// @ts-ignore for CSS driver this is necessary to attach the transition
|
|
610
|
+
// also motion driver at least though i suspect all drivers?
|
|
611
|
+
transition={isDragging || disableAnimation ? null : transition}
|
|
612
|
+
// @ts-ignore
|
|
613
|
+
disableClassName
|
|
614
|
+
style={[
|
|
615
|
+
{
|
|
616
|
+
position: 'absolute',
|
|
617
|
+
zIndex,
|
|
618
|
+
width: '100%',
|
|
619
|
+
height: forcedContentHeight,
|
|
620
|
+
minHeight: forcedContentHeight,
|
|
621
|
+
opacity: !shouldHideParentSheet ? opacity : 0,
|
|
622
|
+
...((shouldHideParentSheet || !open) && {
|
|
623
|
+
pointerEvents: 'none',
|
|
624
|
+
}),
|
|
625
|
+
},
|
|
626
|
+
animatedStyle,
|
|
627
|
+
]}
|
|
628
|
+
>
|
|
629
|
+
{gestureHandlerEnabled && panGesture ? (
|
|
630
|
+
<GestureDetectorWrapper gesture={panGesture} style={{ flex: 1 }}>
|
|
631
|
+
{props.children}
|
|
632
|
+
</GestureDetectorWrapper>
|
|
633
|
+
) : (
|
|
634
|
+
props.children
|
|
635
|
+
)}
|
|
636
|
+
</AnimatedView>
|
|
637
|
+
</GestureSheetProvider>
|
|
572
638
|
</SheetProvider>
|
|
573
639
|
</ParentSheetContext.Provider>
|
|
574
640
|
</LayoutMeasurementController>
|
|
@@ -623,7 +689,9 @@ function getYPositions(
|
|
|
623
689
|
screenSize?: number,
|
|
624
690
|
frameSize?: number
|
|
625
691
|
) {
|
|
626
|
-
if (!screenSize || !frameSize)
|
|
692
|
+
if (!screenSize || !frameSize) {
|
|
693
|
+
return 0
|
|
694
|
+
}
|
|
627
695
|
|
|
628
696
|
if (mode === 'mixed') {
|
|
629
697
|
if (typeof point === 'number') {
|
|
@@ -638,8 +706,7 @@ function getYPositions(
|
|
|
638
706
|
console.warn('Invalid snapPoint percentage string')
|
|
639
707
|
return 0
|
|
640
708
|
}
|
|
641
|
-
|
|
642
|
-
return next
|
|
709
|
+
return Math.round(screenSize - pct * screenSize)
|
|
643
710
|
}
|
|
644
711
|
console.warn('Invalid snapPoint unknown value')
|
|
645
712
|
return 0
|
package/src/SheetScrollView.tsx
CHANGED
|
@@ -5,10 +5,12 @@ import { ScrollView } from '@tamagui/scroll-view'
|
|
|
5
5
|
import { useControllableState } from '@tamagui/use-controllable-state'
|
|
6
6
|
import React, { useEffect, useRef, useState } from 'react'
|
|
7
7
|
import type { ScrollView as RNScrollView } from 'react-native'
|
|
8
|
+
import { useGestureSheetContext } from './GestureSheetContext'
|
|
9
|
+
import { getGestureHandlerState, isGestureHandlerEnabled } from './gestureState'
|
|
8
10
|
import { useSheetContext } from './SheetContext'
|
|
9
11
|
import type { SheetScopedProps } from './types'
|
|
10
12
|
|
|
11
|
-
//
|
|
13
|
+
// uses react-native-gesture-handler's simultaneousWithExternalGesture for native-quality coordination
|
|
12
14
|
|
|
13
15
|
/* -------------------------------------------------------------------------------------------------
|
|
14
16
|
* SheetScrollView
|
|
@@ -33,6 +35,7 @@ export const SheetScrollView = React.forwardRef<
|
|
|
33
35
|
ref
|
|
34
36
|
) => {
|
|
35
37
|
const context = useSheetContext(SHEET_SCROLL_VIEW_NAME, __scopeSheet)
|
|
38
|
+
const gestureContext = useGestureSheetContext()
|
|
36
39
|
const { scrollBridge, setHasScrollView } = context
|
|
37
40
|
const [scrollEnabled, setScrollEnabled_] = useControllableState({
|
|
38
41
|
prop: scrollEnabledProp,
|
|
@@ -40,16 +43,71 @@ export const SheetScrollView = React.forwardRef<
|
|
|
40
43
|
})
|
|
41
44
|
const scrollRef = React.useRef<RNScrollView | null>(null)
|
|
42
45
|
|
|
46
|
+
// get the pan gesture ref for simultaneousHandlers
|
|
47
|
+
// react-native-actions-sheet pattern: pass panGestureRef to ScrollView's simultaneousHandlers
|
|
48
|
+
const panGestureRef = gestureContext?.panGestureRef
|
|
49
|
+
|
|
50
|
+
// get RNGH ScrollView from gestureState (passed via setupGestureHandler to avoid double registration)
|
|
51
|
+
const { ScrollView: RNGHScrollView } = getGestureHandlerState()
|
|
52
|
+
|
|
53
|
+
// determine which ScrollView component to use - need RNGH ScrollView for simultaneousHandlers
|
|
54
|
+
const useRNGHScrollView = isGestureHandlerEnabled() && RNGHScrollView && panGestureRef
|
|
55
|
+
|
|
56
|
+
// console.warn('[RNGH-Scroll] Setup:', {
|
|
57
|
+
// enabled: isGestureHandlerEnabled(),
|
|
58
|
+
// hasRNGHScrollView: !!RNGHScrollView,
|
|
59
|
+
// hasPanRef: !!panGestureRef,
|
|
60
|
+
// useRNGHScrollView
|
|
61
|
+
// })
|
|
62
|
+
|
|
43
63
|
// could make it so it has negative bottom margin and then pads the bottom content
|
|
44
64
|
// to avoid clipping effect when resizing smaller
|
|
45
65
|
// or more ideally could use context to register if it has a scrollview and change behavior
|
|
46
66
|
// const offscreenSize = useSheetOffscreenSize(context)
|
|
47
67
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
// actions-sheet pattern: track scroll offset continuously via ref
|
|
69
|
+
// this is updated on EVERY scroll event so it's always current
|
|
70
|
+
const currentScrollOffset = useRef(0)
|
|
71
|
+
// the position to lock at when scroll is disabled
|
|
72
|
+
const lockedScrollY = useRef(0)
|
|
73
|
+
|
|
74
|
+
const setScrollEnabled = (next: boolean, lockTo?: number) => {
|
|
75
|
+
// console.warn('[RNGH-Scroll] setScrollEnabled', next, 'currentOffset:', currentScrollOffset.current, 'lockTo:', lockTo)
|
|
76
|
+
if (!next) {
|
|
77
|
+
// locking scroll - freeze at specified position or CURRENT position
|
|
78
|
+
// if lockTo is provided (e.g., 0), use that; otherwise freeze at current
|
|
79
|
+
// key insight: we use currentScrollOffset which is updated every onScroll
|
|
80
|
+
// this ensures we freeze at the actual position, not a stale value
|
|
81
|
+
const lockY = lockTo ?? currentScrollOffset.current
|
|
82
|
+
lockedScrollY.current = lockY
|
|
83
|
+
scrollBridge.scrollLockY = lockY
|
|
84
|
+
// console.warn('[RNGH-Scroll] LOCKING at', lockY)
|
|
85
|
+
// immediately scroll to lock position
|
|
86
|
+
scrollRef.current?.scrollTo?.({
|
|
87
|
+
x: 0,
|
|
88
|
+
y: lockY,
|
|
89
|
+
animated: false,
|
|
90
|
+
})
|
|
91
|
+
} else {
|
|
92
|
+
// unlocking - save current position before clearing lock
|
|
93
|
+
lockedScrollY.current = currentScrollOffset.current
|
|
94
|
+
scrollBridge.scrollLockY = undefined
|
|
95
|
+
// console.warn('[RNGH-Scroll] UNLOCKING at', lockedScrollY.current)
|
|
96
|
+
}
|
|
97
|
+
// NOTE: intentionally NOT using setNativeProps or scrollEnabled state
|
|
98
|
+
// because that kills the RNGH scroll gesture mid-touch, breaking handoff.
|
|
99
|
+
// This is the react-native-actions-sheet pattern: both gestures run,
|
|
100
|
+
// we use scrollLockY + scrollTo to "freeze" scroll position during pan.
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// force scroll to specific position (called from pan gesture to compensate for async props)
|
|
104
|
+
const forceScrollTo = (y: number) => {
|
|
105
|
+
// console.warn('[RNGH-Scroll] forceScrollTo:', y)
|
|
106
|
+
scrollRef.current?.scrollTo?.({
|
|
107
|
+
x: 0,
|
|
108
|
+
y,
|
|
109
|
+
animated: false,
|
|
51
110
|
})
|
|
52
|
-
setScrollEnabled_(next)
|
|
53
111
|
}
|
|
54
112
|
|
|
55
113
|
const state = React.useRef({
|
|
@@ -62,8 +120,21 @@ export const SheetScrollView = React.forwardRef<
|
|
|
62
120
|
|
|
63
121
|
useEffect(() => {
|
|
64
122
|
setHasScrollView(true)
|
|
123
|
+
|
|
124
|
+
// register setScrollEnabled on scrollBridge for RNGH coordination
|
|
125
|
+
if (isGestureHandlerEnabled()) {
|
|
126
|
+
scrollBridge.setScrollEnabled = setScrollEnabled
|
|
127
|
+
scrollBridge.forceScrollTo = forceScrollTo
|
|
128
|
+
}
|
|
129
|
+
|
|
65
130
|
return () => {
|
|
66
131
|
setHasScrollView(false)
|
|
132
|
+
if (scrollBridge.setScrollEnabled) {
|
|
133
|
+
scrollBridge.setScrollEnabled = undefined
|
|
134
|
+
}
|
|
135
|
+
if (scrollBridge.forceScrollTo) {
|
|
136
|
+
scrollBridge.forceScrollTo = undefined
|
|
137
|
+
}
|
|
67
138
|
}
|
|
68
139
|
}, [])
|
|
69
140
|
|
|
@@ -141,7 +212,8 @@ export const SheetScrollView = React.forwardRef<
|
|
|
141
212
|
|
|
142
213
|
const setIsScrollable = () => {
|
|
143
214
|
if (parentHeight.current && contentHeight.current) {
|
|
144
|
-
|
|
215
|
+
const isScrollable = contentHeight.current > parentHeight.current
|
|
216
|
+
setHasScrollableContent(isScrollable)
|
|
145
217
|
}
|
|
146
218
|
}
|
|
147
219
|
|
|
@@ -149,7 +221,85 @@ export const SheetScrollView = React.forwardRef<
|
|
|
149
221
|
scrollBridge.hasScrollableContent = hasScrollableContent
|
|
150
222
|
}, [hasScrollableContent])
|
|
151
223
|
|
|
152
|
-
|
|
224
|
+
// Use RNGH ScrollView with simultaneousHandlers for native-quality gesture coordination
|
|
225
|
+
// Pattern from react-native-actions-sheet: pass panGestureRef to simultaneousHandlers
|
|
226
|
+
if (useRNGHScrollView && RNGHScrollView && panGestureRef) {
|
|
227
|
+
const RNGHComponent = RNGHScrollView as any
|
|
228
|
+
// console.warn('[RNGH-Scroll] RENDER with simultaneousHandlers')
|
|
229
|
+
return (
|
|
230
|
+
<RNGHComponent
|
|
231
|
+
ref={composeRefs(scrollRef as any, ref)}
|
|
232
|
+
style={{ flex: 1 }}
|
|
233
|
+
scrollEventThrottle={1}
|
|
234
|
+
scrollEnabled={scrollable}
|
|
235
|
+
simultaneousHandlers={[panGestureRef]}
|
|
236
|
+
onLayout={(e: any) => {
|
|
237
|
+
parentHeight.current = Math.ceil(e.nativeEvent.layout.height)
|
|
238
|
+
setIsScrollable()
|
|
239
|
+
}}
|
|
240
|
+
onScroll={(e: any) => {
|
|
241
|
+
const { y } = e.nativeEvent.contentOffset
|
|
242
|
+
|
|
243
|
+
// actions-sheet pattern: ALWAYS track current offset
|
|
244
|
+
// this ensures setScrollEnabled knows the exact current position
|
|
245
|
+
currentScrollOffset.current = y
|
|
246
|
+
|
|
247
|
+
// if scroll is locked, force it back to lock position
|
|
248
|
+
if (scrollBridge.scrollLockY !== undefined) {
|
|
249
|
+
if (y !== scrollBridge.scrollLockY) {
|
|
250
|
+
scrollRef.current?.scrollTo?.({
|
|
251
|
+
x: 0,
|
|
252
|
+
y: scrollBridge.scrollLockY,
|
|
253
|
+
animated: false,
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
// still update bridge y to the lock position for consistency
|
|
257
|
+
scrollBridge.y = scrollBridge.scrollLockY
|
|
258
|
+
// fire callback but with locked position (for UI updates)
|
|
259
|
+
const lockedEvent = {
|
|
260
|
+
...e,
|
|
261
|
+
nativeEvent: {
|
|
262
|
+
...e.nativeEvent,
|
|
263
|
+
contentOffset: {
|
|
264
|
+
...e.nativeEvent.contentOffset,
|
|
265
|
+
y: scrollBridge.scrollLockY,
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
onScroll?.(lockedEvent)
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// console.warn('[RNGH-Scroll] y:', y)
|
|
274
|
+
scrollBridge.y = y
|
|
275
|
+
if (y > 0) {
|
|
276
|
+
scrollBridge.scrollStartY = -1
|
|
277
|
+
}
|
|
278
|
+
onScroll?.(e)
|
|
279
|
+
}}
|
|
280
|
+
contentContainerStyle={{ minHeight: '100%' }}
|
|
281
|
+
bounces={false}
|
|
282
|
+
{...props}
|
|
283
|
+
>
|
|
284
|
+
{/* wrapper to measure actual content height (not min-height expanded) */}
|
|
285
|
+
<View
|
|
286
|
+
onLayout={(e) => {
|
|
287
|
+
const height = Math.floor(e.nativeEvent.layout.height)
|
|
288
|
+
// only update if different to avoid loops
|
|
289
|
+
if (height !== contentHeight.current) {
|
|
290
|
+
contentHeight.current = height
|
|
291
|
+
setIsScrollable()
|
|
292
|
+
}
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
{children}
|
|
296
|
+
</View>
|
|
297
|
+
</RNGHComponent>
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Fallback: regular Tamagui ScrollView for web or when RNGH not available
|
|
302
|
+
const content = (
|
|
153
303
|
<ScrollView
|
|
154
304
|
onLayout={(e) => {
|
|
155
305
|
parentHeight.current = Math.ceil(e.nativeEvent.layout.height)
|
|
@@ -157,7 +307,7 @@ export const SheetScrollView = React.forwardRef<
|
|
|
157
307
|
}}
|
|
158
308
|
ref={composeRefs(scrollRef as any, ref)}
|
|
159
309
|
flex={1}
|
|
160
|
-
scrollEventThrottle={
|
|
310
|
+
scrollEventThrottle={1}
|
|
161
311
|
onResponderRelease={release}
|
|
162
312
|
className="_ovs-contain"
|
|
163
313
|
scrollEnabled={scrollable}
|
|
@@ -175,7 +325,6 @@ export const SheetScrollView = React.forwardRef<
|
|
|
175
325
|
scrollBridge.scrollStartY = -1
|
|
176
326
|
}
|
|
177
327
|
|
|
178
|
-
// Process the "onScroll" prop here if any
|
|
179
328
|
onScroll?.(e)
|
|
180
329
|
|
|
181
330
|
// This assures that we do not skip the "scrollBridge" values processing
|
|
@@ -256,21 +405,21 @@ export const SheetScrollView = React.forwardRef<
|
|
|
256
405
|
}}
|
|
257
406
|
{...props}
|
|
258
407
|
>
|
|
259
|
-
{/* content height
|
|
408
|
+
{/* wrapper to measure actual content height */}
|
|
260
409
|
<View
|
|
261
|
-
position="absolute"
|
|
262
|
-
inset={0}
|
|
263
|
-
pointerEvents="none"
|
|
264
|
-
zIndex={-1}
|
|
265
410
|
onLayout={(e) => {
|
|
266
|
-
|
|
267
|
-
contentHeight.current
|
|
268
|
-
|
|
411
|
+
const height = Math.floor(e.nativeEvent.layout.height)
|
|
412
|
+
if (height !== contentHeight.current) {
|
|
413
|
+
contentHeight.current = height
|
|
414
|
+
setIsScrollable()
|
|
415
|
+
}
|
|
269
416
|
}}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
417
|
+
>
|
|
418
|
+
{children}
|
|
419
|
+
</View>
|
|
273
420
|
</ScrollView>
|
|
274
421
|
)
|
|
422
|
+
|
|
423
|
+
return content
|
|
275
424
|
}
|
|
276
425
|
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export gesture state from @tamagui/native.
|
|
3
|
+
* Sheet uses this for backward compatibility with existing code.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getGestureHandler, type GestureState } from '@tamagui/native'
|
|
7
|
+
|
|
8
|
+
export type { GestureState as GestureHandlerState } from '@tamagui/native'
|
|
9
|
+
|
|
10
|
+
// backward compat helpers
|
|
11
|
+
export function isGestureHandlerEnabled(): boolean {
|
|
12
|
+
return getGestureHandler().isEnabled
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getGestureHandlerState(): GestureState {
|
|
16
|
+
return getGestureHandler().state
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function setGestureHandlerState(updates: Partial<GestureState>): void {
|
|
20
|
+
getGestureHandler().set(updates)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// alias for backward compatibility
|
|
24
|
+
export const setGestureState = setGestureHandlerState
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy setup - prefer `import '@tamagui/native/setup-gesture-handler'` instead.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getGestureHandler } from '@tamagui/native'
|
|
6
|
+
|
|
7
|
+
export function isGestureHandlerEnabled(): boolean {
|
|
8
|
+
return getGestureHandler().isEnabled
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SetupGestureHandlerConfig {
|
|
12
|
+
Gesture: any
|
|
13
|
+
GestureDetector: any
|
|
14
|
+
ScrollView?: any
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function setupGestureHandler(config: SetupGestureHandlerConfig): void {
|
|
18
|
+
const g = globalThis as any
|
|
19
|
+
if (g.__tamagui_sheet_gesture_handler_setup) {
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
g.__tamagui_sheet_gesture_handler_setup = true
|
|
23
|
+
|
|
24
|
+
const { Gesture, GestureDetector, ScrollView } = config
|
|
25
|
+
|
|
26
|
+
if (Gesture && GestureDetector) {
|
|
27
|
+
getGestureHandler().set({
|
|
28
|
+
enabled: true,
|
|
29
|
+
Gesture,
|
|
30
|
+
GestureDetector,
|
|
31
|
+
ScrollView: ScrollView || null,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/types.tsx
CHANGED
|
@@ -109,4 +109,19 @@ export type ScrollBridge = {
|
|
|
109
109
|
onParentDragging: (props: (val: boolean) => void) => () => void
|
|
110
110
|
setParentDragging: (val: boolean) => void
|
|
111
111
|
onFinishAnimate?: () => void
|
|
112
|
+
// gesture handler state for RNGH integration
|
|
113
|
+
blockPan?: boolean
|
|
114
|
+
initialPosition?: number
|
|
115
|
+
isScrollablePositionLocked?: boolean
|
|
116
|
+
// control scroll enabled state for RNGH coordination
|
|
117
|
+
// lockTo parameter: when disabling, lock scroll to this position (undefined = current position)
|
|
118
|
+
setScrollEnabled?: (enabled: boolean, lockTo?: number) => void
|
|
119
|
+
// track touch position for direction detection in RNGH
|
|
120
|
+
_lastTouchY?: number
|
|
121
|
+
// scroll lock position for forcing scroll back when pan handles
|
|
122
|
+
scrollLockY?: number
|
|
123
|
+
// force scroll to position (compensates for async setNativeProps)
|
|
124
|
+
forceScrollTo?: (y: number) => void
|
|
125
|
+
// whether sheet is at top position (for scroll enable/disable)
|
|
126
|
+
isAtTop?: boolean
|
|
112
127
|
}
|