@lodev09/react-native-true-sheet 3.6.8 → 3.6.10

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.
@@ -135,6 +135,11 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
135
135
  val child = getChildAt(index)
136
136
  if (child is TrueSheetContainerView) {
137
137
  child.delegate = null
138
+
139
+ // Dismiss the sheet when container is removed
140
+ if (viewController.isPresented) {
141
+ viewController.dismiss(animated = false)
142
+ }
138
143
  }
139
144
  viewController.removeView(child)
140
145
  }
@@ -159,14 +164,12 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
159
164
  fun onDropInstance() {
160
165
  reactContext.removeLifecycleEventListener(this)
161
166
 
162
- if (viewController.isPresented) {
163
- viewController.dismiss(animated = false)
164
- }
167
+ viewController.dismiss()
168
+ viewController.delegate = null
165
169
 
166
170
  TrueSheetModule.unregisterView(id)
167
171
  TrueSheetStackManager.removeSheet(this)
168
172
 
169
- viewController.delegate = null
170
173
  didInitiallyPresent = false
171
174
  }
172
175
 
@@ -245,6 +248,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
245
248
  viewController.insetAdjustment = insetAdjustment
246
249
  }
247
250
 
251
+ fun setScrollable(scrollable: Boolean) {
252
+ viewController.scrollable = scrollable
253
+ }
254
+
248
255
  // ==================== State Management ====================
249
256
 
