@tamagui/sheet 2.0.0-1769233344020 → 2.0.0-1769256574467

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/cjs/GestureDetectorWrapper.cjs +48 -0
  2. package/dist/cjs/GestureDetectorWrapper.js +29 -0
  3. package/dist/cjs/GestureDetectorWrapper.js.map +6 -0
  4. package/dist/cjs/GestureDetectorWrapper.native.js +53 -0
  5. package/dist/cjs/GestureDetectorWrapper.native.js.map +1 -0
  6. package/dist/cjs/GestureSheetContext.cjs +52 -0
  7. package/dist/cjs/GestureSheetContext.js +43 -0
  8. package/dist/cjs/GestureSheetContext.js.map +6 -0
  9. package/dist/cjs/GestureSheetContext.native.js +56 -0
  10. package/dist/cjs/GestureSheetContext.native.js.map +1 -0
  11. package/dist/cjs/SheetImplementationCustom.cjs +85 -40
  12. package/dist/cjs/SheetImplementationCustom.js +81 -58
  13. package/dist/cjs/SheetImplementationCustom.js.map +1 -1
  14. package/dist/cjs/SheetImplementationCustom.native.js +105 -50
  15. package/dist/cjs/SheetImplementationCustom.native.js.map +1 -1
  16. package/dist/cjs/SheetScrollView.cjs +80 -8
  17. package/dist/cjs/SheetScrollView.js +76 -10
  18. package/dist/cjs/SheetScrollView.js.map +1 -1
  19. package/dist/cjs/SheetScrollView.native.js +94 -10
  20. package/dist/cjs/SheetScrollView.native.js.map +1 -1
  21. package/dist/cjs/gestureState.cjs +31 -0
  22. package/dist/cjs/gestureState.js +24 -0
  23. package/dist/cjs/gestureState.js.map +6 -0
  24. package/dist/cjs/gestureState.native.js +34 -0
  25. package/dist/cjs/gestureState.native.js.map +1 -0
  26. package/dist/cjs/setupGestureHandler.cjs +43 -0
  27. package/dist/cjs/setupGestureHandler.js +35 -0
  28. package/dist/cjs/setupGestureHandler.js.map +6 -0
  29. package/dist/cjs/setupGestureHandler.native.js +47 -0
  30. package/dist/cjs/setupGestureHandler.native.js.map +1 -0
  31. package/dist/cjs/useGestureHandlerPan.cjs +125 -0
  32. package/dist/cjs/useGestureHandlerPan.js +116 -0
  33. package/dist/cjs/useGestureHandlerPan.js.map +6 -0
  34. package/dist/cjs/useGestureHandlerPan.native.js +134 -0
  35. package/dist/cjs/useGestureHandlerPan.native.js.map +1 -0
  36. package/dist/esm/GestureDetectorWrapper.js +15 -0
  37. package/dist/esm/GestureDetectorWrapper.js.map +6 -0
  38. package/dist/esm/GestureDetectorWrapper.mjs +25 -0
  39. package/dist/esm/GestureDetectorWrapper.mjs.map +1 -0
  40. package/dist/esm/GestureDetectorWrapper.native.js +27 -0
  41. package/dist/esm/GestureDetectorWrapper.native.js.map +1 -0
  42. package/dist/esm/GestureSheetContext.js +28 -0
  43. package/dist/esm/GestureSheetContext.js.map +6 -0
  44. package/dist/esm/GestureSheetContext.mjs +28 -0
  45. package/dist/esm/GestureSheetContext.mjs.map +1 -0
  46. package/dist/esm/GestureSheetContext.native.js +29 -0
  47. package/dist/esm/GestureSheetContext.native.js.map +1 -0
  48. package/dist/esm/SheetImplementationCustom.js +83 -57
  49. package/dist/esm/SheetImplementationCustom.js.map +1 -1
  50. package/dist/esm/SheetImplementationCustom.mjs +85 -40
  51. package/dist/esm/SheetImplementationCustom.mjs.map +1 -1
  52. package/dist/esm/SheetImplementationCustom.native.js +105 -50
  53. package/dist/esm/SheetImplementationCustom.native.js.map +1 -1
  54. package/dist/esm/SheetScrollView.js +77 -9
  55. package/dist/esm/SheetScrollView.js.map +1 -1
  56. package/dist/esm/SheetScrollView.mjs +80 -8
  57. package/dist/esm/SheetScrollView.mjs.map +1 -1
  58. package/dist/esm/SheetScrollView.native.js +94 -10
  59. package/dist/esm/SheetScrollView.native.js.map +1 -1
  60. package/dist/esm/gestureState.js +13 -0
  61. package/dist/esm/gestureState.js.map +6 -0
  62. package/dist/esm/gestureState.mjs +5 -0
  63. package/dist/esm/gestureState.mjs.map +1 -0
  64. package/dist/esm/gestureState.native.js +5 -0
  65. package/dist/esm/gestureState.native.js.map +1 -0
  66. package/dist/esm/setupGestureHandler.js +19 -0
  67. package/dist/esm/setupGestureHandler.js.map +6 -0
  68. package/dist/esm/setupGestureHandler.mjs +19 -0
  69. package/dist/esm/setupGestureHandler.mjs.map +1 -0
  70. package/dist/esm/setupGestureHandler.native.js +20 -0
  71. package/dist/esm/setupGestureHandler.native.js.map +1 -0
  72. package/dist/esm/useGestureHandlerPan.js +101 -0
  73. package/dist/esm/useGestureHandlerPan.js.map +6 -0
  74. package/dist/esm/useGestureHandlerPan.mjs +102 -0
  75. package/dist/esm/useGestureHandlerPan.mjs.map +1 -0
  76. package/dist/esm/useGestureHandlerPan.native.js +108 -0
  77. package/dist/esm/useGestureHandlerPan.native.js.map +1 -0
  78. package/dist/jsx/GestureDetectorWrapper.js +15 -0
  79. package/dist/jsx/GestureDetectorWrapper.js.map +6 -0
  80. package/dist/jsx/GestureDetectorWrapper.mjs +25 -0
  81. package/dist/jsx/GestureDetectorWrapper.mjs.map +1 -0
  82. package/dist/jsx/GestureDetectorWrapper.native.js +53 -0
  83. package/dist/jsx/GestureDetectorWrapper.native.js.map +1 -0
  84. package/dist/jsx/GestureSheetContext.js +28 -0
  85. package/dist/jsx/GestureSheetContext.js.map +6 -0
  86. package/dist/jsx/GestureSheetContext.mjs +28 -0
  87. package/dist/jsx/GestureSheetContext.mjs.map +1 -0
  88. package/dist/jsx/GestureSheetContext.native.js +56 -0
  89. package/dist/jsx/GestureSheetContext.native.js.map +1 -0
  90. package/dist/jsx/SheetImplementationCustom.js +83 -57
  91. package/dist/jsx/SheetImplementationCustom.js.map +1 -1
  92. package/dist/jsx/SheetImplementationCustom.mjs +85 -40
  93. package/dist/jsx/SheetImplementationCustom.mjs.map +1 -1
  94. package/dist/jsx/SheetImplementationCustom.native.js +105 -50
  95. package/dist/jsx/SheetImplementationCustom.native.js.map +1 -1
  96. package/dist/jsx/SheetScrollView.js +77 -9
  97. package/dist/jsx/SheetScrollView.js.map +1 -1
  98. package/dist/jsx/SheetScrollView.mjs +80 -8
  99. package/dist/jsx/SheetScrollView.mjs.map +1 -1
  100. package/dist/jsx/SheetScrollView.native.js +94 -10
  101. package/dist/jsx/SheetScrollView.native.js.map +1 -1
  102. package/dist/jsx/gestureState.js +13 -0
  103. package/dist/jsx/gestureState.js.map +6 -0
  104. package/dist/jsx/gestureState.mjs +5 -0
  105. package/dist/jsx/gestureState.mjs.map +1 -0
  106. package/dist/jsx/gestureState.native.js +34 -0
  107. package/dist/jsx/gestureState.native.js.map +1 -0
  108. package/dist/jsx/setupGestureHandler.js +19 -0
  109. package/dist/jsx/setupGestureHandler.js.map +6 -0
  110. package/dist/jsx/setupGestureHandler.mjs +19 -0
  111. package/dist/jsx/setupGestureHandler.mjs.map +1 -0
  112. package/dist/jsx/setupGestureHandler.native.js +47 -0
  113. package/dist/jsx/setupGestureHandler.native.js.map +1 -0
  114. package/dist/jsx/useGestureHandlerPan.js +101 -0
  115. package/dist/jsx/useGestureHandlerPan.js.map +6 -0
  116. package/dist/jsx/useGestureHandlerPan.mjs +102 -0
  117. package/dist/jsx/useGestureHandlerPan.mjs.map +1 -0
  118. package/dist/jsx/useGestureHandlerPan.native.js +134 -0
  119. package/dist/jsx/useGestureHandlerPan.native.js.map +1 -0
  120. package/package.json +48 -21
  121. package/src/GestureDetectorWrapper.tsx +41 -0
  122. package/src/GestureSheetContext.tsx +62 -0
  123. package/src/SheetImplementationCustom.tsx +124 -57
  124. package/src/SheetScrollView.tsx +157 -8
  125. package/src/gestureState.ts +17 -0
  126. package/src/setupGestureHandler.ts +32 -0
  127. package/src/types.tsx +15 -0
  128. package/src/useGestureHandlerPan.tsx +299 -0
  129. package/types/GestureDetectorWrapper.d.ts +14 -0
  130. package/types/GestureDetectorWrapper.d.ts.map +1 -0
  131. package/types/GestureDetectorWrapper.native.d.ts +14 -0
  132. package/types/GestureDetectorWrapper.native.d.ts.map +1 -0
  133. package/types/GestureSheetContext.d.ts +36 -0
  134. package/types/GestureSheetContext.d.ts.map +1 -0
  135. package/types/SheetImplementationCustom.d.ts.map +1 -1
  136. package/types/SheetScrollView.d.ts.map +1 -1
  137. package/types/gestureState.d.ts +9 -0
  138. package/types/gestureState.d.ts.map +1 -0
  139. package/types/gestureState.native.d.ts +12 -0
  140. package/types/gestureState.native.d.ts.map +1 -0
  141. package/types/setupGestureHandler.d.ts +12 -0
  142. package/types/setupGestureHandler.d.ts.map +1 -0
  143. package/types/setupGestureHandler.native.d.ts +41 -0
  144. package/types/setupGestureHandler.native.d.ts.map +1 -0
  145. package/types/types.d.ts +8 -0
  146. package/types/types.d.ts.map +1 -1
  147. package/types/useGestureHandlerPan.d.ts +43 -0
  148. package/types/useGestureHandlerPan.d.ts.map +1 -0
  149. package/types/useGestureHandlerPan.native.d.ts +33 -0
  150. package/types/useGestureHandlerPan.native.d.ts.map +1 -0
