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

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.
@@ -1,8 +1,10 @@
1
1
  package com.lodev09.truesheet
2
2
 
3
3
  import com.facebook.react.module.annotations.ReactModule
4
+ import com.facebook.react.uimanager.PointerEvents
4
5
  import com.facebook.react.uimanager.ThemedReactContext
5
6
  import com.facebook.react.uimanager.ViewGroupManager
7
+ import com.facebook.react.uimanager.annotations.ReactProp
6
8
 
7
9
  /**
8
10
  * ViewManager for TrueSheetContainerView
@@ -15,6 +17,11 @@ class TrueSheetContainerViewManager : ViewGroupManager<TrueSheetContainerView>()
15
17
 
16
18
  override fun createViewInstance(reactContext: ThemedReactContext): TrueSheetContainerView = TrueSheetContainerView(reactContext)
17
19
 
20
+ @ReactProp(name = "pointerEvents")
21
+ fun setPointerEvents(view: TrueSheetContainerView, pointerEventsStr: String?) {
22
+ view.pointerEvents = PointerEvents.parsePointerEvents(pointerEventsStr)
23
+ }
24
+
18
25
  companion object {
19
26
  const val REACT_CLASS = "TrueSheetContainerView"
20
27
  }
@@ -1,8 +1,10 @@
1
1
  package com.lodev09.truesheet
2
2
 
3
3
  import com.facebook.react.module.annotations.ReactModule
4
+ import com.facebook.react.uimanager.PointerEvents
4
5
  import com.facebook.react.uimanager.ThemedReactContext
5
6
  import com.facebook.react.uimanager.ViewGroupManager
7
+ import com.facebook.react.uimanager.annotations.ReactProp
6
8
 
7
9
  /**
8
10
  * ViewManager for TrueSheetContentView
@@ -15,6 +17,11 @@ class TrueSheetContentViewManager : ViewGroupManager<TrueSheetContentView>() {
15
17
 
16
18
  override fun createViewInstance(reactContext: ThemedReactContext): TrueSheetContentView = TrueSheetContentView(reactContext)
17
19
 
20
+ @ReactProp(name = "pointerEvents")
21
+ fun setPointerEvents(view: TrueSheetContentView, pointerEventsStr: String?) {
22
+ view.pointerEvents = PointerEvents.parsePointerEvents(pointerEventsStr)
23
+ }
24
+
18
25
  companion object {
19
26
  const val REACT_CLASS = "TrueSheetContentView"
20
27
  }
@@ -1,8 +1,10 @@
1
1
  package com.lodev09.truesheet
2
2
 
3
3
  import com.facebook.react.module.annotations.ReactModule
4
+ import com.facebook.react.uimanager.PointerEvents
4
5
  import com.facebook.react.uimanager.ThemedReactContext
5
6
  import com.facebook.react.uimanager.ViewGroupManager
7
+ import com.facebook.react.uimanager.annotations.ReactProp
6
8
 
7
9
  /**
8
10
  * ViewManager for TrueSheetFooterView
@@ -15,6 +17,11 @@ class TrueSheetFooterViewManager : ViewGroupManager<TrueSheetFooterView>() {
15
17
 
16
18
  override fun createViewInstance(reactContext: ThemedReactContext): TrueSheetFooterView = TrueSheetFooterView(reactContext)
17
19
 
20
+ @ReactProp(name = "pointerEvents")
21
+ fun setPointerEvents(view: TrueSheetFooterView, pointerEventsStr: String?) {
22
+ view.pointerEvents = PointerEvents.parsePointerEvents(pointerEventsStr)
23
+ }
24
+
18
25
  companion object {
19
26
  const val REACT_CLASS = "TrueSheetFooterView"
20
27
  }
@@ -1,8 +1,10 @@
1
1
  package com.lodev09.truesheet
2
2
 
3
3
  import com.facebook.react.module.annotations.ReactModule
4
+ import com.facebook.react.uimanager.PointerEvents
4
5
  import com.facebook.react.uimanager.ThemedReactContext
5
6
  import com.facebook.react.uimanager.ViewGroupManager
7
+ import com.facebook.react.uimanager.annotations.ReactProp
6
8
 
7
9
  /**
8
10
  * ViewManager for TrueSheetHeaderView
@@ -15,6 +17,11 @@ class TrueSheetHeaderViewManager : ViewGroupManager<TrueSheetHeaderView>() {
15
17
 
16
18
  override fun createViewInstance(reactContext: ThemedReactContext): TrueSheetHeaderView = TrueSheetHeaderView(reactContext)
17
19
 
20
+ @ReactProp(name = "pointerEvents")
21
+ fun setPointerEvents(view: TrueSheetHeaderView, pointerEventsStr: String?) {
22
+ view.pointerEvents = PointerEvents.parsePointerEvents(pointerEventsStr)
23
+ }
24
+
18
25
  companion object {
19
26
  const val REACT_CLASS = "TrueSheetHeaderView"
20
27
  }
@@ -151,6 +151,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
151
151
  // ==================== Lifecycle ====================
152
152
 
153
153
  override fun onHostResume() {
154
+ viewController.reapplyHiddenState()
154
155
  finalizeUpdates()
155
156
  }
156
157
 
@@ -277,10 +278,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
277
278
  if (!viewController.isPresented) {
278
279
  // Attach coordinator to the root container
279
280
  rootContainerView = findRootContainerView()
280
- viewController.coordinatorLayout?.let { coordinator ->
281
- rootContainerView?.addView(coordinator)
282
- coordinator.post { measureCoordinatorLayout() }
283
- }
281
+ viewController.coordinatorLayout?.let { rootContainerView?.addView(it) }
284
282
 
285
283
  // Register with observer to track sheet stack hierarchy
286
284
  viewController.parentSheetView = TrueSheetStackManager.onSheetWillPresent(this, detentIndex)
@@ -436,7 +434,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
436
434
 
437
435
  override fun viewControllerDidChangeSize(width: Int, height: Int) {
438
436
  updateState(width, height)
439
- measureCoordinatorLayout()
440
437
  }
441
438
 
442
439
  override fun viewControllerWillFocus() {
@@ -481,19 +478,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
481
478
 
482
479
  // ==================== Private Helpers ====================
483
480
 
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
-
497
481
  /**
498
482
  * Find the root container view for presenting the sheet.
499
483
  * This traverses up the view hierarchy to find the content view (android.R.id.content)
@@ -502,20 +486,14 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
502
486
  */
