@lodev09/react-native-true-sheet 3.6.7 → 3.6.9

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.
@@ -2,6 +2,7 @@ package com.lodev09.truesheet
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.view.View
5
+ import android.view.ViewGroup
5
6
  import android.view.ViewStructure
6
7
  import android.view.accessibility.AccessibilityEvent
7
8
  import androidx.annotation.UiThread
@@ -64,6 +65,9 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
64
65
  // Debounce flag to coalesce rapid layout changes into a single sheet update
65
66
  private var isSheetUpdatePending: Boolean = false
66
67
 
68
+ // Root container for the coordinator layout (activity or Modal dialog content view)
69
+ private var rootContainerView: ViewGroup? = null
70
+
67
71
  // ==================== Initialization ====================
68
72
 
69
73
  init {
@@ -131,6 +135,11 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
131
135
  val child = getChildAt(index)
132
136
  if (child is TrueSheetContainerView) {
133
137
  child.delegate = null
138
+
139
+ // Dismiss the sheet when container is removed
140
+ if (viewController.isPresented) {
141
+ viewController.dismiss(animated = false)
142
+ }
134
143
  }
135
144
  viewController.removeView(child)
136
145
  }
@@ -155,14 +164,12 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
155
164
  fun onDropInstance() {
156
165
  reactContext.removeLifecycleEventListener(this)
157
166
 
158
- if (viewController.isPresented) {
159
- viewController.dismiss(animated = false)
160
- }
167
+ viewController.dismiss()
168
+ viewController.delegate = null
161
169
 
162
170
  TrueSheetModule.unregisterView(id)
163
171
  TrueSheetStackManager.removeSheet(this)
164
172
 
165
- viewController.delegate = null
166
173
  didInitiallyPresent = false
167
174
  }
168
175
 
@@ -265,6 +272,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
265
272
  @UiThread
266
273
  fun present(detentIndex: Int, animated: Boolean = true, promiseCallback: () -> Unit) {
267
274
  if (!viewController.isPresented) {
275
+ // Attach coordinator to the root container
276
+ rootContainerView = findRootContainerView()
277
+ viewController.coordinatorLayout?.let { rootContainerView?.addView(it) }
278
+
268
279
  // Register with observer to track sheet stack hierarchy
269
280
  viewController.parentSheetView = TrueSheetStackManager.onSheetWillPresent(this, detentIndex)
270
281
  }
@@ -390,6 +401,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
390
401
  }
391
402
 
392
403
  override fun viewControllerDidDismiss(hadParent: Boolean) {
404
+ // Detach coordinator from the root container view
405
+ viewController.coordinatorLayout?.let { rootContainerView?.removeView(it) }
406
+ rootContainerView = null
407
+
393
408
  val surfaceId = UIManagerHelper.getSurfaceId(this)
394
409
  eventDispatcher?.dispatchEvent(DidDismissEvent(surfaceId, id))
395
410
 
@@ -464,4 +479,25 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
464
479
  // Footer changes don't affect detents, only reposition it
465
480
  viewController.positionFooter()
466
481
  }
482
+
483
+ // ==================== Private Helpers ====================
484
+
485
+ /**
486
+ * Find the root container view for presenting the sheet.
487
+ * This traverses up the view hierarchy to find the content view (android.R.id.content)
488
+ * of whichever window this view is in - whether that's the activity's window or a
489
+ * Modal's dialog window.
490
+ */
491
+ private fun findRootContainerView(): ViewGroup? {
492
+ var current: android.view.ViewParent? = parent
493
+
494
+ while (current != null) {
495
+ if (current is ViewGroup && current.id == android.R.id.content) {
496
+ return current
497
+ }
498
+ current = current.parent
499
+ }
500
+
501
+ return reactContext.currentActivity?.findViewById(android.R.id.content)
502
+ }
467
503
  }
@@ -110,7 +110,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
110
110
 
111
111
  // CoordinatorLayout components (replaces DialogFragment)
112
112
  internal var sheetView: TrueSheetBottomSheetView? = null
113
- private var coordinatorLayout: TrueSheetCoordinatorLayout? = null
113
+ internal var coordinatorLayout: TrueSheetCoordinatorLayout? = null
114
114
  private var dimView: TrueSheetDimView? = null
115
115
  private var parentDimView: TrueSheetDimView? = null
116
116
 
@@ -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
@@ -238,6 +237,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
238
237
  private val currentKeyboardInset: Int
239
238
  get() = keyboardObserver?.currentHeight ?: 0
240
239
 
240
+ private val isKeyboardTransitioning: Boolean
241
+ get() = keyboardObserver?.isTransitioning ?: false
242
+
243
+ private fun isFocusedViewWithinSheet(): Boolean {
244
+ val sheet = sheetView ?: return false
245
+ return keyboardObserver?.isFocusedViewWithinSheet(sheet) ?: false
246
+ }
247
+
241
248
  val bottomInset: Int
242
249
  get() = if (edgeToEdgeEnabled) ScreenUtils.getInsets(reactContext).bottom else 0
243
250
 
@@ -303,9 +310,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
303
310
  cleanupBackCallback()
304
311
  sheetView?.animate()?.cancel()
305
312
 
306
- // Remove from activity
307
- removeFromActivity()
308
-
309
313
  // Cleanup dim views
310
314
  dimView?.detach()
311
315
  dimView = null
@@ -323,19 +327,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
323
327
  isPresented = false
324
328
  isSheetVisible = false
325
329
  wasHiddenByModal = false
