@lodev09/react-native-true-sheet 3.6.9 → 3.6.11

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/README.md CHANGED
@@ -31,6 +31,7 @@ The true native bottom sheet experience for your React Native Apps. 💩
31
31
 
32
32
  - React Native >= 0.76 (Expo SDK 52+)
33
33
  - New Architecture enabled (default in RN 0.76+)
34
+ - Xcode 26.2 (strongly recommended for better library functionality)
34
35
 
35
36
  ### Expo
36
37
 
@@ -45,6 +46,27 @@ yarn add @lodev09/react-native-true-sheet
45
46
  cd ios && pod install
46
47
  ```
47
48
 
49
+ ### EAS Build (iOS)
50
+
51
+ When using [EAS Build](https://docs.expo.dev/build/introduction/) to build your iOS app, you must configure your `eas.json` to use a build image that includes Xcode 26.2. Use `"image": "latest"` or choose from the [available build images](https://docs.expo.dev/build-reference/infrastructure/#ios-server-images):
52
+
53
+ ```json
54
+ {
55
+ "build": {
56
+ "production": {
57
+ "ios": {
58
+ "image": "latest"
59
+ }
60
+ },
61
+ "development": {
62
+ "ios": {
63
+ "image": "latest"
64
+ }
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
48
70
  ## Documentation
49
71
 
50
72
  - [Example](example)
@@ -3,9 +3,11 @@ package com.lodev09.truesheet
3
3
  import android.annotation.SuppressLint
4
4
  import android.view.View
5
5
  import com.facebook.react.uimanager.ThemedReactContext
6
+ import com.facebook.react.uimanager.events.EventDispatcher
6
7
  import com.facebook.react.views.view.ReactViewGroup
7
8
 
8
9
  interface TrueSheetContainerViewDelegate {
10
+ val eventDispatcher: EventDispatcher?
9
11
  fun containerViewContentDidChangeSize(width: Int, height: Int)
10
12
  fun containerViewHeaderDidChangeSize(width: Int, height: Int)
11
13
  fun containerViewFooterDidChangeSize(width: Int, height: Int)
@@ -32,6 +34,9 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
32
34
  var headerHeight: Int = 0
33
35
  var footerHeight: Int = 0
34
36
 
37
+ override val eventDispatcher: EventDispatcher?
38
+ get() = delegate?.eventDispatcher
39
+
35
40
  init {
36
41
  // Allow footer to position outside container bounds
37
42
  clipChildren = false
@@ -11,10 +11,11 @@ import com.facebook.react.uimanager.events.EventDispatcher
11
11
  import com.facebook.react.views.view.ReactViewGroup
12
12
 
13
13
  /**
14
- * Delegate interface for footer view size changes
14
+ * Delegate interface for footer view size changes and event dispatching
15
15
  */
16
16
  interface TrueSheetFooterViewDelegate {
17
17
  fun footerViewDidChangeSize(width: Int, height: Int)
18
+ val eventDispatcher: EventDispatcher?
18
19
  }
19
20
 
20
21
  /**
@@ -30,7 +31,9 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
30
31
  RootView {
31
32
 
32
33
  var delegate: TrueSheetFooterViewDelegate? = null
33
- var eventDispatcher: EventDispatcher? = null
34
+
35
+ private val eventDispatcher: EventDispatcher?
36
+ get() = delegate?.eventDispatcher
34
37
 
35
38
  private var lastWidth = 0
36
39
  private var lastHeight = 0
@@ -42,7 +42,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
42
42
  private val containerView: TrueSheetContainerView?
43
43
  get() = viewController.getChildAt(0) as? TrueSheetContainerView
44
44
 
45
- var eventDispatcher: EventDispatcher? = null
45
+ override var eventDispatcher: EventDispatcher? = null
46
46
 
47
47
  // Initial present configuration (set by ViewManager before mount)
48
48
  var initialDetentIndex: Int = -1
@@ -248,6 +248,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
248
248
  viewController.insetAdjustment = insetAdjustment
249
249
  }
250
250
 
251
+ fun setScrollable(scrollable: Boolean) {
252
+ viewController.scrollable = scrollable
253
+ }
254
+
251
255
  // ==================== State Management ====================
252
256
 
253
257
  /**
@@ -380,10 +384,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
380
384
  override fun viewControllerWillPresent(index: Int, position: Float, detent: Float) {
381
385
  val surfaceId = UIManagerHelper.getSurfaceId(this)
382
386
  eventDispatcher?.dispatchEvent(WillPresentEvent(surfaceId, id, index, position, detent))
383
-
384
- // Enable touch event dispatching to React Native while sheet is visible
385
- viewController.eventDispatcher = eventDispatcher
386
- containerView?.footerView?.eventDispatcher = eventDispatcher
387
387
  }
388
388
 
389
389
  override fun viewControllerDidPresent(index: Int, position: Float, detent: Float) {
@@ -394,10 +394,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
394
394
  override fun viewControllerWillDismiss() {
395
395
  val surfaceId = UIManagerHelper.getSurfaceId(this)
396
396
  eventDispatcher?.dispatchEvent(WillDismissEvent(surfaceId, id))
397
-
398
- // Disable touch event dispatching when sheet is dismissing
399
- viewController.eventDispatcher = null
400
- containerView?.footerView?.eventDispatcher = null
401
397
  }
402
398
 
403
399
  override fun viewControllerDidDismiss(hadParent: Boolean) {
@@ -45,6 +45,7 @@ import com.lodev09.truesheet.utils.ScreenUtils
45
45
  data class DetentInfo(val index: Int, val position: Float)
46
46
 
47
47
  interface TrueSheetViewControllerDelegate {
48
+ val eventDispatcher: EventDispatcher?
48
49
  fun viewControllerWillPresent(index: Int, position: Float, detent: Float)
49
50
  fun viewControllerDidPresent(index: Int, position: Float, detent: Float)
50
51
  fun viewControllerWillDismiss()
@@ -155,9 +156,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
155
156
  }
156
157
 
157
158
  // Touch Dispatchers
158
- internal var eventDispatcher: EventDispatcher? = null
159
159
  private val jsTouchDispatcher = JSTouchDispatcher(this)
160
- private var jsPointerDispatcher: JSPointerDispatcher? = null
160
+ private val jsPointerDispatcher = JSPointerDispatcher(this)
161
+
162
+ private val eventDispatcher
163
+ get() = delegate?.eventDispatcher
161
164
 
162
165
  // Detent Configuration
163
166
  override var maxSheetHeight: Int? = null
@@ -170,6 +173,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
170
173
  override var grabberOptions: GrabberOptions? = null
171
174
  override var sheetBackgroundColor: Int? = null
172
175
  var insetAdjustment: String = "automatic"
176
+ var scrollable: Boolean = false
177
+ set(value) {
178
+ field = value
179
+ coordinatorLayout?.scrollable = value
180
+ }
173
181
 
174
182
  override var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
175
183
  set(value) {
@@ -279,14 +287,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
279
287
  private val dimViews: List<TrueSheetDimView>
280
288
  get() = listOfNotNull(dimView, parentDimView)
281
289
 
282
- // =============================================================================
283
- // MARK: - Initialization
284
- // =============================================================================
285
-
286
- init {
287
- jsPointerDispatcher = JSPointerDispatcher(this)
288
- }
289
-
290
290
  // =============================================================================
291
291
  // MARK: - Sheet Creation & Cleanup
292
292
  // =============================================================================
@@ -297,6 +297,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
297
297
  // Create coordinator layout
298
298
  coordinatorLayout = TrueSheetCoordinatorLayout(reactContext).apply {
299
299
  delegate = this@TrueSheetViewController
300
+ scrollable = this@TrueSheetViewController.scrollable
300
301
  }
301
302
 
302
303
  sheetView = TrueSheetBottomSheetView(reactContext).apply {
@@ -1062,32 +1063,26 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1062
1063
  }
1063
1064
  }
1064
1065
 
1065
- override fun handleException(t: Throwable) {
1066
- reactContext.reactApplicationContext.handleException(RuntimeException(t))
1067
- }
1068
-
1069
1066
  // =============================================================================
1070
- // MARK: - Touch Event Handling
1067
+ // MARK: - RootView Touch Handling
1071
1068
  // =============================================================================
1072
1069
 
1073
1070
  override fun dispatchTouchEvent(event: MotionEvent): Boolean {
1074
- // Footer needs special handling since it's positioned absolutely
1075
1071
  val footer = containerView?.footerView
1076
- if (footer != null && footer.isVisible) {
1072
+ if (footer != null && footer.isShown) {
1077
1073
  val footerLocation = ScreenUtils.getScreenLocation(footer)
1078
- val touchScreenX = event.rawX.toInt()
1079
- val touchScreenY = event.rawY.toInt()
1080
-
1081
- // Check if touch is within footer bounds
1082
- if (touchScreenX >= footerLocation[0] &&
1083
- touchScreenX <= footerLocation[0] + footer.width &&
1084
- touchScreenY >= footerLocation[1] &&
1085
- touchScreenY <= footerLocation[1] + footer.height
1074
+ val touchX = event.rawX.toInt()
1075
+ val touchY = event.rawY.toInt()
1076
+
1077
+ if (touchX >= footerLocation[0] &&
1078
+ touchX <= footerLocation[0] + footer.width &&
1079
+ touchY >= footerLocation[1] &&
1080
+ touchY <= footerLocation[1] + footer.height
1086
1081
  ) {
1087
1082
  val localEvent = MotionEvent.obtain(event)
1088
1083
  localEvent.setLocation(
1089
- (touchScreenX - footerLocation[0]).toFloat(),
1090
- (touchScreenY - footerLocation[1]).toFloat()
1084
+ (touchX - footerLocation[0]).toFloat(),
1085
+ (touchY - footerLocation[1]).toFloat()
1091
1086
  )
1092
1087
  val handled = footer.dispatchTouchEvent(localEvent)
1093
1088
  localEvent.recycle()
@@ -1100,7 +1095,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1100
1095
  override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
1101
1096
  eventDispatcher?.let {
1102
1097
  jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
1103
- jsPointerDispatcher?.handleMotionEvent(event, it, true)
1098
+ jsPointerDispatcher.handleMotionEvent(event, it, true)
1104
1099
  }
1105
1100
  return super.onInterceptTouchEvent(event)
1106
1101
  }
@@ -1108,35 +1103,35 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1108
1103
  override fun onTouchEvent(event: MotionEvent): Boolean {
1109
1104
  eventDispatcher?.let {
1110
1105
  jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
1111
- jsPointerDispatcher?.handleMotionEvent(event, it, false)
1106
+ jsPointerDispatcher.handleMotionEvent(event, it, false)
1112
1107
  }
1113
1108
  super.onTouchEvent(event)
1114
1109
  return true
1115
1110
  }
1116
1111
 
1117
1112
  override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
1118
- eventDispatcher?.let { jsPointerDispatcher?.handleMotionEvent(event, it, true) }
1113
+ eventDispatcher?.let { jsPointerDispatcher.handleMotionEvent(event, it, true) }
1119
1114
  return super.onHoverEvent(event)
1120
1115
  }
1121
1116
 
1122
1117
  override fun onHoverEvent(event: MotionEvent): Boolean {
1123
- eventDispatcher?.let { jsPointerDispatcher?.handleMotionEvent(event, it, false) }
1118
+ eventDispatcher?.let { jsPointerDispatcher.handleMotionEvent(event, it, false) }
1124
1119
  return super.onHoverEvent(event)
1125
1120
  }
1126
1121
 
1127
1122
  override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
1128
1123
  eventDispatcher?.let {
1129
1124
  jsTouchDispatcher.onChildStartedNativeGesture(ev, it)
1130
- jsPointerDispatcher?.onChildStartedNativeGesture(childView, ev, it)
1125
+ jsPointerDispatcher.onChildStartedNativeGesture(childView, ev, it)
1131
1126
  }
1132
1127
  }
1133
1128
 
1134
1129
  override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
1135
1130
  eventDispatcher?.let { jsTouchDispatcher.onChildEndedNativeGesture(ev, it) }
1136
- jsPointerDispatcher?.onChildEndedNativeGesture()
1131
+ jsPointerDispatcher.onChildEndedNativeGesture()
1137
1132
  }
1138
1133
 
1139
- override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
1140
- super.requestDisallowInterceptTouchEvent(disallowIntercept)
1134
+ override fun handleException(t: Throwable) {
1135
+ reactContext.reactApplicationContext.handleException(RuntimeException(t))
1141
1136
  }
1142
1137
  }
@@ -190,7 +190,7 @@ class TrueSheetViewManager :
190
190
 
191
191
  @ReactProp(name = "scrollable", defaultBoolean = false)
192
192
  override fun setScrollable(view: TrueSheetView, value: Boolean) {
193
- // iOS-specific prop - no-op on Android
193
+ view.setScrollable(value)
194
194
  }
195
195
 
196
196
  @ReactProp(name = "pageSizing", defaultBoolean = true)
@@ -2,7 +2,10 @@ package com.lodev09.truesheet.core
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.content.Context
5
- import android.view.View
5
+ import android.view.MotionEvent
6
+ import android.view.ViewConfiguration
7
+ import android.view.ViewGroup
8
+ import android.widget.ScrollView
6
9
  import androidx.coordinatorlayout.widget.CoordinatorLayout
7
10
  import com.facebook.react.uimanager.PointerEvents
8
11
  import com.facebook.react.uimanager.ReactPointerEventsView
@@ -22,15 +25,19 @@ class TrueSheetCoordinatorLayout(context: Context) :
22
25
  ReactPointerEventsView {
23
26
 
24
27
  var delegate: TrueSheetCoordinatorLayoutDelegate? = null
28
+ var scrollable: Boolean = false
29
+
30
+ private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
31
+ private var dragging = false
32
+ private var initialY = 0f
33
+ private var activePointerId = 0
25
34
 
26
35
  init {
27
- // Fill the entire screen
28
36
  layoutParams = LayoutParams(
29
37
  LayoutParams.MATCH_PARENT,
30
38
  LayoutParams.MATCH_PARENT
31
39
  )
32
40
 
33
- // Ensure we don't clip the sheet during animations
34
41
  clipChildren = false
35
42
  clipToPadding = false
36
43
  }
@@ -46,10 +53,87 @@ class TrueSheetCoordinatorLayout(context: Context) :
46
53
  delegate?.coordinatorLayoutDidLayout(changed)
47
54
  }
48
55
 
49
- /**
50
- * Allow pointer events to pass through to underlying views.
51
- * The DimView and BottomSheetView handle their own touch interception.
52
- */
53
56
  override val pointerEvents: PointerEvents
54
57
  get() = PointerEvents.BOX_NONE
58
+
59
+ /**
60
+ * Intercepts touch events for ScrollViews that can't scroll (content < viewport),
61
+ * allowing the sheet to be dragged in these cases.
62
+ *
63
+ * TODO: Remove this workaround once NestedScrollView is merged into react-native core.
64
+ * See: https://github.com/facebook/react-native/pull/44099
65
+ */
66
+ override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
67
+ if (!scrollable) {
68
+ return super.onInterceptTouchEvent(ev)
69
+ }
70
+
71
+ val scrollView = findScrollView(this)
72
+ val cannotScroll = scrollView != null &&
73
+ scrollView.scrollY == 0 &&
74
+ !scrollView.canScrollVertically(1)
75
+
76
+ if (cannotScroll) {
77
+ when (ev.action and MotionEvent.ACTION_MASK) {
78
+ MotionEvent.ACTION_DOWN -> {
79
+ dragging = false
80
+ initialY = ev.y
81
+ activePointerId = ev.getPointerId(0)
82
+ }
83
+
84
+ MotionEvent.ACTION_MOVE -> {
85
+ val pointerIndex = ev.findPointerIndex(activePointerId)
86
+ if (pointerIndex != -1) {
87
+ val y = ev.getY(pointerIndex)
88
+ val deltaY = initialY - y
89
+ if (kotlin.math.abs(deltaY) > touchSlop) {
90
+ dragging = true
91
+ parent?.requestDisallowInterceptTouchEvent(true)
92
+ }
93
+ }
94
+ }
95
+
96
+ MotionEvent.ACTION_UP,
97
+ MotionEvent.ACTION_CANCEL -> {
98
+ dragging = false
99
+ }
100
+ }
101
+ } else {
102
+ dragging = false
103
+ }
104
+
105
+ return dragging || super.onInterceptTouchEvent(ev)
106
+ }
107
+
108
+ @SuppressLint("ClickableViewAccessibility")
109
+ override fun onTouchEvent(ev: MotionEvent): Boolean {
110
+ if (dragging) {
111
+ when (ev.action and MotionEvent.ACTION_MASK) {
112
+ MotionEvent.ACTION_UP,
113
+ MotionEvent.ACTION_CANCEL -> {
114
+ dragging = false
115
+ }
116
+ }
117
+ // Let parent CoordinatorLayout handle the touch for BottomSheetBehavior
118
+ return super.onTouchEvent(ev)
119
+ }
120
+ return super.onTouchEvent(ev)
121
+ }
122
+
123
+ private fun findScrollView(view: android.view.View): ScrollView? {
124
+ if (view is ScrollView) {
125
+ return view
126
+ }
127
+
128
+ if (view is ViewGroup) {
129
+ for (i in 0 until view.childCount) {
130
+ val scrollView = findScrollView(view.getChildAt(i))
131
+ if (scrollView != null) {
132
+ return scrollView
133
+ }
134
+ }
135
+ }
136
+
137
+ return null
138
+ }
55
139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodev09/react-native-true-sheet",
3
- "version": "3.6.9",
3
+ "version": "3.6.11",
4
4
  "description": "The true native bottom sheet experience for your React Native Apps.",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/module/index.js",