503
487
  private fun findRootContainerView(): ViewGroup? {
504
488
  var current: android.view.ViewParent? = parent
505
- var contentView: ViewGroup? = null
506
489
 
507
490
  while (current != null) {
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
- }
491
+ if (current is ViewGroup && current.id == android.R.id.content) {
492
+ return current
515
493
  }
516
494
  current = current.parent
517
495
  }
518
496
 
519
- return contentView ?: reactContext.currentActivity?.findViewById(android.R.id.content)
497
+ return reactContext.currentActivity?.findViewById(android.R.id.content)
520
498
  }
521
499
  }
@@ -23,6 +23,7 @@ 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
26
27
  import com.lodev09.truesheet.core.TrueSheetBottomSheetView
27
28
  import com.lodev09.truesheet.core.TrueSheetBottomSheetViewDelegate
28
29
  import com.lodev09.truesheet.core.TrueSheetCoordinatorLayout
@@ -89,6 +90,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
89
90
  private const val DEFAULT_CORNER_RADIUS = 16 // dp
90
91
  private const val TRANSLATE_ANIMATION_DURATION = 200L
91
92
  private const val DISMISS_DURATION = 200L
93
+ private const val MODAL_FADE_DURATION = 150L
92
94
  }
93
95
 
94
96
  // =============================================================================
@@ -128,6 +130,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
128
130
 
129
131
  private var interactionState: InteractionState = InteractionState.Idle
130
132
  private var isDismissing = false
133
+ private var wasHiddenByModal = false
131
134
  private var shouldAnimatePresent = false
132
135
  private var isPresentAnimating = false
133
136
 
@@ -147,6 +150,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
147
150
 
148
151
  // Helper Objects
149
152
  private var keyboardObserver: TrueSheetKeyboardObserver? = null
153
+ private var rnScreensObserver: RNScreensFragmentObserver? = null
150
154
  internal val detentCalculator = TrueSheetDetentCalculator(reactContext).apply {
151
155
  delegate = this@TrueSheetViewController
152
156
  }
@@ -303,6 +307,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
303
307
 
304
308
  private fun cleanupSheet() {
305
309
  cleanupKeyboardObserver()
310
+ cleanupModalObserver()
306
311
  cleanupBackCallback()
307
312
  sheetView?.animate()?.cancel()
308
313
 
@@ -322,6 +327,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
322
327
  isDismissing = false
323
328
  isPresented = false
324
329
  isSheetVisible = false
330
+ wasHiddenByModal = false
325
331
  isPresentAnimating = false
326
332
  lastEmittedPositionPx = -1
327
333
  detentIndexBeforeKeyboard = -1
@@ -498,6 +504,84 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
498
504
  }
499
505
  }
500
506
 
