@lodev09/react-native-true-sheet 3.6.10 → 3.7.0-beta.0

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
@@ -31,6 +31,7 @@ The true native bottom sheet experience for your React Native Apps. 💩
31
31
 
32
32
  - React Native >= 0.76 (Expo SDK 52+)
33
33
  - New Architecture enabled (default in RN 0.76+)
34
+ - Xcode 26.2 (strongly recommended for better library functionality)
34
35
 
35
36
  ### Expo
36
37
 
@@ -45,6 +46,27 @@ yarn add @lodev09/react-native-true-sheet
45
46
  cd ios && pod install
46
47
  ```
47
48
 
49
+ ### EAS Build (iOS)
50
+
51
+ When using [EAS Build](https://docs.expo.dev/build/introduction/) to build your iOS app, you must configure your `eas.json` to use a build image that includes Xcode 26.2. Use `"image": "latest"` or choose from the [available build images](https://docs.expo.dev/build-reference/infrastructure/#ios-server-images):
52
+
53
+ ```json
54
+ {
55
+ "build": {
56
+ "production": {
57
+ "ios": {
58
+ "image": "latest"
59
+ }
60
+ },
61
+ "development": {
62
+ "ios": {
63
+ "image": "latest"
64
+ }
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
48
70
  ## Documentation
49
71
 
50
72
  - [Example](example)
@@ -3,9 +3,11 @@ package com.lodev09.truesheet
3
3
  import android.annotation.SuppressLint
4
4
  import android.view.View
5
5
  import com.facebook.react.uimanager.ThemedReactContext
6
+ import com.facebook.react.uimanager.events.EventDispatcher
6
7
  import com.facebook.react.views.view.ReactViewGroup
7
8
 
8
9
  interface TrueSheetContainerViewDelegate {
10
+ val eventDispatcher: EventDispatcher?
9
11
  fun containerViewContentDidChangeSize(width: Int, height: Int)
10
12
  fun containerViewHeaderDidChangeSize(width: Int, height: Int)
11
13
  fun containerViewFooterDidChangeSize(width: Int, height: Int)
@@ -32,6 +34,9 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
32
34
  var headerHeight: Int = 0
33
35
  var footerHeight: Int = 0
34
36
 
37
+ override val eventDispatcher: EventDispatcher?
38
+ get() = delegate?.eventDispatcher
39
+
35
40
  init {
36
41
  // Allow footer to position outside container bounds
37
42
  clipChildren = false
@@ -11,10 +11,11 @@ import com.facebook.react.uimanager.events.EventDispatcher
11
11
  import com.facebook.react.views.view.ReactViewGroup
12
12
 
13
13
  /**
14
- * Delegate interface for footer view size changes
14
+ * Delegate interface for footer view size changes and event dispatching
15
15
  */
16
16
  interface TrueSheetFooterViewDelegate {
17
17
  fun footerViewDidChangeSize(width: Int, height: Int)
18
+ val eventDispatcher: EventDispatcher?
18
19
  }
19
20
 
20
21
  /**
@@ -30,7 +31,9 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
30
31
  RootView {
31
32
 
32
33
  var delegate: TrueSheetFooterViewDelegate? = null
33
- var eventDispatcher: EventDispatcher? = null
34
+
35
+ private val eventDispatcher: EventDispatcher?
36
+ get() = delegate?.eventDispatcher
34
37
 
35
38
  private var lastWidth = 0
36
39
  private var lastHeight = 0
@@ -42,7 +42,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
42
42
  private val containerView: TrueSheetContainerView?
43
43
  get() = viewController.getChildAt(0) as? TrueSheetContainerView
44
44
 
45
- var eventDispatcher: EventDispatcher? = null
45
+ override var eventDispatcher: EventDispatcher? = null
46
46
 
47
47
  // Initial present configuration (set by ViewManager before mount)
48
48
  var initialDetentIndex: Int = -1
@@ -66,7 +66,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
66
66
  private var isSheetUpdatePending: Boolean = false
67
67
 
68
68
  // Root container for the coordinator layout (activity or Modal dialog content view)
69
- private var rootContainerView: ViewGroup? = null
69
+ internal var rootContainerView: ViewGroup? = null
70
70
 
71
71
  // ==================== Initialization ====================
72
72
 
@@ -151,7 +151,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
151
151
  // ==================== Lifecycle ====================
152
152
 
153
153
  override fun onHostResume() {
154
- viewController.reapplyHiddenState()
155
154
  finalizeUpdates()
156
155
  }
157
156
 
@@ -278,7 +277,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
278
277
  if (!viewController.isPresented) {
279
278
  // Attach coordinator to the root container
280
279
  rootContainerView = findRootContainerView()
281
- viewController.coordinatorLayout?.let { rootContainerView?.addView(it) }
280
+ viewController.coordinatorLayout?.let { coordinator ->
281
+ rootContainerView?.addView(coordinator)
282
+ coordinator.post { measureCoordinatorLayout() }
283
+ }
282
284
 
283
285
  // Register with observer to track sheet stack hierarchy
284
286
  viewController.parentSheetView = TrueSheetStackManager.onSheetWillPresent(this, detentIndex)
@@ -384,10 +386,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
384
386
  override fun viewControllerWillPresent(index: Int, position: Float, detent: Float) {
385
387
  val surfaceId = UIManagerHelper.getSurfaceId(this)
386
388
  eventDispatcher?.dispatchEvent(WillPresentEvent(surfaceId, id, index, position, detent))
387
-
388
- // Enable touch event dispatching to React Native while sheet is visible
389
- viewController.eventDispatcher = eventDispatcher
390
- containerView?.footerView?.eventDispatcher = eventDispatcher
391
389
  }
392
390
 
393
391
  override fun viewControllerDidPresent(index: Int, position: Float, detent: Float) {
@@ -398,10 +396,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
398
396
  override fun viewControllerWillDismiss() {
399
397
  val surfaceId = UIManagerHelper.getSurfaceId(this)
400
398
  eventDispatcher?.dispatchEvent(WillDismissEvent(surfaceId, id))
401
-
402
- // Disable touch event dispatching when sheet is dismissing
403
- viewController.eventDispatcher = null
404
- containerView?.footerView?.eventDispatcher = null
405
399
  }
406
400
 
407
401
  override fun viewControllerDidDismiss(hadParent: Boolean) {
@@ -442,6 +436,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
442
436
 
443
437
  override fun viewControllerDidChangeSize(width: Int, height: Int) {
444
438
  updateState(width, height)
439
+ measureCoordinatorLayout()
445
440
  }
446
441
 
447
442
  override fun viewControllerWillFocus() {
@@ -486,6 +481,19 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
486
481
 
487
482
  // ==================== Private Helpers ====================
488
483
 
484
+ private fun measureCoordinatorLayout() {
485
+ val coordinator = viewController.coordinatorLayout ?: return
486
+ val width = viewController.screenWidth
487
+ val height = viewController.realScreenHeight
488
+
489
+ if (width > 0 && height > 0) {
490
+ val widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)
491
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
492
+ coordinator.measure(widthSpec, heightSpec)
493
+ coordinator.layout(0, 0, width, height)
494
+ }
495
+ }
496
+
489
497
  /**
490
498
  * Find the root container view for presenting the sheet.
491
499
  * This traverses up the view hierarchy to find the content view (android.R.id.content)
@@ -494,14 +502,20 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
494
502
  */
495
503
  private fun findRootContainerView(): ViewGroup? {
496
504
  var current: android.view.ViewParent? = parent
505
+ var contentView: ViewGroup? = null
497
506
 
498
507
  while (current != null) {
499
- if (current is ViewGroup && current.id == android.R.id.content) {
500
- return current
508
+ if (current is ViewGroup) {
509
+ if (current.javaClass.name == "com.swmansion.rnscreens.Screen") {
510
+ return current
511
+ }
512
+ if (contentView == null && current.id == android.R.id.content) {
513
+ contentView = current
514
+ }
501
515
  }
502
516
  current = current.parent
503
517
  }
504
518
 
505
- return reactContext.currentActivity?.findViewById(android.R.id.content)
519
+ return contentView ?: reactContext.currentActivity?.findViewById(android.R.id.content)
506
520
  }
507
521
  }
@@ -23,7 +23,6 @@ import com.facebook.react.util.RNLog
23
23
  import com.facebook.react.views.view.ReactViewGroup
24
24
  import com.google.android.material.bottomsheet.BottomSheetBehavior
25
25
  import com.lodev09.truesheet.core.GrabberOptions
26
- import com.lodev09.truesheet.core.RNScreensFragmentObserver
27
26
  import com.lodev09.truesheet.core.TrueSheetBottomSheetView
28
27
  import com.lodev09.truesheet.core.TrueSheetBottomSheetViewDelegate
29
28
  import com.lodev09.truesheet.core.TrueSheetCoordinatorLayout
@@ -45,6 +44,7 @@ import com.lodev09.truesheet.utils.ScreenUtils
45
44
  data class DetentInfo(val index: Int, val position: Float)
46
45
 
47
46
  interface TrueSheetViewControllerDelegate {
47
+ val eventDispatcher: EventDispatcher?
48
48
  fun viewControllerWillPresent(index: Int, position: Float, detent: Float)
49
49
  fun viewControllerDidPresent(index: Int, position: Float, detent: Float)
50
50
  fun viewControllerWillDismiss()
@@ -89,7 +89,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
89
89
  private const val DEFAULT_CORNER_RADIUS = 16 // dp
90
90
  private const val TRANSLATE_ANIMATION_DURATION = 200L
91
91
  private const val DISMISS_DURATION = 200L
92
- private const val MODAL_FADE_DURATION = 150L
93
92
  }
94
93
 
95
94
  // =============================================================================
@@ -129,7 +128,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
129
128
 
130
129
  private var interactionState: InteractionState = InteractionState.Idle
131
130
  private var isDismissing = false
132
- private var wasHiddenByModal = false
133
131
  private var shouldAnimatePresent = false
134
132
  private var isPresentAnimating = false
135
133
 
@@ -149,15 +147,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
149
147
 
150
148
  // Helper Objects
151
149
  private var keyboardObserver: TrueSheetKeyboardObserver? = null
152
- private var rnScreensObserver: RNScreensFragmentObserver? = null
153
150
  internal val detentCalculator = TrueSheetDetentCalculator(reactContext).apply {
154
151
  delegate = this@TrueSheetViewController
155
152
  }
156
153
 
157
154
  // Touch Dispatchers
158
- internal var eventDispatcher: EventDispatcher? = null
159
155
  private val jsTouchDispatcher = JSTouchDispatcher(this)
160
- private var jsPointerDispatcher: JSPointerDispatcher? = null
156
+ private val jsPointerDispatcher = JSPointerDispatcher(this)
157
+
158
+ private val eventDispatcher
159
+ get() = delegate?.eventDispatcher
161
160
 
162
161
  // Detent Configuration
163
162
  override var maxSheetHeight: Int? = null
@@ -284,14 +283,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
284
283
  private val dimViews: List<TrueSheetDimView>
285
284
  get() = listOfNotNull(dimView, parentDimView)
286
285
 
287
- // =============================================================================
288
- // MARK: - Initialization
289
- // =============================================================================
290
-
291
- init {
292
- jsPointerDispatcher = JSPointerDispatcher(this)
293
- }
294
-
295
286
  // =============================================================================
296
287
  // MARK: - Sheet Creation & Cleanup
297
288
  // =============================================================================
@@ -312,7 +303,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
312
303
 
313
304
  private fun cleanupSheet() {
314
305
  cleanupKeyboardObserver()
315
- cleanupModalObserver()
316
306
  cleanupBackCallback()
317
307
  sheetView?.animate()?.cancel()
318
308
 
@@ -332,7 +322,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
332
322
  isDismissing = false
333
323
  isPresented = false
334
324
  isSheetVisible = false
335
- wasHiddenByModal = false
336
325
  isPresentAnimating = false
337
326
  lastEmittedPositionPx = -1
338
327
  detentIndexBeforeKeyboard = -1
@@ -372,6 +361,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
372
361
  }
373
362
  }
374
363
 
364
+ override fun coordinatorLayoutDidChangeConfiguration() {
365
+ if (!isPresented) return
366
+
367
+ updateStateDimensions()
368
+ sheetView?.let { emitChangePositionDelegate(it.top, realtime = false) }
369
+ }
370
+
375
371
  // =============================================================================
376
372
  // MARK: - TrueSheetDimViewDelegate
377
373
  // =============================================================================
@@ -502,84 +498,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
502
498
  }
503
499
  }
504
500
 
505
- // =============================================================================
506
- // MARK: - Modal Observer (react-native-screens)
507
- // =============================================================================
508
-
509
- private fun setupModalObserver() {
510
- rnScreensObserver = RNScreensFragmentObserver(
511
- reactContext = reactContext,
512
- onModalPresented = {
513
- if (isPresented && isSheetVisible && isTopmostSheet) {
514
- hideForModal()
515
- }
516
- },
517
- onModalWillDismiss = {
518
- if (isPresented && wasHiddenByModal && isTopmostSheet) {
519
- showAfterModal()
520
- }
521
- },
522
- onModalDidDismiss = {
523
- if (isPresented && wasHiddenByModal) {
524
- wasHiddenByModal = false
525
- // Restore parent sheet after this sheet is restored
526
- parentSheetView?.viewController?.let { parent ->
527
- post { parent.showAfterModal() }
528
- }
529
- }
530
- }
531
- )
532
- rnScreensObserver?.start()
533
- }
534
-
535
- private fun cleanupModalObserver() {
536
- rnScreensObserver?.stop()
537
- rnScreensObserver = null
538
- }
539
-
540
- private fun setSheetVisibility(visible: Boolean) {
541
- coordinatorLayout?.visibility = if (visible) VISIBLE else GONE
542
- dimViews.forEach { it.visibility = if (visible) VISIBLE else INVISIBLE }
543
- }
544
-
545
- private fun hideForModal() {
546
- val sheet = sheetView ?: run {
547
- RNLog.e(reactContext, "TrueSheet: sheetView is null in hideForModal")
548
- return
549
- }
550
-
551
- isSheetVisible = false
552
- wasHiddenByModal = true
553
-
554
- dimViews.forEach { it.animate().alpha(0f).setDuration(MODAL_FADE_DURATION).start() }
555
- sheet.animate()
556
- .alpha(0f)
557
- .setDuration(MODAL_FADE_DURATION)
558
- .withEndAction {
559
- setSheetVisibility(false)
560
- }
561
- .start()
562
-
563
- // This will hide parent sheets first
564
- parentSheetView?.viewController?.hideForModal()
565
- }
566
-
567
- private fun showAfterModal() {
568
- isSheetVisible = true
569
- setSheetVisibility(true)
570
- sheetView?.alpha = 1f
571
- updateDimAmount(animated = true)
572
- }
573
-
574
- /**
575
- * Re-applies hidden state after returning from background.
576
- * Android may restore visibility on activity resume, so we need to hide it again.
577
- */
578
- fun reapplyHiddenState() {
579
- if (!wasHiddenByModal) return
580
- setSheetVisibility(false)
581
- }
582
-
583
501
  // =============================================================================
584
502
  // MARK: - Presentation
585
503
  // =============================================================================
@@ -611,7 +529,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
611
529
  setupSheetDetents()
612
530
  setupDimmedBackground(currentDetentIndex)
613
531
  setupKeyboardObserver()
614
- setupModalObserver()
615
532
  setupBackCallback()
616
533
 
617
534
  sheet.setupBackground()
@@ -754,15 +671,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
754
671
  animate = isPresented
755
672
  )
756
673
 
757
- val offset = if (expandedOffset == 0) topInset else 0
758
- val newHeight = realScreenHeight - expandedOffset - offset
759
- val newWidth = minOf(screenWidth, DEFAULT_MAX_WIDTH.dpToPx().toInt())
760
-
761
- if (lastStateWidth != newWidth || lastStateHeight != newHeight) {
762
- lastStateWidth = newWidth
763
- lastStateHeight = newHeight
764
- delegate?.viewControllerDidChangeSize(newWidth, newHeight)
765
- }
674
+ updateStateDimensions(expandedOffset)
766
675
 
767
676
  if (isPresented) {
768
677
  setStateForDetentIndex(currentDetentIndex)
@@ -885,7 +794,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
885
794
  // =============================================================================
886
795
 
887
796
  private fun shouldHandleKeyboard(checkFocus: Boolean = true): Boolean {
888
- if (wasHiddenByModal) return false
889
797
  if (!isTopmostSheet) return false
890
798
  if (checkFocus && !isFocusedViewWithinSheet()) return false
891
799
  return true
@@ -912,14 +820,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
912
820
  setupSheetDetents()
913
821
  if (!isDismissing && detentIndexBeforeKeyboard >= 0) {
914
822
  setStateForDetentIndex(detentIndexBeforeKeyboard)
915
- detentIndexBeforeKeyboard = -1
916
823
  }
917
824
  }
918
825
 
919
- override fun keyboardDidHide() {}
826
+ override fun keyboardDidHide() {
827
+ if (!shouldHandleKeyboard(checkFocus = false)) return
828
+ detentIndexBeforeKeyboard = -1
829
+ positionFooter()
830
+ }
920
831
 
921
832
  override fun keyboardDidChangeHeight(height: Int) {
922
- if (!shouldHandleKeyboard()) return
833
+ // Skip focus check if already handling keyboard (focus may be lost during hide)
834
+ val isHandlingKeyboard = detentIndexBeforeKeyboard >= 0
835
+ if (!shouldHandleKeyboard(checkFocus = !isHandlingKeyboard)) return
923
836
  positionFooter()
924
837
  }
925
838
  }
@@ -1008,6 +921,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1008
921
  // MARK: - Detent Helpers
1009
922
  // =============================================================================
1010
923
 
924
+ private fun updateStateDimensions(expandedOffset: Int? = null) {
925
+ val offset = expandedOffset ?: (realScreenHeight - detentCalculator.getDetentHeight(detents.last()))
926
+ val topOffset = if (offset == 0) topInset else 0
927
+ val newHeight = realScreenHeight - offset - topOffset
928
+ val newWidth = minOf(screenWidth, DEFAULT_MAX_WIDTH.dpToPx().toInt())
929
+
930
+ if (lastStateWidth != newWidth || lastStateHeight != newHeight) {
931
+ lastStateWidth = newWidth
932
+ lastStateHeight = newHeight
933
+ delegate?.viewControllerDidChangeSize(newWidth, newHeight)
934
+ }
935
+ }
936
+
1011
937
  fun translateSheet(translationY: Int) {
1012
938
  val sheet = sheetView ?: return
1013
939
 
@@ -1052,48 +978,26 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1052
978
  (getTag(R.id.react_test_id) as? String)?.let { info.viewIdResourceName = it }
1053
979
  }
1054
980
 
1055
- override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
1056
- super.onSizeChanged(w, h, oldw, oldh)
1057
-
1058
- if (w == oldw && h == oldh) return
1059
- if (!isPresented) return
1060
-
1061
- // Skip reconfiguration if expanded and only height changed (e.g., keyboard)
1062
- if (h + topInset >= screenHeight && isExpanded && oldw == w) return
1063
-
1064
- post {
1065
- setupSheetDetents()
1066
- positionFooter()
1067
- sheetView?.let { emitChangePositionDelegate(it.top, realtime = false) }
1068
- }
1069
- }
1070
-
1071
- override fun handleException(t: Throwable) {
1072
- reactContext.reactApplicationContext.handleException(RuntimeException(t))
1073
- }
1074
-
1075
981
  // =============================================================================
