@swmansion/react-native-bottom-sheet 0.10.0-next.3 → 0.10.0-next.5

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.
@@ -85,6 +85,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
85
85
  private var scrimColor = Color.TRANSPARENT
86
86
  private var scrimProgress = 0f
87
87
  private var suppressScrimForClosingTarget = false
88
+ private var scrimPinnedFull = false
88
89
  private var maxDetentHeight = Float.NaN
89
90
  private var contentHeightMarker: View? = null
90
91
 
@@ -294,6 +295,13 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
294
295
  }
295
296
 
296
297
  val previousMaxHeight = detentSpecs.maxOfOrNull { it.height } ?: resolvedMaxDetentHeight()
298
+ // Whether the scrim is currently fully opaque, i.e. the sheet is settled at
299
+ // or above the first non-zero detent. If so, a detent resize must not dip
300
+ // the scrim while the sheet re-anchors to the new geometry.
301
+ val wasScrimFull =
302
+ modal &&
303
+ firstNonZeroDetentHeight > 0f &&
304
+ currentSheetHeight() + 0.5f >= firstNonZeroDetentHeight
297
305
  detentSpecs = resolvedDetents
298
306
  if (width > 0 && height > 0 && detentSpecs.isNotEmpty()) {
299
307
  layoutSheetContainer(width, height)
@@ -317,8 +325,15 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
317
325
  // sheet surface keeps the same on-screen height across the resize.
318
326
  val visibleHeight = previousMaxHeight - currentTy
319
327
  sheetContainer.translationY = (newMaxHeight - visibleHeight).coerceIn(0f, newMaxHeight)
328
+ scrimPinnedFull = scrimPinnedFull || wasScrimFull
320
329
  emitPosition()
321
- snapToIndex(targetIndex, 0f, emitIndexChange = false, emitSettle = shouldEmitSettle)
330
+ snapToIndex(
331
+ targetIndex,
332
+ 0f,
333
+ emitIndexChange = false,
334
+ emitSettle = shouldEmitSettle,
335
+ preserveScrimPin = true,
336
+ )
322
337
  } else {
323
338
  val currentVisibleHeight = previousMaxHeight - sheetContainer.translationY
324
339
  val targetHeight = detentSpecs.getOrNull(targetIndex)?.height ?: 0f
@@ -332,8 +347,15 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
332
347
  // up to the taller detent.
333
348
  sheetContainer.translationY =
334
349
  (newMaxHeight - currentVisibleHeight).coerceIn(0f, newMaxHeight)
350
+ scrimPinnedFull = scrimPinnedFull || wasScrimFull
335
351
  emitPosition()
336
- snapToIndex(targetIndex, 0f, emitIndexChange = false, emitSettle = false)
352
+ snapToIndex(
353
+ targetIndex,
354
+ 0f,
355
+ emitIndexChange = false,
356
+ emitSettle = false,
357
+ preserveScrimPin = true,
358
+ )
337
359
  }
338
360
  }
339
361
  }
@@ -474,12 +496,16 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
474
496
  velocity: Float,
475
497
  emitIndexChange: Boolean = true,
476
498
  emitSettle: Boolean = true,