326
- isKeyboardTransitioning = false
327
330
  isPresentAnimating = false
328
331
  lastEmittedPositionPx = -1
329
332
  detentIndexBeforeKeyboard = -1
330
333
  shouldAnimatePresent = true
331
334
  }
332
335
 
333
- private fun removeFromActivity() {
334
- val coordinator = coordinatorLayout ?: return
335
- val contentView = reactContext.currentActivity?.findViewById<ViewGroup>(android.R.id.content)
336
- contentView?.removeView(coordinator)
337
- }
338
-
339
336
  // =============================================================================
340
337
  // MARK: - Back Button Handling
341
338
  // =============================================================================
@@ -603,15 +600,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
603
600
  // Setup sheet in coordinator layout
604
601
  setupSheetInCoordinator(coordinator, sheet)
605
602
 
606
- // Add coordinator to activity
607
- val activity = reactContext.currentActivity ?: run {
608
- RNLog.w(reactContext, "TrueSheet: No activity available for presentation.")
609
- return
610
- }
611
-
612
- val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
613
- contentView?.addView(coordinator)
614
-
615
603
  emitWillPresentEvents()
616
604
 
617
605
  setupSheetDetents()
@@ -890,9 +878,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
890
878
  // MARK: - Keyboard Handling
891
879
  // =============================================================================
892
880
 
893
- private fun shouldHandleKeyboard(): Boolean {
881
+ private fun shouldHandleKeyboard(checkFocus: Boolean = true): Boolean {
894
882
  if (wasHiddenByModal) return false
895
- return isTopmostSheet
883
+ if (!isTopmostSheet) return false
884
+ if (checkFocus && !isFocusedViewWithinSheet()) return false
885
+ return true
896
886
  }
897
887
 
898
888
  fun setupKeyboardObserver() {
@@ -904,7 +894,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
904
894
  keyboardObserver = TrueSheetKeyboardObserver(coordinator, reactContext).apply {
905
895
  delegate = object : TrueSheetKeyboardObserverDelegate {
906
896
  override fun keyboardWillShow(height: Int) {
907
- isKeyboardTransitioning = true
908
897
  if (!shouldHandleKeyboard()) return
909
898
  detentIndexBeforeKeyboard = currentDetentIndex
910
899
  setupSheetDetents()
@@ -912,7 +901,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
912
901
  }
913
902
 
914
903
  override fun keyboardWillHide() {
915
- if (!shouldHandleKeyboard()) return
904
+ if (!shouldHandleKeyboard(checkFocus = false)) return
905
+
916
906
  setupSheetDetents()
917
907
  if (!isDismissing && detentIndexBeforeKeyboard >= 0) {
918
908
  setStateForDetentIndex(detentIndexBeforeKeyboard)
@@ -920,9 +910,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
920
910
  }
921
911
  }
922
912
 
923
- override fun keyboardDidHide() {
924
- isKeyboardTransitioning = false
925
- }
913
+ override fun keyboardDidHide() {}
926
914
 
927
915
  override fun keyboardDidChangeHeight(height: Int) {
928
916
  if (!shouldHandleKeyboard()) return
@@ -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()
@@ -22,6 +22,7 @@ using namespace facebook::react;
22
22
  CGFloat _lastHeight;
23
23
  BOOL _didInitialLayout;
24
24
  NSLayoutConstraint *_bottomConstraint;
25
+ CGFloat _currentKeyboardOffset;
25
26
  }
26
27
 
27
28
  + (ComponentDescriptorProvider)componentDescriptorProvider {
@@ -39,6 +40,7 @@ using namespace facebook::react;
39
40
  _lastHeight = 0;
40
41
  _didInitialLayout = NO;
41
42
  _bottomConstraint = nil;
43
+ _currentKeyboardOffset = 0;
42
44
  }
43
45
  return self;
44
46
  }
@@ -59,8 +61,9 @@ using namespace facebook::react;
59
61
  [self.leadingAnchor constraintEqualToAnchor:parentView.leadingAnchor].active = YES;
60
62
  [self.trailingAnchor constraintEqualToAnchor:parentView.trailingAnchor].active = YES;
61
63
 
62
- // Store bottom constraint for keyboard adjustment
63
- _bottomConstraint = [self.bottomAnchor constraintEqualToAnchor:parentView.bottomAnchor];
64
+ // Store bottom constraint for keyboard adjustment, preserving current keyboard offset
65
+ _bottomConstraint = [self.bottomAnchor constraintEqualToAnchor:parentView.bottomAnchor
66
+ constant:-_currentKeyboardOffset];
64
67
  _bottomConstraint.active = YES;
65
68
 
66
69
  // Apply height constraint
@@ -109,6 +112,7 @@ using namespace facebook::react;
109
112
  _lastHeight = 0;
110
113
  _didInitialLayout = NO;
111
114
  _bottomConstraint = nil;
115
+ _currentKeyboardOffset = 0;
112
116
  }
113
117
 
114
118
  #pragma mark - Keyboard Handling
@@ -163,6 +167,9 @@ using namespace facebook::react;
163
167
  // Cap to ensure we don't go negative
164
168
  CGFloat bottomOffset = MAX(0, keyboardHeight);
165
169
 
170
+ // Store the current keyboard offset so it persists through constraint recreation
171
+ _currentKeyboardOffset = bottomOffset;
172
+
166
173
  [UIView animateWithDuration:duration
167
174
  delay:0
168
175
  options:curve | UIViewAnimationOptionBeginFromCurrentState
@@ -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.7",
3
+ "version": "3.6.9",
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",