1076
- // MARK: - Touch Event Handling
982
+ // MARK: - RootView Touch Handling
1077
983
  // =============================================================================
1078
984
 
1079
985
  override fun dispatchTouchEvent(event: MotionEvent): Boolean {
1080
- // Footer needs special handling since it's positioned absolutely
1081
986
  val footer = containerView?.footerView
1082
- if (footer != null && footer.isVisible) {
987
+ if (footer != null && footer.isShown) {
1083
988
  val footerLocation = ScreenUtils.getScreenLocation(footer)
1084
- val touchScreenX = event.rawX.toInt()
1085
- val touchScreenY = event.rawY.toInt()
1086
-
1087
- // Check if touch is within footer bounds
1088
- if (touchScreenX >= footerLocation[0] &&
1089
- touchScreenX <= footerLocation[0] + footer.width &&
1090
- touchScreenY >= footerLocation[1] &&
1091
- touchScreenY <= footerLocation[1] + footer.height
989
+ val touchX = event.rawX.toInt()
990
+ val touchY = event.rawY.toInt()
991
+
992
+ if (touchX >= footerLocation[0] &&
993
+ touchX <= footerLocation[0] + footer.width &&
994
+ touchY >= footerLocation[1] &&
995
+ touchY <= footerLocation[1] + footer.height
1092
996
  ) {
1093
997
  val localEvent = MotionEvent.obtain(event)
1094
998
  localEvent.setLocation(
1095
- (touchScreenX - footerLocation[0]).toFloat(),
1096
- (touchScreenY - footerLocation[1]).toFloat()
999
+ (touchX - footerLocation[0]).toFloat(),
1000
+ (touchY - footerLocation[1]).toFloat()
1097
1001
  )
1098
1002
  val handled = footer.dispatchTouchEvent(localEvent)
1099
1003
  localEvent.recycle()
@@ -1106,7 +1010,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1106
1010
  override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
1107
1011
  eventDispatcher?.let {
1108
1012
  jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
1109
- jsPointerDispatcher?.handleMotionEvent(event, it, true)
1013
+ jsPointerDispatcher.handleMotionEvent(event, it, true)
1110
1014
  }
1111
1015
  return super.onInterceptTouchEvent(event)
1112
1016
  }
@@ -1114,35 +1018,35 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1114
1018
  override fun onTouchEvent(event: MotionEvent): Boolean {
1115
1019
  eventDispatcher?.let {
1116
1020
  jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
1117
- jsPointerDispatcher?.handleMotionEvent(event, it, false)
1021
+ jsPointerDispatcher.handleMotionEvent(event, it, false)
1118
1022
  }
1119
1023
  super.onTouchEvent(event)
1120
1024
  return true
1121
1025
  }
1122
1026
 
1123
1027
  override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
1124
- eventDispatcher?.let { jsPointerDispatcher?.handleMotionEvent(event, it, true) }
1028
+ eventDispatcher?.let { jsPointerDispatcher.handleMotionEvent(event, it, true) }
1125
1029
  return super.onHoverEvent(event)
1126
1030
  }
1127
1031
 
1128
1032
  override fun onHoverEvent(event: MotionEvent): Boolean {
1129
- eventDispatcher?.let { jsPointerDispatcher?.handleMotionEvent(event, it, false) }
1033
+ eventDispatcher?.let { jsPointerDispatcher.handleMotionEvent(event, it, false) }
1130
1034
  return super.onHoverEvent(event)
1131
1035
  }
1132
1036
 
1133
1037
  override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
1134
1038
  eventDispatcher?.let {
1135
1039
  jsTouchDispatcher.onChildStartedNativeGesture(ev, it)
1136
- jsPointerDispatcher?.onChildStartedNativeGesture(childView, ev, it)
1040
+ jsPointerDispatcher.onChildStartedNativeGesture(childView, ev, it)
1137
1041
  }
1138
1042
  }
1139
1043
 
1140
1044
  override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
1141
1045
  eventDispatcher?.let { jsTouchDispatcher.onChildEndedNativeGesture(ev, it) }
1142
- jsPointerDispatcher?.onChildEndedNativeGesture()
1046
+ jsPointerDispatcher.onChildEndedNativeGesture()
1143
1047
  }
1144
1048
 
1145
- override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
1146
- super.requestDisallowInterceptTouchEvent(disallowIntercept)
1049
+ override fun handleException(t: Throwable) {
1050
+ reactContext.reactApplicationContext.handleException(RuntimeException(t))
1147
1051
  }
1148
1052
  }