499
+ preserveScrimPin: Boolean = false,
477
500
  ) {
478
501
  if (index < 0 || index >= detentSpecs.size) return
479
502
  targetIndex = index
480
503
  if (!isTargetingClosedDetent) {
481
504
  suppressScrimForClosingTarget = false
482
505
  }
506
+ if (!preserveScrimPin) {
507
+ scrimPinnedFull = false
508
+ }
483
509
 
484
510
  val targetTy = translationY(index)
485
511
  val currentTy = sheetContainer.translationY
@@ -504,6 +530,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
504
530
  activeAnimation = null
505
531
  activeAnimationEmitsSettle = false
506
532
  suppressScrimForClosingTarget = false
533
+ scrimPinnedFull = false
507
534
  if (closedIndex == index) {
508
535
  sheetContainer.translationY = translationY(index)
509
536
  hideScrim()
@@ -693,6 +720,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
693
720
 
694
721
  private fun beginPan(event: MotionEvent) {
695
722
  isPanning = true
723
+ scrimPinnedFull = false
696
724
  panStartingIndex = targetIndex
697
725
  activePointerId = event.getPointerId(0)
698
726
  lastTouchY = event.y
@@ -818,6 +846,15 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
818
846
  return
819
847
  }
820
848
 
849
+ // While the sheet is fully open and only its content/detent geometry is
850
+ // resizing, the position momentarily lags the grown detent height. Keep the
851
+ // scrim at full opacity instead of dipping it until the re-anchor settles.
852
+ if (scrimPinnedFull) {
853
+ scrimProgress = 1f
854
+ invalidate()
855
+ return
856
+ }
857
+
821
858
  val threshold = firstNonZeroDetentHeight
822
859
  scrimProgress = if (threshold <= 0f) 0f else (position / threshold).coerceIn(0f, 1f)
823
860
  invalidate()
@@ -3,6 +3,7 @@
3
3
  #import "../common/cpp/react/renderer/components/ReactNativeBottomSheetSpec/BottomSheetStateHelper.h"
4
4
  #import "../common/cpp/react/renderer/components/ReactNativeBottomSheetSpec/ComponentDescriptors.h"
5
5
 
6
+ #import <React/RCTAssert.h>
6
7
  #import <React/RCTConversions.h>
7
8
  #import <React/RCTFabricComponentsPlugins.h>
8
9
  #import <react/renderer/components/ReactNativeBottomSheetSpec/EventEmitters.h>
@@ -138,6 +139,11 @@ using namespace facebook::react;
138
139
  }
139
140
  }
140
141
 
142
+ - (void)bottomSheetView:(BottomSheetContentView *)view didReportError:(NSString *)message
143
+ {
144
+ RCTFatal([NSError errorWithDomain:RCTErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : message}]);
145
+ }
146
+
141
147
  - (void)prepareForRecycle
142
148
  {
143
149
  [super prepareForRecycle];
@@ -8,6 +8,7 @@ NS_ASSUME_NONNULL_BEGIN
8
8
  - (void)bottomSheetView:(BottomSheetContentView *)view didChangeIndex:(NSInteger)index;
9
9
  - (void)bottomSheetView:(BottomSheetContentView *)view didSettle:(NSInteger)index;
10
10
  - (void)bottomSheetView:(BottomSheetContentView *)view didChangePosition:(CGFloat)position;
11
+ - (void)bottomSheetView:(BottomSheetContentView *)view didReportError:(NSString *)message;
11
12
  @end
12
13
 
13
14
  @interface BottomSheetContentView : UIView
@@ -115,6 +115,11 @@
115
115
  [self.delegate bottomSheetView:self didChangePosition:position];
116
116
  }
117
117
 
118
+ - (void)bottomSheetHostingView:(RNSBottomSheetHostingView *)view didReportError:(NSString *)message
119
+ {
120
+ [self.delegate bottomSheetView:self didReportError:message];
121
+ }
122
+
118
123
  - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
119
124
  {
120
125
  CGPoint implPoint = [self convertPoint:point toView:_impl];
@@ -4,6 +4,7 @@ import UIKit
4
4
  func bottomSheetHostingView(_ view: RNSBottomSheetHostingView, didChangeIndex index: Int)
5
5
  func bottomSheetHostingView(_ view: RNSBottomSheetHostingView, didSettle index: Int)
6
6
  func bottomSheetHostingView(_ view: RNSBottomSheetHostingView, didChangePosition position: CGFloat)
7
+ func bottomSheetHostingView(_ view: RNSBottomSheetHostingView, didReportError message: String)
7
8
  }
8
9
 