@@ -0,0 +1,62 @@
1
+ import type React from 'react'
2
+ import { createContext, useContext, type RefObject } from 'react'
3
+
4
+ export interface GestureSheetContextValue {
5
+ /**
6
+ * The sheet's pan gesture object, used for simultaneousHandlers
7
+ * in ScrollView to coordinate gestures
8
+ */
9
+ panGesture: any | null
10
+ /**
11
+ * Ref to the pan gesture for simultaneousHandlers prop
12
+ */
13
+ panGestureRef: RefObject<any> | null
14
+ /**
15
+ * Whether the sheet is currently being dragged by the user
16
+ */
17
+ isDragging: boolean
18
+ /**
19
+ * Set whether panning should be blocked (e.g., when scrolling)
20
+ */
21
+ setBlockPan: (blocked: boolean) => void
22
+ /**
23
+ * Whether pan gesture is currently blocked
24
+ */
25
+ blockPan: boolean
26
+ }
27
+
28
+ const GestureSheetContext = createContext<GestureSheetContextValue | null>(null)
29
+
30
+ export function useGestureSheetContext(): GestureSheetContextValue | null {
31
+ return useContext(GestureSheetContext)
32
+ }
33
+
34
+ export interface GestureSheetProviderProps {
35
+ children: React.ReactNode
36
+ isDragging: boolean
37
+ blockPan: boolean
38
+ setBlockPan: (blocked: boolean) => void
39
+ panGesture: any | null
40
+ panGestureRef: RefObject<any> | null
41
+ }
42
+
43
+ export function GestureSheetProvider({
44
+ children,
45
+ isDragging,
46
+ blockPan,
47
+ setBlockPan,
48
+ panGesture,
49
+ panGestureRef,
50
+ }: GestureSheetProviderProps) {
51
+ const value: GestureSheetContextValue = {
52
+ panGesture,
53
+ panGestureRef,
54
+ isDragging,
55
+ blockPan,
56
+ setBlockPan,
57
+ }
58
+
59
+ return (
60
+ <GestureSheetContext.Provider value={value}>{children}</GestureSheetContext.Provider>
61
+ )
62
+ }
@@ -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
- // FIX: Use stable frameSize when closing to prevent position jumps
138
- getYPositions(
139
- snapPointsMode,
140
- point,
141
- screenSize,
142
- open ? frameSize : stableFrameSize.current
143
- )
144
+ getYPositions(snapPointsMode, point, screenSize, effectiveFrameSize)
144
145
  ),