250
257
  /**
@@ -139,7 +139,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
139
139
 
140
140
  // Keyboard State
141
141
  private var detentIndexBeforeKeyboard: Int = -1
142
- private var isKeyboardTransitioning: Boolean = false
143
142
 
144
143
  // Promises
145
144
  var presentPromise: (() -> Unit)? = null
@@ -171,6 +170,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
171
170
  override var grabberOptions: GrabberOptions? = null
172
171
  override var sheetBackgroundColor: Int? = null
173
172
  var insetAdjustment: String = "automatic"
173
+ var scrollable: Boolean = false
174
+ set(value) {
175
+ field = value
176
+ coordinatorLayout?.scrollable = value
177
+ }
174
178
 
175
179
  override var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
176
180
  set(value) {
@@ -238,6 +242,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
238
242
  private val currentKeyboardInset: Int
239
243
  get() = keyboardObserver?.currentHeight ?: 0
240
244
 
245
+ private val isKeyboardTransitioning: Boolean
246
+ get() = keyboardObserver?.isTransitioning ?: false
247
+
248
+ private fun isFocusedViewWithinSheet(): Boolean {
249
+ val sheet = sheetView ?: return false
250
+ return keyboardObserver?.isFocusedViewWithinSheet(sheet) ?: false
251
+ }
252
+
241
253
  val bottomInset: Int
242
254
  get() = if (edgeToEdgeEnabled) ScreenUtils.getInsets(reactContext).bottom else 0
243
255
 
@@ -290,6 +302,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
290
302
  // Create coordinator layout
291
303
  coordinatorLayout = TrueSheetCoordinatorLayout(reactContext).apply {
292
304
  delegate = this@TrueSheetViewController
305
+ scrollable = this@TrueSheetViewController.scrollable
293
306
  }
294
307
 
295
308
  sheetView = TrueSheetBottomSheetView(reactContext).apply {
@@ -320,7 +333,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
320
333
  isPresented = false
321
334
  isSheetVisible = false
322
335
  wasHiddenByModal = false
323
- isKeyboardTransitioning = false
324
336
  isPresentAnimating = false
325
337
  lastEmittedPositionPx = -1
326
338
  detentIndexBeforeKeyboard = -1
@@ -872,9 +884,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
872
884
  // MARK: - Keyboard Handling
873
885
  // =============================================================================
874
886
 
875
- private fun shouldHandleKeyboard(): Boolean {
887
+ private fun shouldHandleKeyboard(checkFocus: Boolean = true): Boolean {
876
888
  if (wasHiddenByModal) return false
877
- return isTopmostSheet
889
+ if (!isTopmostSheet) return false
890
+ if (checkFocus && !isFocusedViewWithinSheet()) return false
891
+ return true
878
892
  }
879
893
 
880
894
  fun setupKeyboardObserver() {
@@ -886,7 +900,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
886
900
  keyboardObserver = TrueSheetKeyboardObserver(coordinator, reactContext).apply {
887
901
  delegate = object : TrueSheetKeyboardObserverDelegate {
888
902
  override fun keyboardWillShow(height: Int) {
889
- isKeyboardTransitioning = true
890
903
  if (!shouldHandleKeyboard()) return
891
904
  detentIndexBeforeKeyboard = currentDetentIndex
892
905
  setupSheetDetents()
@@ -894,7 +907,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
894
907
  }
895
908
 
896
909
  override fun keyboardWillHide() {
897
- if (!shouldHandleKeyboard()) return
910
+ if (!shouldHandleKeyboard(checkFocus = false)) return
911
+
898
912
  setupSheetDetents()
899
913
  if (!isDismissing && detentIndexBeforeKeyboard >= 0) {
900
914
  setStateForDetentIndex(detentIndexBeforeKeyboard)
@@ -902,9 +916,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
902
916
  }
903
917
  }
904
918
 
905
- override fun keyboardDidHide() {
906
- isKeyboardTransitioning = false
907
- }
919
+ override fun keyboardDidHide() {}
908
920
 
909
921
  override fun keyboardDidChangeHeight(height: Int) {
910
922
  if (!shouldHandleKeyboard()) return
@@ -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
  }
@@ -31,6 +31,20 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
31
31
  var targetHeight: Int = 0
32
32
  private set
33
33
 
34
+ var isTransitioning: Boolean = false
35
+ private set
36
+
37
+ fun isFocusedViewWithinSheet(sheetView: View): Boolean {
38
+ val focusedView = reactContext.currentActivity?.currentFocus ?: return false
39
+ var current: View? = focusedView
40
+ while (current != null && current !== targetView) {
41
+ if (current === sheetView) return true
42
+ val parent = current.parent
43
+ current = if (parent is View) parent else null
44
+ }
45
+ return false
46
+ }
47
+
34
48
  private var isHiding: Boolean = false
35
49
  private var globalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
36
50
  private var activityRootView: View? = null
@@ -80,6 +94,7 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
80
94
  endHeight = getKeyboardHeight()
81
95
  targetHeight = endHeight
82
96
  isHiding = endHeight < startHeight
97
+ isTransitioning = true
83
98
  if (endHeight > startHeight) {
84
99
  delegate?.keyboardWillShow(endHeight)
85
100
  } else if (isHiding) {
@@ -102,6 +117,7 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
102
117
  override fun onEnd(animation: WindowInsetsAnimationCompat) {
103
118
  val finalHeight = getKeyboardHeight()
104
119
  updateHeight(startHeight, finalHeight, 1f)
120
+ isTransitioning = false
105
121
  if (isHiding) {
106
122
  delegate?.keyboardDidHide()
107
123
  isHiding = false
@@ -134,6 +150,7 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
134
150
  targetHeight = newHeight
135
151
  isHiding = newHeight < previousHeight
136
152
 
153
+ isTransitioning = true
137
154
  if (newHeight > previousHeight) {
138
155
  delegate?.keyboardWillShow(newHeight)
139
156
  } else if (isHiding) {
@@ -142,6 +159,7 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
142
159
 
143
160
  // On legacy API, keyboard has already animated - just update immediately
144
161
  updateHeight(previousHeight, newHeight, 1f)
162
+ isTransitioning = false
145
163
 
146
164
  if (isHiding && newHeight == 0) {
147
165
  delegate?.keyboardDidHide()
@@ -96,7 +96,12 @@ using namespace facebook::react;
96
96
 
97
97
  - (void)dealloc {
98
98
  if (_controller && _controller.presentingViewController) {
99
- [_controller dismissViewControllerAnimated:NO completion:nil];
99
+ // Find the root presenting controller to dismiss the entire stack
100
+ UIViewController *root = _controller.presentingViewController;
101
+ while (root.presentingViewController != nil) {
102
+ root = root.presentingViewController;
103
+ }
104
+ [root dismissViewControllerAnimated:YES completion:nil];
100
105
  }
101
106
 
102
107
  _controller.delegate = nil;
@@ -258,10 +263,6 @@ using namespace facebook::react;
258
263
  - (void)prepareForRecycle {
259
264
  [super prepareForRecycle];
260
265
 
261
- if (_controller && _controller.presentingViewController) {
262
- [_controller dismissViewControllerAnimated:YES completion:nil];
263
- }
264
-
265
266
  [TrueSheetModule unregisterViewWithTag:@(self.tag)];
266
267
 
267
268
  _lastStateSize = CGSizeZero;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodev09/react-native-true-sheet",
3
- "version": "3.6.8",
3
+ "version": "3.6.10",
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",