9
10
  private struct DetentSpec: Equatable {
@@ -55,10 +56,12 @@ public final class RNSBottomSheetHostingView: UIView {
55
56
  private var panGesture: UIPanGestureRecognizer!
56
57
  private var activeAnimator: UIViewPropertyAnimator?
57
58
  private var activeAnimatorEmitsSettle = false
59
+ private var scrimPinnedFull = false
58
60
  private var displayLink: CADisplayLink?
59
61
  private var pendingIndex: Int?
60
62
  private var hasLaidOut = false
61
63
  private var isPanning = false
64
+ private var lastReportedInvalidDetentMessage: String?
62
65
  private var panStartingIndex: Int?
63
66
  private var isContentInteractionDisabled = false
64
67
  private var contentHeightMarker: UIView?
@@ -364,10 +367,14 @@ public final class RNSBottomSheetHostingView: UIView {
364
367
  _ index: Int,
365
368
  velocity: CGFloat,
366
369
  emitIndexChange: Bool = true,
367
- emitSettle: Bool = true
370
+ emitSettle: Bool = true,
371
+ preserveScrimPin: Bool = false
368
372
  ) {
369
373
  guard index >= 0, index < detentSpecs.count else { return }
370
374
  targetIndex = index
375
+ if !preserveScrimPin {
376
+ scrimPinnedFull = false
377
+ }
371
378
 
372
379
  let currentTy = sheetContainer.transform.ty
373
380
  let targetTy = translationY(for: index)
@@ -391,6 +398,7 @@ public final class RNSBottomSheetHostingView: UIView {
391
398
  self.emitPosition()
392
399
  self.activeAnimator = nil
393
400
  self.activeAnimatorEmitsSettle = false
401
+ self.scrimPinnedFull = false
394
402
  self.setContentInteractionEnabled(true)
395
403
  self.updateInteractionState()
396
404
  if emitIndexChange {
@@ -411,6 +419,7 @@ public final class RNSBottomSheetHostingView: UIView {
411
419
  switch gesture.state {
412
420
  case .began:
413
421
  isPanning = true
422
+ scrimPinnedFull = false
414
423
  panStartingIndex = targetIndex
415
424
  sheetContainer.endEditing(true)
416
425
  setContentInteractionEnabled(false)
@@ -571,28 +580,35 @@ public final class RNSBottomSheetHostingView: UIView {
571
580
  detentSpecs.map(\.height).max()
572
581
  }
573
582
 
574
- private func resolveDetentSpecs() -> [DetentSpec] {
583
+ private func resolveDetentSpecs() -> [DetentSpec]? {
575
584
  let maxHeight = resolvedMaxDetentHeight
576
585
  let measuredContentHeight = maxHeight > 0 ? validContentHeight.map { min($0, maxHeight) } : nil
577
586
  let contentHeight = measuredContentHeight ?? maxHeight
578
- return rawDetentSpecs.enumerated().map { index, spec in
587
+ var resolvedDetents: [DetentSpec] = []
588
+ resolvedDetents.reserveCapacity(rawDetentSpecs.count)
589
+
590
+ for (index, spec) in rawDetentSpecs.enumerated() {
579
591
  let height: CGFloat
580
592
  switch spec.kind {
581
593
  case .points:
582
594
  if measuredContentHeight != nil, spec.value > contentHeight {
583
- NSException(
584
- name: NSExceptionName.invalidArgumentException,
585
- reason:
586
- "Invalid bottom sheet detent at index \(index): fixed detent \(spec.value) exceeds measured content height \(contentHeight).",
587
- userInfo: nil
588
- ).raise()
595
+ let message =
596
+ "Invalid bottom sheet detent at index \(index): fixed detent \(spec.value) exceeds measured content height \(contentHeight)."
597
+ if lastReportedInvalidDetentMessage != message {
598
+ lastReportedInvalidDetentMessage = message
599
+ eventDelegate?.bottomSheetHostingView(self, didReportError: message)
600
+ }
601
+ return nil
589
602
  }
590
603
  height = spec.value
591
604
  case .content:
592
605
  height = contentHeight
593
606
  }
594
- return DetentSpec(height: min(max(0, height), maxHeight), programmatic: spec.programmatic)
607
+ resolvedDetents.append(DetentSpec(height: min(max(0, height), maxHeight), programmatic: spec.programmatic))
595
608
  }
609
+
610
+ lastReportedInvalidDetentMessage = nil
611
+ return resolvedDetents
596
612
  }
597
613
 
598
614
  private func refreshDetentsFromLayout() {
@@ -602,13 +618,22 @@ public final class RNSBottomSheetHostingView: UIView {
602
618
  return
603
619
  }
604
620
 
605
- let resolvedDetents = resolveDetentSpecs()
621
+ guard let resolvedDetents = resolveDetentSpecs() else {
622
+ updateScrim()
623
+ return
624
+ }
606
625
  guard resolvedDetents != detentSpecs else {
607
626
  updateScrim()
608
627
  return
609
628
  }
610
629
 
611
630
  let previousMaxHeight = maximumResolvedDetentHeight ?? resolvedMaxDetentHeight
631
+ // Whether the scrim is currently fully opaque, i.e. the sheet is settled at
632
+ // or above the first non-zero detent. If so, a detent resize must not dip
633
+ // the scrim while the sheet re-anchors to the new geometry.
634
+ let wasScrimFull = modal
635
+ && firstNonZeroDetentHeight > 0
636
+ && currentSheetHeight + 0.5 >= firstNonZeroDetentHeight
612
637
  detentSpecs = resolvedDetents
613
638
 
614
639
  guard bounds.width > 0, bounds.height > 0, !detentSpecs.isEmpty else {
@@ -632,8 +657,15 @@ public final class RNSBottomSheetHostingView: UIView {
632
657
  let visibleHeight = previousMaxHeight - visualTy
633
658
  let reanchoredTy = min(max(newMaxHeight - visibleHeight, 0), newMaxHeight)
634
659
  sheetContainer.transform = CGAffineTransform(translationX: 0, y: reanchoredTy)
660
+ scrimPinnedFull = scrimPinnedFull || wasScrimFull
635
661
  emitPosition()
636
- snapToIndex(targetIndex, velocity: 0, emitIndexChange: false, emitSettle: shouldEmitSettle)
662
+ snapToIndex(
663
+ targetIndex,
664
+ velocity: 0,
665
+ emitIndexChange: false,
666
+ emitSettle: shouldEmitSettle,
667
+ preserveScrimPin: true
668
+ )
637
669
  } else {
638
670
  let currentVisibleHeight = previousMaxHeight - currentTranslationY
639
671
  let targetHeight = detent(at: targetIndex).height
@@ -647,8 +679,15 @@ public final class RNSBottomSheetHostingView: UIView {
647
679
  // up to the taller detent.
648
680
  let startTy = min(max(newMaxHeight - currentVisibleHeight, 0), newMaxHeight)
649
681
  sheetContainer.transform = CGAffineTransform(translationX: 0, y: startTy)
682
+ scrimPinnedFull = scrimPinnedFull || wasScrimFull
650
683
  emitPosition()
651
- snapToIndex(targetIndex, velocity: 0, emitIndexChange: false, emitSettle: false)
684
+ snapToIndex(
685
+ targetIndex,
686
+ velocity: 0,
687
+ emitIndexChange: false,
688
+ emitSettle: false,
689
+ preserveScrimPin: true
690
+ )
652
691
  }
653
692
  }
654
693
  }
@@ -786,6 +825,15 @@ private extension RNSBottomSheetHostingView {
786
825
  return
787
826
  }
788
827
 
828
+ // While the sheet is fully open and only its content/detent geometry is
829
+ // resizing, the position momentarily lags the grown detent height. Keep the
830
+ // scrim at full opacity instead of dipping it until the re-anchor settles.
831
+ if scrimPinnedFull {
832
+ scrimView.alpha = 1
833
+ scrimView.isHidden = false
834
+ return
835
+ }
836
+
789
837
  let threshold = firstNonZeroDetentHeight
790
838
  let progress: CGFloat
791
839
  if threshold <= 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.3",
3
+ "version": "0.10.0-next.5",
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",