145
- [screenSize, frameSize, snapPoints, snapPointsMode, open]
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
- <AnimatePresence custom={{ open }}>
527
- {shouldHideParentSheet || !open ? null : overlayComponent}
528
- </AnimatePresence>
529
-
530
- {snapPointsMode !== 'percent' && (
531
- <View
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
- {/* <AdaptProvider>{props.children}</AdaptProvider> */}
570
- {props.children}
571
- </AnimatedView>
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) return 0
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
- const next = Math.round(screenSize - pct * screenSize)
642
- return next
709
+ return Math.round(screenSize - pct * screenSize)
643
710
  }
644
711
  console.warn('Invalid snapPoint unknown value')
645
712
  return 0
@@ -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
- // TODO ideally would replicate https://github.com/ammarahm-ed/react-native-actions-sheet/blob/master/src/index.tsx
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
- const setScrollEnabled = (next: boolean) => {
49
- scrollRef.current?.setNativeProps?.({
50
- scrollEnabled: next,
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
 
@@ -149,7 +220,84 @@ export const SheetScrollView = React.forwardRef<
149
220
  scrollBridge.hasScrollableContent = hasScrollableContent
150
221
  }, [hasScrollableContent])
151
222
 
152
- return (
223
+ // Use RNGH ScrollView with simultaneousHandlers for native-quality gesture coordination
224
+ // Pattern from react-native-actions-sheet: pass panGestureRef to simultaneousHandlers
225
+ if (useRNGHScrollView && RNGHScrollView && panGestureRef) {
226
+ const RNGHComponent = RNGHScrollView as any
227
+ // console.warn('[RNGH-Scroll] RENDER with simultaneousHandlers')
228
+ return (
229
+ <RNGHComponent
230
+ ref={composeRefs(scrollRef as any, ref)}
231
+ style={{ flex: 1 }}
232
+ scrollEventThrottle={1}
233
+ scrollEnabled={scrollable}
234
+ simultaneousHandlers={[panGestureRef]}
235
+ onLayout={(e: any) => {
236
+ parentHeight.current = Math.ceil(e.nativeEvent.layout.height)
237
+ setIsScrollable()
238
+ }}
239
+ onScroll={(e: any) => {
240
+ const { y } = e.nativeEvent.contentOffset
241
+
242
+ // actions-sheet pattern: ALWAYS track current offset
243
+ // this ensures setScrollEnabled knows the exact current position
244
+ currentScrollOffset.current = y
245
+
246
+ // if scroll is locked, force it back to lock position
247
+ if (scrollBridge.scrollLockY !== undefined) {
248
+ if (y !== scrollBridge.scrollLockY) {
249
+ scrollRef.current?.scrollTo?.({
250
+ x: 0,
251
+ y: scrollBridge.scrollLockY,
252
+ animated: false,
253
+ })
254
+ }
255
+ // still update bridge y to the lock position for consistency
256
+ scrollBridge.y = scrollBridge.scrollLockY
257
+ // fire callback but with locked position (for UI updates)
258
+ const lockedEvent = {
259
+ ...e,
260
+ nativeEvent: {
261
+ ...e.nativeEvent,
262
+ contentOffset: {
263
+ ...e.nativeEvent.contentOffset,
264
+ y: scrollBridge.scrollLockY,
265
+ },
266
+ },
267
+ }
268
+ onScroll?.(lockedEvent)
269
+ return
270
+ }
271
+
272
+ // console.warn('[RNGH-Scroll] y:', y)
273
+ scrollBridge.y = y
274
+ if (y > 0) {
275
+ scrollBridge.scrollStartY = -1
276
+ }
277
+ onScroll?.(e)
278
+ }}
279
+ contentContainerStyle={{ minHeight: '100%' }}
280
+ bounces={false}
281
+ {...props}
282
+ >
283
+ {/* content height measurer */}
284
+ <View
285
+ position="absolute"
286
+ inset={0}
287
+ pointerEvents="none"
288
+ zIndex={-1}
289
+ onLayout={(e) => {
290
+ contentHeight.current = Math.floor(e.nativeEvent.layout.height)
291
+ setIsScrollable()
292
+ }}
293
+ />
294
+ {children}
295
+ </RNGHComponent>
296
+ )
297
+ }
298
+
299
+ // Fallback: regular Tamagui ScrollView for web or when RNGH not available
300
+ const content = (
153
301
  <ScrollView
154
302
  onLayout={(e) => {
155
303
  parentHeight.current = Math.ceil(e.nativeEvent.layout.height)
@@ -157,7 +305,7 @@ export const SheetScrollView = React.forwardRef<
157
305
  }}
158
306
  ref={composeRefs(scrollRef as any, ref)}
159
307
  flex={1}
160
- scrollEventThrottle={8}
308
+ scrollEventThrottle={1}
161
309
  onResponderRelease={release}
162
310
  className="_ovs-contain"
163
311
  scrollEnabled={scrollable}
@@ -175,7 +323,6 @@ export const SheetScrollView = React.forwardRef<
175
323
  scrollBridge.scrollStartY = -1
176
324
  }
177
325
 
178
- // Process the "onScroll" prop here if any
179
326
  onScroll?.(e)
180
327
 
181
328
  // This assures that we do not skip the "scrollBridge" values processing
@@ -272,5 +419,7 @@ export const SheetScrollView = React.forwardRef<
272
419
  {children}
273
420
  </ScrollView>
274
421
  )
422
+
423
+ return content
275
424
  }
