@lodev09/react-native-true-sheet 3.9.0-beta.2 → 3.9.0-beta.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.
@@ -528,6 +528,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
528
528
  when (newState) {
529
529
  BottomSheetBehavior.STATE_DRAGGING -> handleDragBegin(sheetView)
530
530
 
531
+ BottomSheetBehavior.STATE_SETTLING -> handleSettling(sheetView)
532
+
531
533
  BottomSheetBehavior.STATE_EXPANDED,
532
534
  BottomSheetBehavior.STATE_COLLAPSED,
533
535
  BottomSheetBehavior.STATE_HALF_EXPANDED -> handleStateSettled(sheetView, newState)
@@ -581,7 +583,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
581
583
  val detent = detentCalculator.getDetentValueForIndex(detentInfo.index)
582
584
  delegate?.viewControllerDidDragEnd(detentInfo.index, detentInfo.position, detent)
583
585
 
584
- if (detentInfo.index != currentDetentIndex) {
586
+ // Skip detent change if keyboard inset is still active — detent mapping is unreliable.
587
+ // keyboardWillHide will recalculate detents and settle at the correct index.
588
+ if (detentInfo.index != currentDetentIndex && keyboardInset == 0) {
585
589
  currentDetentIndex = detentInfo.index
586
590
  setupDimmedBackground()
587
591
  delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
@@ -713,6 +717,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
713
717
  return
714
718
  }
715
719
 
720
+ // Commit the new index so keyboardWillHide restores to it instead of the stale one
721
+ if (detentIndexBeforeKeyboard >= 0) {
722
+ detentIndexBeforeKeyboard = detentIndex
723
+ }
724
+
716
725
  setupDimmedBackground()
717
726
  setStateForDetentIndex(detentIndex)
718
727
  resizePromise?.invoke()
@@ -1015,7 +1024,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1015
1024
 
1016
1025
  override fun keyboardWillHide() {
1017
1026
  if (!shouldHandleKeyboard(checkFocus = false)) return
1018
-
1019
1027
  setupSheetDetents()
1020
1028
  if (!isBeingDismissed && detentIndexBeforeKeyboard >= 0) {
1021
1029
  setStateForDetentIndex(detentIndexBeforeKeyboard)
@@ -1062,14 +1070,50 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1062
1070
  detentCalculator.getPositionDp(detentCalculator.getVisibleSheetHeight(sheetView.top))
1063
1071
 
1064
1072
  private fun handleDragBegin(sheetView: View) {
1073
+ detentIndexBeforeKeyboard = -1
1074
+
1065
1075
  val position = getPositionDpForView(sheetView)
1066
1076
  val detent = detentCalculator.getDetentValueForIndex(currentDetentIndex)
1067
1077
  delegate?.viewControllerDidDragBegin(currentDetentIndex, position, detent)
1068
1078
  interactionState = InteractionState.Dragging(startTop = sheetView.top)
1069
1079
  }
1070
1080
 
1081
+ private fun handleSettling(sheetView: View) {
1082
+ if (interactionState !is InteractionState.Dragging) return
1083
+ if (keyboardInset <= 0) return
1084
+
1085
+ // After drag release, check if the sheet was dragged past the midpoint between the
1086
+ // keyboard-expanded position and a non-keyboard detent position. If so, dismiss
1087
+ // keyboard and commit to that detent.
1088
+ val maxAvailableHeight = realScreenHeight - topInset
1089
+ val keyboardHeight = minOf(detentCalculator.getDetentHeight(detents.last()), maxAvailableHeight)
1090
+ val keyboardTop = realScreenHeight - keyboardHeight
1091
+
1092
+ for (i in detents.indices) {
1093
+ val nonKeyboardHeight = minOf(detentCalculator.getDetentHeight(detents[i], includeKeyboard = false), maxAvailableHeight)
1094
+ val nonKeyboardTop = realScreenHeight - nonKeyboardHeight
1095
+ val midpoint = keyboardTop + (nonKeyboardTop - keyboardTop) / 2
1096
+
1097
+ if (sheetView.top >= midpoint) {
1098
+ // Target position matches the keyboard-expanded position — keep keyboard open
1099
+ if (nonKeyboardTop == keyboardTop) break
1100
+
1101
+ detentIndexBeforeKeyboard = -1
1102
+ currentDetentIndex = i
1103
+ dismissKeyboard()
1104
+ setupDimmedBackground()
1105
+
1106
+ val position = getPositionDpForView(sheetView)
1107
+ val detent = detentCalculator.getDetentValueForIndex(i)
1108
+ delegate?.viewControllerDidChangeDetent(i, position, detent)
1109
+ break
1110
+ }
1111
+ }
1112
+ }
1113
+
1071
1114
  private fun handleDragChange(sheetView: View) {
1072
1115
  if (interactionState !is InteractionState.Dragging) return
1116
+
1073
1117
  val position = getPositionDpForView(sheetView)
1074
1118
  val detent = detentCalculator.getDetentValueForIndex(currentDetentIndex)
1075
1119
  delegate?.viewControllerDidDragChange(currentDetentIndex, position, detent)
@@ -39,7 +39,7 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
39
39
  * Calculate the height in pixels for a given detent value.
40
40
  * @param detent The detent value: -1.0 for content-fit, or 0.0-1.0 for screen fraction
41
41
  */
42
- fun getDetentHeight(detent: Double): Int {
42
+ fun getDetentHeight(detent: Double, includeKeyboard: Boolean = true): Int {
43
43
  val baseHeight = if (detent == -1.0) {
44
44
  contentHeight + headerHeight + contentBottomInset
45
45
  } else {
@@ -49,7 +49,7 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
49
49
  (detent * screenHeight).toInt() + contentBottomInset
50
50
  }
51
51
 
52
- val height = baseHeight + keyboardInset
52
+ val height = if (includeKeyboard) baseHeight + keyboardInset else baseHeight
53
53
  val maxAllowedHeight = screenHeight + contentBottomInset
54
54
  return maxContentHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
55
55
  }
@@ -291,6 +291,8 @@ using namespace facebook::react;
291
291
  stateData.containerHeight = static_cast<float>(size.height);
292
292
 
293
293
  #if REACT_NATIVE_VERSION_MINOR >= 82
294
+ // TODO: RN 0.82+ processes state updates in the same layout pass (synchronous).
295
+ // Once stable, we can drop native layout constraints in favor of synchronous Yoga layout.
294
296
  _state->updateState(std::move(stateData), facebook::react::EventQueue::UpdateMode::unstable_Immediate);
295
297
  #else
296
298
  _state->updateState(std::move(stateData));
@@ -669,7 +671,12 @@ using namespace facebook::react;
669
671
  - (void)presenterScreenWillAppear {
670
672
  if (_dismissedByNavigation && !_controller.isPresented && !_controller.isBeingPresented) {
671
673
  _dismissedByNavigation = NO;
672
- _pendingNavigationRepresent = YES;
674
+
675
+ if (self.window) {
676
+ [self presentAtIndex:_controller.activeDetentIndex animated:YES completion:nil];
677
+ } else {
678
+ _pendingNavigationRepresent = YES;
679
+ }
673
680
  }
674
681
  }
675
682
 
@@ -34,7 +34,6 @@ NS_ASSUME_NONNULL_BEGIN
34
34
  - (void)stopObserving;
35
35
 
36
36
  - (void)capturePresenterScreenFromView:(UIView *)view;
37
- - (BOOL)shouldDismissForScreenTag:(NSInteger)screenTag;
38
37
 
39
38
  @end
40
39
 
@@ -25,6 +25,8 @@ using namespace facebook::react;
25
25
  __weak UIViewController *_parentScreenController;
26
26
  __weak UIWindow *_window;
27
27
  BOOL _dismissedByNavigation;
28
+ BOOL _pendingInteractiveDismiss;
29
+ BOOL _pendingInteractiveAppear;
28
30
  }
29
31
 
30
32
  - (instancetype)init {
@@ -60,19 +62,44 @@ using namespace facebook::react;
60
62
 
61
63
  if (auto family = event.shadowNodeFamily.lock()) {
62
64
  Tag screenTag = family->getTag();
65
+ if (![strongSelf->_screenTags containsObject:@(screenTag)]) {
66
+ return false;
67
+ }
68
+
69
+ BOOL interactive = [strongSelf isInteractiveTransition];
63
70
 
64
71
  if (event.type == "topWillDisappear") {
65
- if ([strongSelf->_screenTags containsObject:@(screenTag)]) {
66
- if ([strongSelf shouldDismissForScreenTag:screenTag]) {
67
- strongSelf->_dismissedByNavigation = YES;
68
- [strongSelf.delegate presenterScreenWillDisappear];
69
- }
72
+ if (interactive) {
73
+ strongSelf->_pendingInteractiveDismiss = YES;
74
+ } else if ([strongSelf shouldDismissForScreenTag:screenTag]) {
75
+ strongSelf->_dismissedByNavigation = YES;
76
+ [strongSelf.delegate presenterScreenWillDisappear];
77
+ }
78
+ } else if (event.type == "topDisappear") {
79
+ if (strongSelf->_pendingInteractiveDismiss) {
80
+ strongSelf->_pendingInteractiveDismiss = NO;
81
+ strongSelf->_dismissedByNavigation = YES;
82
+ [strongSelf.delegate presenterScreenWillDisappear];
70
83
  }
71
84
  } else if (event.type == "topWillAppear") {
72
- if ([strongSelf->_screenTags containsObject:@(screenTag)] && strongSelf->_dismissedByNavigation) {
85
+ if (strongSelf->_dismissedByNavigation) {
86
+ if (interactive) {
87
+ strongSelf->_pendingInteractiveAppear = YES;
88
+ } else {
89
+ strongSelf->_dismissedByNavigation = NO;
90
+ [strongSelf.delegate presenterScreenWillAppear];
91
+ }
92
+ }
93
+ strongSelf->_pendingInteractiveDismiss = NO;
94
+ } else if (event.type == "topAppear") {
95
+ if (strongSelf->_pendingInteractiveAppear) {
96
+ strongSelf->_pendingInteractiveAppear = NO;
73
97
  strongSelf->_dismissedByNavigation = NO;
74
98
  [strongSelf.delegate presenterScreenWillAppear];
75
99
  }
100
+ } else if (event.type == "topGestureCancel") {
101
+ strongSelf->_pendingInteractiveDismiss = NO;
102
+ strongSelf->_pendingInteractiveAppear = NO;
76
103
  }
77
104
  }
78
105
  return false;
@@ -121,6 +148,11 @@ using namespace facebook::react;
121
148
  }
122
149
  }
123
150
 
151
+ - (BOOL)isInteractiveTransition {
152
+ UINavigationController *navController = _presenterScreenController.navigationController;
153
+ return navController.transitionCoordinator.isInteractive;
154
+ }
155
+
124
156
  - (BOOL)shouldDismissForScreenTag:(NSInteger)screenTag {
125
157
  // For parent screens (not immediate presenter), check if the presenter screen is being removed
126
158
  // This handles nested stack removal case
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodev09/react-native-true-sheet",
3
- "version": "3.9.0-beta.2",
3
+ "version": "3.9.0-beta.3",
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",