507
+ // =============================================================================
508
+ // MARK: - Modal Observer (react-native-screens)
509
+ // =============================================================================
510
+
511
+ private fun setupModalObserver() {
512
+ rnScreensObserver = RNScreensFragmentObserver(
513
+ reactContext = reactContext,
514
+ onModalPresented = {
515
+ if (isPresented && isSheetVisible && isTopmostSheet) {
516
+ hideForModal()
517
+ }
518
+ },
519
+ onModalWillDismiss = {
520
+ if (isPresented && wasHiddenByModal && isTopmostSheet) {
521
+ showAfterModal()
522
+ }
523
+ },
524
+ onModalDidDismiss = {
525
+ if (isPresented && wasHiddenByModal) {
526
+ wasHiddenByModal = false
527
+ // Restore parent sheet after this sheet is restored
528
+ parentSheetView?.viewController?.let { parent ->
529
+ post { parent.showAfterModal() }
530
+ }
531
+ }
532
+ }
533
+ )
534
+ rnScreensObserver?.start()
535
+ }
536
+
537
+ private fun cleanupModalObserver() {
538
+ rnScreensObserver?.stop()
539
+ rnScreensObserver = null
540
+ }
541
+
542
+ private fun setSheetVisibility(visible: Boolean) {
543
+ coordinatorLayout?.visibility = if (visible) VISIBLE else GONE
544
+ dimViews.forEach { it.visibility = if (visible) VISIBLE else INVISIBLE }
545
+ }
546
+
547
+ private fun hideForModal() {
548
+ val sheet = sheetView ?: run {
549
+ RNLog.e(reactContext, "TrueSheet: sheetView is null in hideForModal")
550
+ return
551
+ }
552
+
553
+ isSheetVisible = false
554
+ wasHiddenByModal = true
555
+
556
+ dimViews.forEach { it.animate().alpha(0f).setDuration(MODAL_FADE_DURATION).start() }
557
+ sheet.animate()
558
+ .alpha(0f)
559
+ .setDuration(MODAL_FADE_DURATION)
560
+ .withEndAction {
561
+ setSheetVisibility(false)
562
+ }
563
+ .start()
564
+
565
+ // This will hide parent sheets first
566
+ parentSheetView?.viewController?.hideForModal()
567
+ }
568
+
569
+ private fun showAfterModal() {
570
+ isSheetVisible = true
571
+ setSheetVisibility(true)
572
+ sheetView?.alpha = 1f
573
+ updateDimAmount(animated = true)
574
+ }
575
+
576
+ /**
577
+ * Re-applies hidden state after returning from background.
578
+ * Android may restore visibility on activity resume, so we need to hide it again.
579
+ */
580
+ fun reapplyHiddenState() {
581
+ if (!wasHiddenByModal) return
582
+ setSheetVisibility(false)
583
+ }
584
+
501
585
  // =============================================================================
502
586
  // MARK: - Presentation
503
587
  // =============================================================================
@@ -529,6 +613,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
529
613
  setupSheetDetents()
530
614
  setupDimmedBackground(currentDetentIndex)
531
615
  setupKeyboardObserver()
616
+ setupModalObserver()
532
617
  setupBackCallback()
533
618
 
534
619
  sheet.setupBackground()
@@ -794,6 +879,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
794
879
  // =============================================================================
795
880
 
796
881
  private fun shouldHandleKeyboard(checkFocus: Boolean = true): Boolean {
882
+ if (wasHiddenByModal) return false
797
883
  if (!isTopmostSheet) return false
798
884
  if (checkFocus && !isFocusedViewWithinSheet()) return false
799
885
  return true
@@ -0,0 +1,181 @@
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
+ }
@@ -68,11 +68,12 @@ 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).
71
72
  * For stacked sheets, pass the parent sheet's bottom sheet view with corner radius.
72
73
  */
73
- fun attach(view: ViewGroup, cornerRadius: Float = 0f) {
74
+ fun attach(view: ViewGroup? = null, cornerRadius: Float = 0f) {
74
75
  if (parent != null) return
75
- targetView = view
76
+ targetView = view ?: reactContext.currentActivity?.window?.decorView as? ViewGroup
76
77
 
77
78
  if (cornerRadius > 0f) {
78
79
  outlineProvider = object : ViewOutlineProvider() {
@@ -86,19 +87,7 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
86
87
  clipToOutline = false
87
88
  }
88
89
 
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
- }
90
+ targetView?.addView(this)
102
91
  }
103
92
 
104
93
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodev09/react-native-true-sheet",
3
- "version": "3.7.0-beta.0",
3
+ "version": "3.7.0-beta.2",
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",