@@ -2,6 +2,7 @@ package com.lodev09.truesheet.core
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.content.Context
5
+ import android.content.res.Configuration
5
6
  import android.view.MotionEvent
6
7
  import android.view.ViewConfiguration
7
8
  import android.view.ViewGroup
@@ -12,6 +13,7 @@ import com.facebook.react.uimanager.ReactPointerEventsView
12
13
 
13
14
  interface TrueSheetCoordinatorLayoutDelegate {
14
15
  fun coordinatorLayoutDidLayout(changed: Boolean)
16
+ fun coordinatorLayoutDidChangeConfiguration()
15
17
  }
16
18
 
17
19
  /**
@@ -53,6 +55,11 @@ class TrueSheetCoordinatorLayout(context: Context) :
53
55
  delegate?.coordinatorLayoutDidLayout(changed)
54
56
  }
55
57
 
58
+ override fun onConfigurationChanged(newConfig: Configuration?) {
59
+ super.onConfigurationChanged(newConfig)
60
+ delegate?.coordinatorLayoutDidChangeConfiguration()
61
+ }
62
+
56
63
  override val pointerEvents: PointerEvents
57
64
  get() = PointerEvents.BOX_NONE
58
65
 
@@ -68,12 +68,11 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
68
68
 
69
69
  /**
70
70
  * Attaches this dim view to a target view group.
71
- * For CoordinatorLayout usage, pass null to use the default (activity's decor view).
72
71
  * For stacked sheets, pass the parent sheet's bottom sheet view with corner radius.
73
72
  */
