@swmansion/react-native-bottom-sheet 0.7.1 → 0.7.3

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
@@ -12,6 +12,7 @@ React Native.
12
12
  - Native implementation for optimal performance.
13
13
  - Bring your own sheet surface.
14
14
  - Dynamic, content‍-‍based sizing out of the box.
15
+ - Automatic handling of vertically scrollable children.
15
16
  - Position tracking for driving UI tied to sheets.
16
17
  - Programmatic‍-‍only detents for snap points unreachable
17
18
  by dragging.
@@ -178,7 +179,7 @@ const position = useSharedValue(0);
178
179
  }}
179
180
  >
180
181
  {/* ... */}
181
- </BottomSheet>;
182
+ </BottomSheet>
182
183
  ```
183
184
 
184
185
  ## By [Software Mansion](https://swmansion.com)
@@ -59,6 +59,10 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
59
59
  init {
60
60
  clipChildren = false
61
61
  clipToPadding = false
62
+ // Set directly rather than via the JSX prop because Fabric doesn't forward
63
+ // pointerEvents to the native view on Android. Without BOX_NONE the view
64
+ // itself becomes a touch target and its onTouchEvent would claim gestures
65
+ // that should go to children.
62
66
  pointerEvents = PointerEvents.BOX_NONE
63
67
  sheetContainer.clipChildren = false
64
68
  sheetContainer.clipToPadding = false
@@ -327,6 +331,10 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
327
331
  if (!isAtMaxDraggable) {
328
332
  lastTouchY = y
329
333
  requestDisallowInterceptTouchEvent(false)
334
+ // Cancel in-flight JS touches. React Native's JSTouchDispatcher
335
+ // processes events at the root view level before onInterceptTouchEvent
336
+ // runs, so without this the JS side never sees a cancel and Pressable
337
+ // would still fire onPress.
330
338
  NativeGestureUtil.notifyNativeGestureStarted(this, ev)
331
339
  return true
332
340
  }
@@ -408,10 +416,28 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
408
416
  }
409
417
 
410
418
  // MARK: - Scroll view helpers
419
+ //
420
+ // Explicit scroll-view detection is required because Android's touch dispatch
421
+ // doesn't support mid-gesture handoff. Once a ScrollView claims a gesture it
422
+ // keeps it for the entire sequence, and it always claims (returns true from
423
+ // onTouchEvent) even when at the scroll boundary. Without this check the sheet
424
+ // could never collapse by dragging down when a ScrollView is at the top.
411
425
 
412
426
  private fun isScrollViewAtTop(): Boolean {
413
427
  val scrollView = findScrollView(sheetContainer) ?: return true
414
- return !scrollView.canScrollVertically(-1)
428
+ if (!isTouchInsideView(scrollView)) return true
429
+ val inverted = isViewInverted(scrollView)
430
+ return if (inverted) !scrollView.canScrollVertically(1) else !scrollView.canScrollVertically(-1)
431
+ }
432
+
433
+ private fun isTouchInsideView(target: View): Boolean {
434
+ val rect = android.graphics.Rect()
435
+ if (!target.getGlobalVisibleRect(rect)) return false
436
+ val myLocation = IntArray(2)
437
+ getLocationOnScreen(myLocation)
438
+ val touchScreenX = (myLocation[0] + initialTouchX).toInt()
439
+ val touchScreenY = (myLocation[1] + initialTouchY).toInt()
440
+ return rect.contains(touchScreenX, touchScreenY)
415
441
  }
416
442
 
417
443
  private fun findScrollView(view: View): View? {
@@ -424,6 +450,19 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
424
450
  return null
425
451
  }
426
452
 
453
+ private fun isViewInverted(view: View): Boolean {
454
+ val values = FloatArray(9)
455
+ var current: View? = view
456
+ while (current != null && current !== sheetContainer) {
457
+ if (!current.matrix.isIdentity) {
458
+ current.matrix.getValues(values)
459
+ if (values[android.graphics.Matrix.MSCALE_Y] < 0) return true
460
+ }
461
+ current = current.parent as? View
462
+ }
463
+ return false
464
+ }
465
+
427
466
  // MARK: - Cleanup
428
467
 
429
468
  fun destroy() {
@@ -42,6 +42,8 @@ public final class RNSBottomSheetHostingView: UIView {
42
42
  panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
43
43
  panGesture.delegate = self
44
44
  panGesture.cancelsTouchesInView = true
45
+ // Delay touch delivery to views so that Pressable doesn't flash its pressed
46
+ // state while the pan gesture is still being disambiguated.
45
47
  panGesture.delaysTouchesBegan = true
46
48
  panGesture.delaysTouchesEnded = false
47
49
  sheetContainer.addGestureRecognizer(panGesture)
@@ -51,6 +53,30 @@ public final class RNSBottomSheetHostingView: UIView {
51
53
  fatalError("init(coder:) has not been implemented")
52
54
  }
53
55
 
56
+ // RCTSurfaceTouchHandler dispatches touch events to JS independently of the
57
+ // pan gesture (it fires in touchesBegan: regardless of its recognizer state).
58
+ // We cache it here and toggle isEnabled in handlePan(.began) to force a
59
+ // touchesCancelled dispatch to JS, preventing Pressable from firing onPress
60
+ // during a sheet drag. This is the iOS equivalent of Android's
61
+ // NativeGestureUtil.notifyNativeGestureStarted.
62
+ private weak var surfaceTouchHandler: UIGestureRecognizer?
63
+
64
+ public override func didMoveToWindow() {
65
+ super.didMoveToWindow()
66
+ surfaceTouchHandler = nil
67
+ guard window != nil else { return }
68
+ var current: UIView? = superview
69
+ while let view = current {
70
+ for gr in view.gestureRecognizers ?? [] {
71
+ if NSStringFromClass(type(of: gr)).contains("TouchHandler") {
72
+ surfaceTouchHandler = gr
73
+ return
74
+ }
75
+ }
76
+ current = view.superview
77
+ }
78
+ }
79
+
54
80
  public override func layoutSubviews() {
55
81
  super.layoutSubviews()
56
82
  guard bounds.width > 0, bounds.height > 0 else { return }
@@ -240,6 +266,10 @@ public final class RNSBottomSheetHostingView: UIView {
240
266
  case .began:
241
267
  isPanning = true
242
268
  setContentInteractionEnabled(false)
269
+ if let handler = surfaceTouchHandler {
270
+ handler.isEnabled = false
271
+ handler.isEnabled = true
272
+ }
243
273
  gesture.setTranslation(.zero, in: self)
244
274
  if let animator = activeAnimator {
245
275
  stopDisplayLink()
@@ -314,6 +344,15 @@ public final class RNSBottomSheetHostingView: UIView {
314
344
  return nil
315
345
  }
316
346
 
347
+ private func isViewInverted(_ view: UIView) -> Bool {
348
+ var current: UIView? = view
349
+ while let v = current, v !== sheetContainer {
350
+ if v.transform.d < 0 { return true }
351
+ current = v.superview
352
+ }
353
+ return false
354
+ }
355
+
317
356
  public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
318
357
  guard gestureRecognizer === panGesture else { return true }
319
358
 
@@ -324,14 +363,23 @@ public final class RNSBottomSheetHostingView: UIView {
324
363
  guard draggable.count > 1 else { return false }
325
364
 
326
365
  let maxDraggableIndex = draggable.last?.offset ?? 0
366
+ // Below max: allow drag in either direction to reach other detents.
327
367
  guard targetIndex >= maxDraggableIndex else { return true }
328
-
368
+ // At max: only allow downward drag, and only when the scroll view (if any)
369
+ // is at its top edge — otherwise the scroll view should handle the gesture.
329
370
  if velocity.y < 0 {
330
371
  return false
331
372
  }
332
373
 
333
- let scrollAtTop = (firstScrollView(in: sheetContainer)?.contentOffset.y ?? 0) <= 0
334
- return scrollAtTop
374
+ guard let scrollView = firstScrollView(in: sheetContainer) else { return true }
375
+ let locationInScroll = panGesture.location(in: scrollView)
376
+ guard scrollView.bounds.contains(locationInScroll) else { return true }
377
+ let inverted = isViewInverted(scrollView)
378
+ if inverted {
379
+ let maxOffsetY = scrollView.contentSize.height - scrollView.bounds.height + scrollView.adjustedContentInset.bottom
380
+ return scrollView.contentOffset.y >= maxOffsetY
381
+ }
382
+ return scrollView.contentOffset.y <= 0
335
383
  }
336
384
  }
337
385
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/react-native-bottom-sheet",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Provides bottom-sheet components for React Native.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",