276
425
  )
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Re-export gesture state from @tamagui/native.
3
+ * Sheet uses this for backward compatibility with existing code.
4
+ */
5
+
6
+ export {
7
+ isGestureHandlerEnabled,
8
+ getGestureHandlerState,
9
+ } from '@tamagui/native'
10
+
11
+ export type { GestureState as GestureHandlerState } from '@tamagui/native'
12
+
13
+ // re-export setGestureHandlerState for backward compat with setupGestureHandler
14
+ export { setGestureHandlerState } from '@tamagui/native'
15
+
16
+ // alias for backward compatibility
17
+ export { setGestureHandlerState as setGestureState } from '@tamagui/native'
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Legacy setup - prefer `import '@tamagui/native/setup-gesture-handler'` instead.
3
+ */
4
+
5
+ import { setGestureHandlerState, isGestureHandlerEnabled } from '@tamagui/native'
6
+
7
+ export { isGestureHandlerEnabled }
8
+
9
+ export interface SetupGestureHandlerConfig {
10
+ Gesture: any
11
+ GestureDetector: any
12
+ ScrollView?: any
13
+ }
14
+
15
+ export function setupGestureHandler(config: SetupGestureHandlerConfig): void {
16
+ const g = globalThis as any
17
+ if (g.__tamagui_sheet_gesture_handler_setup) {
18
+ return
19
+ }
20
+ g.__tamagui_sheet_gesture_handler_setup = true
21
+
22
+ const { Gesture, GestureDetector, ScrollView } = config
23
+
24
+ if (Gesture && GestureDetector) {
25
+ setGestureHandlerState({
26
+ enabled: true,
27
+ Gesture,
28
+ GestureDetector,
29
+ ScrollView: ScrollView || null,
30
+ })
31
+ }
32
+ }
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
  }