74
- fun attach(view: ViewGroup? = null, cornerRadius: Float = 0f) {
73
+ fun attach(view: ViewGroup, cornerRadius: Float = 0f) {
75
74
  if (parent != null) return
76
- targetView = view ?: reactContext.currentActivity?.window?.decorView as? ViewGroup
75
+ targetView = view
77
76
 
78
77
  if (cornerRadius > 0f) {
79
78
  outlineProvider = object : ViewOutlineProvider() {
@@ -87,7 +86,19 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
87
86
  clipToOutline = false
88
87
  }
89
88
 
90
- targetView?.addView(this)
89
+ view.addView(this)
90
+
91
+ // Manually measure and layout for React Native views (they don't layout native children)
92
+ view.post {
93
+ val width = view.width
94
+ val height = view.height
95
+ if (width > 0 && height > 0) {
96
+ val widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
97
+ val heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
98
+ measure(widthSpec, heightSpec)
99
+ layout(0, 0, width, height)
100
+ }
101
+ }
91
102
  }
92
103
 
93
104
  /**
@@ -13,12 +13,18 @@ object TrueSheetStackManager {
13
13
  /**
14
14
  * Called when a sheet is about to be presented.
15
15
  * Returns the visible parent sheet to stack on, or null if none.
16
+ * Only returns a parent if it's in the same container (e.g., same Screen).
16
17
  */
17
18
  @JvmStatic
18
19
  fun onSheetWillPresent(sheetView: TrueSheetView, detentIndex: Int): TrueSheetView? {
19
20
  synchronized(presentedSheetStack) {
21
+ val rootContainer = sheetView.rootContainerView
20
22
  val parentSheet = presentedSheetStack.lastOrNull()
21
- ?.takeIf { it.viewController.isPresented && it.viewController.isSheetVisible }
23
+ ?.takeIf {
24
+ it.viewController.isPresented &&
25
+ it.viewController.isSheetVisible &&
26
+ it.rootContainerView == rootContainer
27
+ }
22
28
 
23
29
  val childSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(detentIndex)
24
30
  parentSheet?.updateTranslationForChild(childSheetTop)
@@ -48,6 +54,7 @@ object TrueSheetStackManager {
48
54
  /**
49
55
  * Called when a presented sheet's size changes (e.g., after setupSheetDetents).
50
56
  * Updates parent sheet translations to match the new sheet position.
57
+ * Only affects parent sheets in the same container.
51
58
  */
52
59
  @JvmStatic
53
60
  fun onSheetSizeChanged(sheetView: TrueSheetView) {
@@ -55,7 +62,9 @@ object TrueSheetStackManager {
55
62
  val index = presentedSheetStack.indexOf(sheetView)
56
63
  if (index <= 0) return
57
64
 
65
+ val rootContainer = sheetView.rootContainerView
58
66
  val parentSheet = presentedSheetStack[index - 1]
67
+ .takeIf { it.rootContainerView == rootContainer } ?: return
59
68
 
60
69
  // Post to ensure layout is complete before reading position
61
70
  sheetView.viewController.post {
@@ -71,15 +80,18 @@ object TrueSheetStackManager {
71
80
  }
72
81
 
73
82
  /**
74
- * Returns all sheets presented on top of the given sheet (children/descendants).
75
- * Returns them in reverse order (top-most first) for proper dismissal.
83
+ * Returns all sheets presented on top of the given sheet (children/descendants)
84
+ * that are in the same container. Returns them in reverse order (top-most first) for proper dismissal.
76
85
  */
77
86
  @JvmStatic
78
87
  fun getSheetsAbove(sheetView: TrueSheetView): List<TrueSheetView> {
79
88
  synchronized(presentedSheetStack) {
80
89
  val index = presentedSheetStack.indexOf(sheetView)
81
90
  if (index < 0 || index >= presentedSheetStack.size - 1) return emptyList()
82
- return presentedSheetStack.subList(index + 1, presentedSheetStack.size).reversed()
91
+ val rootContainer = sheetView.rootContainerView
92
+ return presentedSheetStack.subList(index + 1, presentedSheetStack.size)
93
+ .filter { it.rootContainerView == rootContainer }
94
+ .reversed()
83
95
  }
84
96
  }
85
97
 
@@ -99,23 +111,26 @@ object TrueSheetStackManager {
99
111
 
100
112
  /**
101
113
  * Gets the parent sheet of the given sheet, if any.
114
+ * Only returns a parent if it's in the same container.
102
115
  */
103
116
  @JvmStatic
104
117
  fun getParentSheet(sheetView: TrueSheetView): TrueSheetView? {
105
118
  synchronized(presentedSheetStack) {
106
119
  val index = presentedSheetStack.indexOf(sheetView)
107
120
  if (index <= 0) return null
108
- return presentedSheetStack[index - 1]
121
+ val rootContainer = sheetView.rootContainerView
122
+ return presentedSheetStack[index - 1].takeIf { it.rootContainerView == rootContainer }
109
123
  }
110
124
  }
111
125
 
112
126
  /**
113
- * Returns true if the given sheet is the topmost presented sheet.
127
+ * Returns true if the given sheet is the topmost presented sheet in its container.
114
128
  */
115
129
  @JvmStatic
116
130
  fun isTopmostSheet(sheetView: TrueSheetView): Boolean {
117
131
  synchronized(presentedSheetStack) {
118
- return presentedSheetStack.lastOrNull() == sheetView
132
+ val rootContainer = sheetView.rootContainerView
133
+ return presentedSheetStack.lastOrNull { it.rootContainerView == rootContainer } == sheetView
119
134
  }
120
135
  }
121
136
  }
@@ -22,6 +22,12 @@ class JSI_EXPORT TrueSheetViewShadowNode final
22
22
  using ConcreteViewShadowNode::ConcreteViewShadowNode;
23
23
 
24
24
  public:
25
+ static ShadowNodeTraits BaseTraits() {
26
+ auto traits = ConcreteViewShadowNode::BaseTraits();
27
+ traits.set(ShadowNodeTraits::Trait::RootNodeKind);
28
+ return traits;
29
+ }
30
+
25
31
  void adjustLayoutWithState();
26
32
  };
27
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodev09/react-native-true-sheet",
3
- "version": "3.6.10",
3
+ "version": "3.7.0-beta.0",
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",
@@ -1,181 +0,0 @@
1
- package com.lodev09.truesheet.core
2
-
3
- import android.content.Context
4
- import androidx.appcompat.app.AppCompatActivity
5
- import androidx.fragment.app.Fragment
6
- import androidx.fragment.app.FragmentManager
7
- import androidx.lifecycle.DefaultLifecycleObserver
8
- import androidx.lifecycle.LifecycleOwner
9
- import com.facebook.react.bridge.ReactContext
10
-
11
- private const val RN_SCREENS_PACKAGE = "com.swmansion.rnscreens"
12
-
13
- /**
14
- * Observes fragment lifecycle to detect react-native-screens modal presentation.
15
- * Automatically notifies when modals are presented/dismissed.
16
- */
17
- class RNScreensFragmentObserver(
18
- private val reactContext: ReactContext,
19
- private val onModalPresented: () -> Unit,
20
- private val onModalWillDismiss: () -> Unit,
21
- private val onModalDidDismiss: () -> Unit
22
- ) {
23
- private var fragmentLifecycleCallback: FragmentManager.FragmentLifecycleCallbacks? = null
24
- private var activityLifecycleObserver: DefaultLifecycleObserver? = null
25
- private val activeModalFragments: MutableSet<Fragment> = mutableSetOf()
26
- private var isActivityInForeground = true
27
- private var pendingDismissRunnable: Runnable? = null
28
-
29
- /**
30
- * Start observing fragment lifecycle events.
31
- */
32
- fun start() {
33
- val activity = reactContext.currentActivity as? AppCompatActivity ?: return
34
- val fragmentManager = activity.supportFragmentManager
35
-
36
- // Track activity foreground state to ignore fragment lifecycle events during background/foreground transitions
37
- activityLifecycleObserver = object : DefaultLifecycleObserver {
38
- override fun onResume(owner: LifecycleOwner) {
39
- isActivityInForeground = true
40
- }
41
-
42
- override fun onPause(owner: LifecycleOwner) {
43
- isActivityInForeground = false
44
- }
45
- }
46
- activity.lifecycle.addObserver(activityLifecycleObserver!!)
47
-
48
- fragmentLifecycleCallback = object : FragmentManager.FragmentLifecycleCallbacks() {
49
- override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
50
- super.onFragmentAttached(fm, f, context)
51
-
52
- // Ignore if app is resuming from background
53
- if (!isActivityInForeground) return
54
-
55
- if (isModalFragment(f) && !activeModalFragments.contains(f)) {
56
- // Cancel any pending dismiss since a modal is being presented
57
- cancelPendingDismiss()
58
-
59
- activeModalFragments.add(f)
60
-
61
- if (activeModalFragments.size == 1) {
62
- onModalPresented()
63
- }
64
- }
65
- }
66
-
67
- override fun onFragmentStopped(fm: FragmentManager, f: Fragment) {
68
- super.onFragmentStopped(fm, f)
69
-
70
- // Ignore if app is going to background (fragments stop with activity)
71
- if (!isActivityInForeground) return
72
-
73
- // Only trigger when fragment is being removed (not just stopped for navigation)
74
- if (activeModalFragments.contains(f) && f.isRemoving) {
75
- activeModalFragments.remove(f)
76
-
77
- if (activeModalFragments.isEmpty()) {
78
- // Post dismiss to allow fragment attach to cancel if navigation is happening
79
- schedulePendingDismiss()
80
- }
81
- }
82
- }
83
-
84
- override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
85
- super.onFragmentDestroyed(fm, f)
86
-
87
- if (activeModalFragments.isEmpty() && pendingDismissRunnable == null) {
88
- onModalDidDismiss()
89
- }
90
- }
91
- }
92
-
93
- fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallback!!, true)
94
- }
95
-
96
- /**
97
- * Stop observing and cleanup.
98
- */
99
- fun stop() {
100
- val activity = reactContext.currentActivity as? AppCompatActivity
101
-
102
- cancelPendingDismiss()
103
-
104
- fragmentLifecycleCallback?.let { callback ->
105
- activity?.supportFragmentManager?.unregisterFragmentLifecycleCallbacks(callback)
106
- }
107
- fragmentLifecycleCallback = null
108
-
109
- activityLifecycleObserver?.let { observer ->
110
- activity?.lifecycle?.removeObserver(observer)
111
- }
112
- activityLifecycleObserver = null
113
-
114
- activeModalFragments.clear()
115
- }
116
-
117
- private fun schedulePendingDismiss() {
118
- val activity = reactContext.currentActivity ?: return
119
- val decorView = activity.window?.decorView ?: return
120
-
121
- cancelPendingDismiss()
122
-
123
- pendingDismissRunnable = Runnable {
124
- pendingDismissRunnable = null
125
- if (activeModalFragments.isEmpty()) {
126
- onModalWillDismiss()
127
- }
128
- }
129
- decorView.post(pendingDismissRunnable)
130
- }
131
-
132
- private fun cancelPendingDismiss() {
133
- val activity = reactContext.currentActivity ?: return
134
- val decorView = activity.window?.decorView ?: return
135
-
136
- pendingDismissRunnable?.let {
137
- decorView.removeCallbacks(it)
138
- pendingDismissRunnable = null
139
- }
140
- }
141
-
142
- companion object {
143
- /**
144
- * Check if fragment is from react-native-screens.
145
- */
146
- private fun isScreensFragment(fragment: Fragment): Boolean = fragment.javaClass.name.startsWith(RN_SCREENS_PACKAGE)
147
-
148
- /**
149
- * Check if fragment is a react-native-screens modal (fullScreenModal, transparentModal, or formSheet).
150
- * Uses reflection to check the fragment's screen.stackPresentation property.
151
- */
152
- private fun isModalFragment(fragment: Fragment): Boolean {
153
- val className = fragment.javaClass.name
154
-
155
- if (!isScreensFragment(fragment)) {
156
- return false
157
- }
158
-
159
- // ScreenModalFragment is always a modal (used for formSheet with BottomSheetDialog)
160
- if (className.contains("ScreenModalFragment")) {
161
- return true
162
- }
163
-
164
- // For ScreenStackFragment, check the screen's stackPresentation via reflection
165
- try {
166
- val getScreenMethod = fragment.javaClass.getMethod("getScreen")
167
- val screen = getScreenMethod.invoke(fragment) ?: return false
168
-
169
- val getStackPresentationMethod = screen.javaClass.getMethod("getStackPresentation")
170
- val stackPresentation = getStackPresentationMethod.invoke(screen) ?: return false
171
-
172
- val presentationName = stackPresentation.toString()
173
- return presentationName == "MODAL" ||
174
- presentationName == "TRANSPARENT_MODAL" ||
175
- presentationName == "FORM_SHEET"
176
- } catch (e: Exception) {
177
- return false
178
- }
179
- }
180
- }
181
- }