@swmansion/react-native-bottom-sheet 0.10.0-next.2 → 0.10.0-next.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.
@@ -62,6 +62,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
62
62
  private var pendingIndex: Int? = null
63
63
  private var hasLaidOut = false
64
64
  private var isPanning = false
65
+ private var panStartingIndex: Int? = null
65
66
 
66
67
  // MARK: - Internal
67
68
 
@@ -393,20 +394,29 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
393
394
  private val isTargetingClosedDetent: Boolean
394
395
  get() = closedIndex?.let { targetIndex == it } == true
395
396
 
396
- private val draggableMinTy: Float
397
- get() {
398
- val highestIndex = detentSpecs.indices.lastOrNull { !detentSpecs[it].programmatic } ?: 0
399
- return translationY(highestIndex)
397
+ private fun snapCandidateIndices(includeIndex: Int? = null): List<Int> {
398
+ val indices = detentSpecs.indices.filter { !detentSpecs[it].programmatic }.toMutableList()
399
+ if (
400
+ includeIndex != null &&
401
+ includeIndex in detentSpecs.indices &&
402
+ detentSpecs[includeIndex].programmatic
403
+ ) {
404
+ indices.add(includeIndex)
400
405
  }
406
+ return indices.distinct().sortedBy { detentSpecs[it].height }
407
+ }
401
408
 
402
- private val draggableMaxTy: Float
403
- get() {
404
- val lowestIndex = detentSpecs.indices.firstOrNull { !detentSpecs[it].programmatic } ?: 0
405
- return translationY(lowestIndex)
406
- }
409
+ private fun draggableRange(includeIndex: Int? = null): ClosedFloatingPointRange<Float> {
410
+ val candidates = snapCandidateIndices(includeIndex)
411
+ if (candidates.isEmpty()) return 0f..0f
412
+ val translations = candidates.map(::translationY)
413
+ return (translations.minOrNull() ?: 0f)..(translations.maxOrNull() ?: 0f)
414
+ }
407
415
 
408
- private val isAtMaxDraggable: Boolean
409
- get() = sheetContainer.translationY <= draggableMinTy + 1f
416
+ private fun isAtMaxDragCandidate(includeIndex: Int? = null): Boolean {
417
+ val range = draggableRange(includeIndex)
418
+ return sheetContainer.translationY <= range.start + 1f
419
+ }
410
420
 
411
421
  private fun emitPosition() {
412
422
  val maxHeight = detentSpecs.maxOfOrNull { it.height } ?: resolvedMaxDetentHeight()
@@ -510,24 +520,24 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
510
520
  spring.start()
511
521
  }
512
522
 
513
- private fun bestSnapIndex(currentHeight: Float, velocity: Float): Int {
514
- val draggable = detentSpecs.withIndex().filter { !it.value.programmatic }
515
- if (draggable.isEmpty()) return targetIndex
523
+ private fun bestSnapIndex(currentHeight: Float, velocity: Float, includeIndex: Int? = null): Int {
524
+ val candidates = snapCandidateIndices(includeIndex)
525
+ if (candidates.isEmpty()) return targetIndex
516
526
 
517
527
  val flickThreshold = 600f * density
518
528
 
519
529
  if (velocity < -flickThreshold) {
520
- return draggable.firstOrNull { it.value.height > currentHeight }?.index
521
- ?: draggable.lastOrNull()?.index
530
+ return candidates.firstOrNull { detentSpecs[it].height > currentHeight }
531
+ ?: candidates.lastOrNull()
522
532
  ?: targetIndex
523
533
  }
524
534
  if (velocity > flickThreshold) {
525
- return draggable.lastOrNull { it.value.height < currentHeight }?.index
526
- ?: draggable.firstOrNull()?.index
535
+ return candidates.lastOrNull { detentSpecs[it].height < currentHeight }
536
+ ?: candidates.firstOrNull()
527
537
  ?: targetIndex
528
538
  }
529
539
 
530
- return draggable.minByOrNull { abs(it.value.height - currentHeight) }?.index ?: targetIndex
540
+ return candidates.minByOrNull { abs(detentSpecs[it].height - currentHeight) } ?: targetIndex
531
541
  }
532
542
 
533
543
  // MARK: - Touch handling
@@ -563,11 +573,12 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
563
573
  val dx = x - initialTouchX
564
574
  val dy = y - initialTouchY
565
575
 
566
- if (abs(dy) > touchSlop && abs(dy) > abs(dx) && draggableMinTy < draggableMaxTy) {
576
+ val dragRange = draggableRange(targetIndex)
577
+ if (abs(dy) > touchSlop && abs(dy) > abs(dx) && dragRange.start < dragRange.endInclusive) {
567
578
  if (disableScrollableNegotiation && findScrollableAtTouch() != null) {
568
579
  return false
569
580
  }
570
- if (!isAtMaxDraggable) {
581
+ if (!isAtMaxDragCandidate(targetIndex)) {
571
582
  lastTouchY = y
572
583
  requestDisallowInterceptTouchEvent(false)
573
584
  // Cancel in-flight JS touches. React Native's JSTouchDispatcher
@@ -640,7 +651,9 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
640
651
  val dy = y - lastTouchY
641
652
  lastTouchY = y
642
653
 
643
- val newTy = (sheetContainer.translationY + dy).coerceIn(draggableMinTy, draggableMaxTy)
654
+ val dragRange = draggableRange(panStartingIndex)
655
+ val newTy =
656
+ (sheetContainer.translationY + dy).coerceIn(dragRange.start, dragRange.endInclusive)
644
657
  sheetContainer.translationY = newTy
645
658
  emitPosition()
646
659
  return true
@@ -659,7 +672,8 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
659
672
  velocityTracker = null
660
673
  val maxHeight = detentSpecs.maxOfOrNull { it.height } ?: resolvedMaxDetentHeight()
661
674
  val currentHeight = maxHeight - sheetContainer.translationY
662
- val index = bestSnapIndex(currentHeight, velocity)
675
+ val index = bestSnapIndex(currentHeight, velocity, panStartingIndex)
676
+ panStartingIndex = null
663
677
  snapToIndex(index, velocity)
664
678
  return true
665
679
  }
@@ -679,6 +693,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
679
693
 
680
694
  private fun beginPan(event: MotionEvent) {
681
695
  isPanning = true
696
+ panStartingIndex = targetIndex
682
697
  activePointerId = event.getPointerId(0)
683
698
  lastTouchY = event.y
684
699
  velocityTracker?.recycle()
@@ -771,6 +786,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
771
786
  pendingIndex = null
772
787
  hasLaidOut = false
773
788
  isPanning = false
789
+ panStartingIndex = null
774
790
  initialTouchY = 0f
775
791
  initialTouchX = 0f
776
792
  lastTouchY = 0f
@@ -59,6 +59,7 @@ public final class RNSBottomSheetHostingView: UIView {
59
59
  private var pendingIndex: Int?
60
60
  private var hasLaidOut = false
61
61
  private var isPanning = false
62
+ private var panStartingIndex: Int?
62
63
  private var isContentInteractionDisabled = false
63
64
  private var contentHeightMarker: UIView?
64
65
  private static var markerObservationContext = 0
@@ -237,6 +238,7 @@ public final class RNSBottomSheetHostingView: UIView {
237
238
  pendingIndex = nil
238
239
  hasLaidOut = false
239
240
  isPanning = false
241
+ panStartingIndex = nil
240
242
  setContentInteractionEnabled(true)
241
243
  stopObservingContentHeightMarker()
242
244
  sheetContainer.transform = .identity
@@ -260,11 +262,27 @@ public final class RNSBottomSheetHostingView: UIView {
260
262
  return maxHeight - snapHeight
261
263
  }
262
264
 
263
- private var draggableRange: (minTy: CGFloat, maxTy: CGFloat) {
264
- let draggable = detentSpecs.enumerated().filter { !$0.element.programmatic }
265
- let highestIndex = draggable.last?.offset ?? 0
266
- let lowestIndex = draggable.first?.offset ?? 0
267
- return (minTy: translationY(for: highestIndex), maxTy: translationY(for: lowestIndex))
265
+ private func snapCandidateIndices(including index: Int? = nil) -> [Int] {
266
+ var indices = detentSpecs.indices.filter { !detentSpecs[$0].programmatic }
267
+ if
268
+ let index,
269
+ detentSpecs.indices.contains(index),
270
+ detentSpecs[index].programmatic
271
+ {
272
+ indices.append(index)
273
+ }
274
+ return Array(Set(indices)).sorted {
275
+ detentSpecs[$0].height < detentSpecs[$1].height
276
+ }
277
+ }
278
+
279
+ private func draggableRange(including index: Int? = nil) -> (minTy: CGFloat, maxTy: CGFloat) {
280
+ let candidates = snapCandidateIndices(including: index)
281
+ guard !candidates.isEmpty else { return (minTy: 0, maxTy: 0) }
282
+ return (
283
+ minTy: candidates.map { translationY(for: $0) }.min() ?? 0,
284
+ maxTy: candidates.map { translationY(for: $0) }.max() ?? 0
285
+ )
268
286
  }
269
287
 
270
288
  private var closedIndex: Int? {
@@ -393,6 +411,7 @@ public final class RNSBottomSheetHostingView: UIView {
393
411
  switch gesture.state {
394
412
  case .began:
395
413
  isPanning = true
414
+ panStartingIndex = targetIndex
396
415
  sheetContainer.endEditing(true)
397
416
  setContentInteractionEnabled(false)
398
417
  if let handler = surfaceTouchHandler {
@@ -411,8 +430,9 @@ public final class RNSBottomSheetHostingView: UIView {
411
430
  case .changed:
412
431
  let delta = gesture.translation(in: self).y
413
432
  gesture.setTranslation(.zero, in: self)
414
- let minTy = draggableRange.minTy
415
- let maxTy = draggableRange.maxTy
433
+ let range = draggableRange(including: panStartingIndex)
434
+ let minTy = range.minTy
435
+ let maxTy = range.maxTy
416
436
  let newTy = max(minTy, min(maxTy, sheetContainer.transform.ty + delta))
417
437
  sheetContainer.transform = CGAffineTransform(translationX: 0, y: newTy)
418
438
  emitPosition()
@@ -421,7 +441,8 @@ public final class RNSBottomSheetHostingView: UIView {
421
441
  isPanning = false
422
442
  let velocity = gesture.velocity(in: self).y
423
443
  let currentHeight = maxHeight - sheetContainer.transform.ty
424
- let index = bestSnapIndex(for: currentHeight, velocity: velocity)
444
+ let index = bestSnapIndex(for: currentHeight, velocity: velocity, including: panStartingIndex)
445
+ panStartingIndex = nil
425
446
  snapToIndex(index, velocity: velocity)
426
447
 
427
448
  case .cancelled:
@@ -429,11 +450,17 @@ public final class RNSBottomSheetHostingView: UIView {
429
450
  setContentInteractionEnabled(true)
430
451
  let cancelVelocity = gesture.velocity(in: self).y
431
452
  let cancelHeight = maxHeight - sheetContainer.transform.ty
432
- let cancelIndex = bestSnapIndex(for: cancelHeight, velocity: cancelVelocity)
453
+ let cancelIndex = bestSnapIndex(
454
+ for: cancelHeight,
455
+ velocity: cancelVelocity,
456
+ including: panStartingIndex
457
+ )
458
+ panStartingIndex = nil
433
459
  snapToIndex(cancelIndex, velocity: cancelVelocity)
434
460
 
435
461
  case .failed:
436
462
  isPanning = false
463
+ panStartingIndex = nil
437
464
  setContentInteractionEnabled(true)
438
465
 
439
466
  default:
@@ -441,24 +468,28 @@ public final class RNSBottomSheetHostingView: UIView {
441
468
  }
442
469
  }
443
470
 
444
- private func bestSnapIndex(for height: CGFloat, velocity: CGFloat) -> Int {
445
- let draggable = detentSpecs.enumerated().filter { !$0.element.programmatic }
446
- guard !draggable.isEmpty else { return targetIndex }
471
+ private func bestSnapIndex(
472
+ for height: CGFloat,
473
+ velocity: CGFloat,
474
+ including index: Int? = nil
475
+ ) -> Int {
476
+ let candidates = snapCandidateIndices(including: index)
477
+ guard !candidates.isEmpty else { return targetIndex }
447
478
 
448
479
  let flickThreshold: CGFloat = 600
449
480
 
450
481
  if velocity < -flickThreshold {
451
- return draggable.first(where: { $0.element.height > height })?.offset
452
- ?? draggable.last?.offset ?? targetIndex
482
+ return candidates.first(where: { detentSpecs[$0].height > height })
483
+ ?? candidates.last ?? targetIndex
453
484
  }
454
485
  if velocity > flickThreshold {
455
- return draggable.last(where: { $0.element.height < height })?.offset
456
- ?? draggable.first?.offset ?? targetIndex
486
+ return candidates.last(where: { detentSpecs[$0].height < height })
487
+ ?? candidates.first ?? targetIndex
457
488
  }
458
489
 
459
- return draggable.min(by: {
460
- abs($0.element.height - height) < abs($1.element.height - height)
461
- })?.offset ?? targetIndex
490
+ return candidates.min(by: {
491
+ abs(detentSpecs[$0].height - height) < abs(detentSpecs[$1].height - height)
492
+ }) ?? targetIndex
462
493
  }
463
494
 
464
495
  private func isVerticallyScrollable(_ scrollView: UIScrollView) -> Bool {
@@ -498,8 +529,8 @@ public final class RNSBottomSheetHostingView: UIView {
498
529
  let velocity = panGesture.velocity(in: self)
499
530
  guard abs(velocity.y) > abs(velocity.x) else { return false }
500
531
 
501
- let draggable = detentSpecs.enumerated().filter { !$0.element.programmatic }
502
- guard draggable.count > 1 else { return false }
532
+ let candidates = snapCandidateIndices(including: targetIndex)
533
+ guard candidates.count > 1 else { return false }
503
534
 
504
535
  if disableScrollableNegotiation {
505
536
  let locationInContainer = panGesture.location(in: sheetContainer)
@@ -508,9 +539,9 @@ public final class RNSBottomSheetHostingView: UIView {
508
539
  }
509
540
  }
510
541
 
511
- let maxDraggableIndex = draggable.last?.offset ?? 0
542
+ let maxCandidateHeight = candidates.map { detentSpecs[$0].height }.max() ?? 0
512
543
  // Below max: allow drag in either direction to reach other detents.
513
- guard targetIndex >= maxDraggableIndex else { return true }
544
+ guard currentSheetHeight >= maxCandidateHeight - 0.5 else { return true }
514
545
  // At max: only allow downward drag, and only when the scroll view (if any)
515
546
  // is at its top edge — otherwise the scroll view should handle the gesture.
516
547
  if velocity.y < 0 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/react-native-bottom-sheet",
3
- "version": "0.10.0-next.2",
3
+ "version": "0.10.0-next.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",