@lodev09/react-native-true-sheet 3.7.0-beta.5 → 3.7.1

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.
Files changed (45) hide show
  1. package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +25 -0
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +34 -16
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +60 -3
  4. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +26 -1
  5. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +32 -0
  6. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +26 -10
  7. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetKeyboardObserver.kt +2 -2
  8. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +26 -3
  9. package/ios/TrueSheetModule.mm +39 -0
  10. package/ios/TrueSheetView.h +4 -0
  11. package/ios/TrueSheetView.mm +49 -0
  12. package/ios/TrueSheetViewController.h +16 -0
  13. package/ios/TrueSheetViewController.mm +16 -0
  14. package/lib/module/TrueSheet.js +10 -0
  15. package/lib/module/TrueSheet.js.map +1 -1
  16. package/lib/module/TrueSheet.web.js +34 -7
  17. package/lib/module/TrueSheet.web.js.map +1 -1
  18. package/lib/module/TrueSheetProvider.js +2 -1
  19. package/lib/module/TrueSheetProvider.js.map +1 -1
  20. package/lib/module/TrueSheetProvider.web.js +24 -1
  21. package/lib/module/TrueSheetProvider.web.js.map +1 -1
  22. package/lib/module/mocks/index.js +3 -1
  23. package/lib/module/mocks/index.js.map +1 -1
  24. package/lib/module/specs/NativeTrueSheetModule.js.map +1 -1
  25. package/lib/typescript/src/TrueSheet.d.ts +7 -0
  26. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  27. package/lib/typescript/src/TrueSheet.types.d.ts +4 -0
  28. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  29. package/lib/typescript/src/TrueSheet.web.d.ts +9 -1
  30. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  31. package/lib/typescript/src/TrueSheetProvider.d.ts.map +1 -1
  32. package/lib/typescript/src/TrueSheetProvider.web.d.ts +5 -0
  33. package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
  34. package/lib/typescript/src/mocks/index.d.ts +1 -0
  35. package/lib/typescript/src/mocks/index.d.ts.map +1 -1
  36. package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts +6 -0
  37. package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/TrueSheet.tsx +10 -0
  40. package/src/TrueSheet.types.ts +4 -0
  41. package/src/TrueSheet.web.tsx +50 -6
  42. package/src/TrueSheetProvider.tsx +1 -0
  43. package/src/TrueSheetProvider.web.tsx +26 -0
  44. package/src/mocks/index.ts +2 -0
  45. package/src/specs/NativeTrueSheetModule.ts +7 -0
@@ -95,6 +95,31 @@ class TrueSheetModule(reactContext: ReactApplicationContext) :
95
95
  }
96
96
  }
97
97
 
98
+ /**
99
+ * Dismiss all presented sheets by dismissing from the bottom of the stack
100
+ *
101
+ * @param animated Whether to animate the dismissals
102
+ * @param promise Promise that resolves when all sheets are dismissed
103
+ */
104
+ @ReactMethod
105
+ fun dismissAll(animated: Boolean, promise: Promise) {
106
+ Handler(Looper.getMainLooper()).post {
107
+ try {
108
+ val rootSheet = TrueSheetStackManager.getRootSheet()
109
+ if (rootSheet == null) {
110
+ promise.resolve(null)
111
+ return@post
112
+ }
113
+
114
+ rootSheet.dismissAll(animated) {
115
+ promise.resolve(null)
116
+ }
117
+ } catch (e: Exception) {
118
+ promise.reject("OPERATION_FAILED", "Failed to dismiss all sheets: ${e.message}", e)
119
+ }
120
+ }
121
+ }
122
+
98
123
  /**
99
124
  * Helper method to get TrueSheetView by tag and execute closure
100
125
  */
@@ -39,10 +39,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
39
39
  // ==================== Properties ====================
40
40
 
41
41
  internal val viewController: TrueSheetViewController = TrueSheetViewController(reactContext)
42
-
43
- private val containerView: TrueSheetContainerView?
44
- get() = viewController.getChildAt(0) as? TrueSheetContainerView
45
-
46
42
  override var eventDispatcher: EventDispatcher? = null
47
43
 
48
44
  // Initial present configuration (set by ViewManager before mount)
@@ -81,10 +77,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
81
77
 
82
78
  // ==================== ReactViewGroup Overrides ====================
83
79
 
84
- override fun dispatchProvideStructure(structure: ViewStructure) {
85
- super.dispatchProvideStructure(structure)
86
- }
87
-
88
80
  override fun onLayout(
89
81
  changed: Boolean,
90
82
  left: Int,
@@ -117,6 +109,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
117
109
  }
118
110
 
119
111
  override fun addView(child: View?, index: Int) {
112
+ if (child is TrueSheetContainerView) {
113
+ viewController.removeSheetSnapshot()
114
+ }
115
+
120
116
  viewController.addView(child, index)
121
117
 
122
118
  if (child is TrueSheetContainerView) {
@@ -136,10 +132,11 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
136
132
  val child = getChildAt(index)
137
133
  if (child is TrueSheetContainerView) {
138
134
  child.delegate = null
135
+ viewController.createSheetSnapshot()
139
136
 
140
- // Dismiss the sheet when container is removed
137
+ // Dismiss when container is removed
141
138
  if (viewController.isPresented) {
142
- viewController.dismiss(animated = false)
139
+ dismissAll(true) {}
143
140
  }
144
141
  }
145
142
  viewController.removeView(child)
@@ -165,13 +162,19 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
165
162
  fun onDropInstance() {
166
163
  reactContext.removeLifecycleEventListener(this)
167
164
 
168
- viewController.dismiss()
169
- viewController.delegate = null
170
-
171
165
  TrueSheetModule.unregisterView(id)
172
166
  TrueSheetStackManager.removeSheet(this)
173
167
 
174
168
  didInitiallyPresent = false
169
+
170
+ if (viewController.isPresented) {
171
+ viewController.dismissPromise = {
172
+ viewController.delegate = null
173
+ }
174
+ viewController.dismiss()
175
+ } else {
176
+ viewController.delegate = null
177
+ }
175
178
  }
176
179
 
177
180
  /**
@@ -318,6 +321,16 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
318
321
  viewController.dismiss(animated)
319
322
  }
320
323
 
324
+ @UiThread
325
+ fun dismissAll(animated: Boolean = true, promiseCallback: () -> Unit) {
326
+ // Dismiss all sheets above first
327
+ dismiss(animated) {}
328
+
329
+ // Then dismiss itself
330
+ viewController.dismissPromise = promiseCallback
331
+ viewController.dismiss(animated)
332
+ }
333
+
321
334
  @UiThread
322
335
  fun resize(detentIndex: Int, promiseCallback: () -> Unit) {
323
336
  if (!viewController.isPresented) {
@@ -334,12 +347,13 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
334
347
  * Uses post() to ensure all layout passes complete before reconfiguring.
335
348
  */
336
349
  fun updateSheetIfNeeded() {
337
- if (!viewController.isPresented) return
338
- if (isSheetUpdatePending) return
350
+ if (!viewController.isPresented || isSheetUpdatePending) return
339
351
 
340
352
  isSheetUpdatePending = true
341
353
  viewController.post {
342
354
  isSheetUpdatePending = false
355
+ if (viewController.containerView == null) return@post
356
+
343
357
  viewController.setupSheetDetentsForSizeChange()
344
358
  TrueSheetStackManager.onSheetSizeChanged(this)
345
359
  }
@@ -474,6 +488,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
474
488
  eventDispatcher?.dispatchEvent(BackPressEvent(surfaceId, id))
475
489
  }
476
490
 
491
+ override fun viewControllerDidDetectScreenDisappear() {
492
+ dismissAll(animated = true) {}
493
+ }
494
+
477
495
  // ==================== TrueSheetContainerViewDelegate ====================
478
496
 
479
497
  override fun containerViewContentDidChangeSize(width: Int, height: Int) {
@@ -497,7 +515,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
497
515
  * of whichever window this view is in - whether that's the activity's window or a
498
516
  * Modal's dialog window.
499
517
  */
500
- private fun findRootContainerView(): ViewGroup? {
518
+ override fun findRootContainerView(): ViewGroup? {
501
519
  var current: android.view.ViewParent? = parent
502
520
 
503
521
  while (current != null) {
@@ -1,14 +1,18 @@
1
1
  package com.lodev09.truesheet
2
2
 
3
3
  import android.annotation.SuppressLint
4
+ import android.graphics.Bitmap
5
+ import android.graphics.Canvas
4
6
  import android.os.Build
5
7
  import android.view.MotionEvent
6
8
  import android.view.View
7
9
  import android.view.ViewGroup
8
10
  import android.view.accessibility.AccessibilityNodeInfo
11
+ import android.widget.ImageView
9
12
  import androidx.activity.OnBackPressedCallback
10
13
  import androidx.appcompat.app.AppCompatActivity
11
14
  import androidx.coordinatorlayout.widget.CoordinatorLayout
15
+ import androidx.core.graphics.createBitmap
12
16
  import androidx.core.view.isNotEmpty
13
17
  import androidx.core.view.isVisible
14
18
  import com.facebook.react.R
@@ -46,6 +50,7 @@ data class DetentInfo(val index: Int, val position: Float)
46
50
 
47
51
  interface TrueSheetViewControllerDelegate {
48
52
  val eventDispatcher: EventDispatcher?
53
+ fun findRootContainerView(): ViewGroup?
49
54
  fun viewControllerWillPresent(index: Int, position: Float, detent: Float)
50
55
  fun viewControllerDidPresent(index: Int, position: Float, detent: Float)
51
56
  fun viewControllerWillDismiss()
@@ -61,6 +66,7 @@ interface TrueSheetViewControllerDelegate {
61
66
  fun viewControllerWillBlur()
62
67
  fun viewControllerDidBlur()
63
68
  fun viewControllerDidBackPress()
69
+ fun viewControllerDidDetectScreenDisappear()
64
70
  }
65
71
 
66
72
  // =============================================================================
@@ -130,7 +136,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
130
136
 
131
137
  private var interactionState: InteractionState = InteractionState.Idle
132
138
  private var isDismissing = false
133
- private var wasHiddenByModal = false
139
+ internal var wasHiddenByModal = false
134
140
  private var shouldAnimatePresent = false
135
141
  private var isPresentAnimating = false
136
142
 
@@ -213,7 +219,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
213
219
  private val behavior: BottomSheetBehavior<TrueSheetBottomSheetView>?
214
220
  get() = sheetView?.behavior
215
221
 
216
- private val containerView: TrueSheetContainerView?
222
+ internal val containerView: TrueSheetContainerView?
217
223
  get() = if (this.isNotEmpty()) getChildAt(0) as? TrueSheetContainerView else null
218
224
 
219
225
  // Screen Measurements
@@ -320,6 +326,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
320
326
  parentDimView?.detach()
321
327
  parentDimView = null
322
328
 
329
+ // Cleanup snapshot
330
+ removeSheetSnapshot()
331
+
323
332
  // Detach content from sheet
324
333
  sheetView?.removeView(this)
325
334
 
@@ -338,6 +347,34 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
338
347
  shouldAnimatePresent = true
339
348
  }
340
349
 
350
+ private var snapshotView: View? = null
351
+
352
+ fun createSheetSnapshot() {
353
+ if (!isPresented) return
354
+ val sheet = sheetView ?: return
355
+ if (sheet.width <= 0 || sheet.height <= 0) return
356
+
357
+ val bitmap = createBitmap(sheet.width, sheet.height)
358
+ val canvas = Canvas(bitmap)
359
+ sheet.draw(canvas)
360
+
361
+ val snapshot = ImageView(reactContext).apply {
362
+ setImageBitmap(bitmap)
363
+ layoutParams = LayoutParams(sheet.width, sheet.height)
364
+ }
365
+
366
+ sheet.addView(snapshot, 0)
367
+ snapshotView = snapshot
368
+ }
369
+
370
+ fun removeSheetSnapshot() {
371
+ snapshotView?.let {
372
+ (it as? ImageView)?.setImageDrawable(null)
373
+ sheetView?.removeView(it)
374
+ }
375
+ snapshotView = null
376
+ }
377
+
341
378
  // =============================================================================
342
379
  // MARK: - Back Button Handling
343
380
  // =============================================================================
@@ -405,6 +442,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
405
442
  dismissOrCollapseToLowest()
406
443
  }
407
444
 
445
+ // =============================================================================
446
+ // MARK: - TrueSheetBottomSheetViewDelegate - Grabber Tap
447
+ // =============================================================================
448
+
449
+ override fun bottomSheetViewDidTapGrabber() {
450
+ val nextIndex = (currentDetentIndex + 1) % detents.size
451
+ if (nextIndex == 0 && detents.size == 1 && dismissible) {
452
+ dismiss(animated = true)
453
+ } else {
454
+ setStateForDetentIndex(nextIndex)
455
+ }
456
+ }
457
+
408
458
  // =============================================================================
409
459
  // MARK: - BottomSheetCallback
410
460
  // =============================================================================
@@ -534,6 +584,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
534
584
  post { parent.showAfterModal() }
535
585
  }
536
586
  }
587
+ },
588
+ onNonModalScreenPushed = {
589
+ // Only handle on root sheet (no parent) to trigger dismissAll
590
+ if (isPresented && isSheetVisible && parentSheetView == null) {
591
+ delegate?.viewControllerDidDetectScreenDisappear()
592
+ }
537
593
  }
538
594
  )
539
595
  rnScreensObserver?.start()
@@ -892,7 +948,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
892
948
  }
893
949
 
894
950
  fun saveFocusedView() {
895
- focusedViewBeforeBlur = reactContext.currentActivity?.currentFocus
951
+ focusedViewBeforeBlur = delegate?.findRootContainerView()?.findFocus()
952
+ ?: reactContext.currentActivity?.currentFocus
896
953
  }
897
954
 
898
955
  fun restoreFocusedView() {
@@ -18,7 +18,8 @@ class RNScreensFragmentObserver(
18
18
  private val reactContext: ReactContext,
19
19
  private val onModalPresented: () -> Unit,
20
20
  private val onModalWillDismiss: () -> Unit,
21
- private val onModalDidDismiss: () -> Unit
21
+ private val onModalDidDismiss: () -> Unit,
22
+ private val onNonModalScreenPushed: () -> Unit
22
23
  ) {
23
24
  private var fragmentLifecycleCallback: FragmentManager.FragmentLifecycleCallbacks? = null
24
25
  private var activityLifecycleObserver: DefaultLifecycleObserver? = null
@@ -61,6 +62,9 @@ class RNScreensFragmentObserver(
61
62
  if (activeModalFragments.size == 1) {
62
63
  onModalPresented()
63
64
  }
65
+ } else if (activeModalFragments.isEmpty() && isNonModalScreenFragment(f)) {
66
+ // Only trigger non-modal push when no modals are active
67
+ onNonModalScreenPushed()
64
68
  }
65
69
  }
66
70
 
@@ -145,6 +149,27 @@ class RNScreensFragmentObserver(
145
149
  */
146
150
  private fun isScreensFragment(fragment: Fragment): Boolean = fragment.javaClass.name.startsWith(RN_SCREENS_PACKAGE)
147
151
 
152
+ /**
153
+ * Check if fragment is a non-modal screen (regular push presentation).
154
+ */
155
+ private fun isNonModalScreenFragment(fragment: Fragment): Boolean {
156
+ if (!isScreensFragment(fragment)) return false
157
+ // ScreenModalFragment is always a modal
158
+ if (fragment.javaClass.name.contains("ScreenModalFragment")) return false
159
+
160
+ try {
161
+ val getScreenMethod = fragment.javaClass.getMethod("getScreen")
162
+ val screen = getScreenMethod.invoke(fragment) ?: return false
163
+
164
+ val getStackPresentationMethod = screen.javaClass.getMethod("getStackPresentation")
165
+ val stackPresentation = getStackPresentationMethod.invoke(screen) ?: return false
166
+
167
+ return stackPresentation.toString() == "PUSH"
168
+ } catch (e: Exception) {
169
+ return false
170
+ }
171
+ }
172
+
148
173
  /**
149
174
  * Check if fragment is a react-native-screens modal (fullScreenModal, transparentModal, or formSheet).
150
175
  * Uses reflection to check the fragment's screen.stackPresentation property.
@@ -3,10 +3,13 @@ package com.lodev09.truesheet.core
3
3
  import android.annotation.SuppressLint
4
4
  import android.graphics.Color
5
5
  import android.graphics.Outline
6
+ import android.graphics.Rect
6
7
  import android.graphics.drawable.ShapeDrawable
7
8
  import android.graphics.drawable.shapes.RoundRectShape
8
9
  import android.util.TypedValue
10
+ import android.view.GestureDetector
9
11
  import android.view.Gravity
12
+ import android.view.MotionEvent
10
13
  import android.view.View
11
14
  import android.view.ViewOutlineProvider
12
15
  import android.widget.FrameLayout
@@ -22,6 +25,7 @@ interface TrueSheetBottomSheetViewDelegate {
22
25
  val sheetBackgroundColor: Int?
23
26
  val grabber: Boolean
24
27
  val grabberOptions: GrabberOptions?
28
+ fun bottomSheetViewDidTapGrabber()
25
29
  }
26
30
 
27
31
  /**
@@ -162,4 +166,32 @@ class TrueSheetBottomSheetView(private val reactContext: ThemedReactContext) : F
162
166
 
163
167
  addView(grabberView, 0)
164
168
  }
169
+
170
+ // =============================================================================
171
+ // MARK: - Grabber Tap Detection
172
+ // =============================================================================
173
+
174
+ private val grabberTapDetector = GestureDetector(
175
+ context,
176
+ object : GestureDetector.SimpleOnGestureListener() {
177
+ override fun onSingleTapUp(e: MotionEvent): Boolean {
178
+ delegate?.bottomSheetViewDidTapGrabber()
179
+ return true
180
+ }
181
+ }
182
+ )
183
+
184
+ override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
185
+ val grabber = findViewWithTag<View>(GRABBER_TAG)
186
+ if (grabber != null) {
187
+ val rect = Rect()
188
+ grabber.getHitRect(rect)
189
+ if (rect.contains(ev.x.toInt(), ev.y.toInt())) {
190
+ if (grabberTapDetector.onTouchEvent(ev)) {
191
+ return true
192
+ }
193
+ }
194
+ }
195
+ return super.dispatchTouchEvent(ev)
196
+ }
165
197
  }
@@ -25,16 +25,18 @@ data class GrabberOptions(
25
25
 
26
26
  /**
27
27
  * Native grabber (drag handle) view for the bottom sheet.
28
- * Displays a small pill-shaped indicator at the top of the sheet.
28
+ * Displays a small pill-shaped indicator at the top of the sheet with a tappable hitbox.
29
29
  */
30
30
  @SuppressLint("ViewConstructor")
31
- class TrueSheetGrabberView(context: Context, private val options: GrabberOptions? = null) : View(context) {
31
+ class TrueSheetGrabberView(context: Context, private val options: GrabberOptions? = null) : FrameLayout(context) {
32
32
 
33
33
  companion object {
34
34
  private const val DEFAULT_WIDTH = 32f // dp
35
35
  private const val DEFAULT_HEIGHT = 4f // dp
36
36
  private const val DEFAULT_TOP_MARGIN = 16f // dp
37
37
  private const val DEFAULT_ALPHA = 0.4f
38
+ private const val HITBOX_PADDING_HORIZONTAL = 16f // dp
39
+ private const val HITBOX_PADDING_VERTICAL = 16f // dp
38
40
  private val DEFAULT_COLOR = Color.argb((DEFAULT_ALPHA * 255).toInt(), 73, 69, 79) // #49454F @ 40%
39
41
  }
40
42
 
@@ -57,19 +59,33 @@ class TrueSheetGrabberView(context: Context, private val options: GrabberOptions
57
59
  get() = if (isAdaptive) getAdaptiveColor(options?.color) else options?.color ?: DEFAULT_COLOR
58
60
 
59
61
  init {
60
- layoutParams = FrameLayout.LayoutParams(
61
- grabberWidth.dpToPx().toInt(),
62
- grabberHeight.dpToPx().toInt()
62
+ val hitboxWidth = grabberWidth + (HITBOX_PADDING_HORIZONTAL * 2)
63
+ val hitboxHeight = grabberHeight + (HITBOX_PADDING_VERTICAL * 2)
64
+
65
+ layoutParams = LayoutParams(
66
+ hitboxWidth.dpToPx().toInt(),
67
+ hitboxHeight.dpToPx().toInt()
63
68
  ).apply {
64
69
  gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
65
- topMargin = grabberTopMargin.dpToPx().toInt()
70
+ topMargin = (grabberTopMargin - HITBOX_PADDING_VERTICAL).dpToPx().toInt()
66
71
  }
67
72
 
68
- background = GradientDrawable().apply {
69
- shape = GradientDrawable.RECTANGLE
70
- cornerRadius = grabberCornerRadius.dpToPx()
71
- setColor(grabberColor)
73
+ // The visible pill centered inside the hitbox
74
+ val pillView = View(context).apply {
75
+ layoutParams = LayoutParams(
76
+ grabberWidth.dpToPx().toInt(),
77
+ grabberHeight.dpToPx().toInt()
78
+ ).apply {
79
+ gravity = Gravity.CENTER
80
+ }
81
+ background = GradientDrawable().apply {
82
+ shape = GradientDrawable.RECTANGLE
83
+ cornerRadius = grabberCornerRadius.dpToPx()
84
+ setColor(grabberColor)
85
+ }
72
86
  }
87
+
88
+ addView(pillView)
73
89
  }
74
90
 
75
91
  private fun getAdaptiveColor(baseColor: Int? = null): Int {
@@ -35,7 +35,7 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
35
35
  private set
36
36
 
37
37
  fun isFocusedViewWithinSheet(sheetView: View): Boolean {
38
- val focusedView = reactContext.currentActivity?.currentFocus ?: return false
38
+ val focusedView = targetView.rootView?.findFocus() ?: return false
39
39
  var current: View? = focusedView
40
40
  while (current != null && current !== targetView) {
41
41
  if (current === sheetView) return true
@@ -131,7 +131,7 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
131
131
  // Ensure we don't add duplicate listeners
132
132
  if (globalLayoutListener != null) return
133
133
 
134
- val rootView = reactContext.currentActivity?.window?.decorView?.rootView ?: return
134
+ val rootView = targetView.rootView ?: return
135
135
  activityRootView = rootView
136
136
 
137
137
  globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
@@ -18,16 +18,16 @@ object TrueSheetStackManager {
18
18
  */
19
19
  private fun getParentSheetAt(index: Int, rootContainer: ViewGroup?): TrueSheetView? {
20
20
  if (index <= 0) return null
21
- return presentedSheetStack[index - 1].takeIf { it.rootContainerView == rootContainer }
21
+ return presentedSheetStack[index - 1].takeIf { !it.viewController.wasHiddenByModal && it.rootContainerView == rootContainer }
22
22
  }
23
23
 
24
24
  /**
25
- * Returns the topmost presented and visible sheet.
25
+ * Returns the topmost presented and visible sheet that is not hidden by modal.
26
26
  * Must be called within synchronized block.
27
27
  */
28
28
  private fun findTopmostSheet(): TrueSheetView? =
29
29
  presentedSheetStack.lastOrNull {
30
- it.viewController.isPresented && it.viewController.isSheetVisible
30
+ it.viewController.isPresented && it.viewController.isSheetVisible && !it.viewController.wasHiddenByModal
31
31
  }
32
32
 
33
33
  /**
@@ -151,4 +151,27 @@ object TrueSheetStackManager {
151
151
  return findTopmostSheet()
152
152
  }
153
153
  }
154
+
155
+ /**
156
+ * Returns the root presented sheet for dismissAll.
157
+ * Starts from the topmost sheet and walks up to find the root of its stack.
158
+ * Stops at modal boundary (parent hidden by modal) or when there's no parent.
159
+ */
160
+ @JvmStatic
161
+ fun getRootSheet(): TrueSheetView? {
162
+ synchronized(presentedSheetStack) {
163
+ val topmost = presentedSheetStack.lastOrNull { it.viewController.isPresented } ?: return null
164
+
165
+ var current: TrueSheetView = topmost
166
+ while (true) {
167
+ val parent = current.viewController.parentSheetView ?: return current
168
+
169
+ if (parent.viewController.wasHiddenByModal) {
170
+ return current
171
+ }
172
+
173
+ current = parent
174
+ }
175
+ }
176
+ }
154
177
  }
@@ -14,6 +14,7 @@
14
14
  #import "TrueSheetModule.h"
15
15
  #import <React/RCTUtils.h>
16
16
  #import "TrueSheetView.h"
17
+ #import "TrueSheetViewController.h"
17
18
 
18
19
  #import <TrueSheetSpec/TrueSheetSpec.h>
19
20
 
@@ -115,6 +116,44 @@ RCT_EXPORT_MODULE(TrueSheetModule)
115
116
  });
116
117
  }
117
118
 
119
+ - (void)dismissAll:(BOOL)animated resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
120
+ RCTExecuteOnMainQueue(^{
121
+ @synchronized(viewRegistry) {
122
+ // Find the root presented sheet (one without a parent TrueSheet)
123
+ TrueSheetView *rootSheet = nil;
124
+
125
+ for (TrueSheetView *view in viewRegistry.allValues) {
126
+ if (!view.viewController.isPresented) {
127
+ continue;
128
+ }
129
+
130
+ UIViewController *presenter = view.viewController.presentingViewController;
131
+ BOOL hasParentSheet = [presenter isKindOfClass:[TrueSheetViewController class]];
132
+
133
+ if (!hasParentSheet) {
134
+ rootSheet = view;
135
+ break;
136
+ }
137
+ }
138
+
139
+ if (!rootSheet) {
140
+ resolve(nil);
141
+ return;
142
+ }
143
+
144
+ [rootSheet
145
+ dismissAllAnimated:animated
146
+ completion:^(BOOL success, NSError *_Nullable error) {
147
+ if (success) {
148
+ resolve(nil);
149
+ } else {
150
+ reject(@"DISMISS_FAILED", error.localizedDescription ?: @"Failed to dismiss sheets", error);
151
+ }
152
+ }];
153
+ }
154
+ });
155
+ }
156
+
118
157
  #pragma mark - Helper Methods
119
158
 
120
159
  + (nullable TrueSheetView *)getTrueSheetViewByTag:(NSNumber *)reactTag {
@@ -23,6 +23,8 @@ typedef void (^TrueSheetCompletionBlock)(BOOL success, NSError *_Nullable error)
23
23
 
24
24
  @interface TrueSheetView : RCTViewComponentView
25
25
 
26
+ @property (nonatomic, readonly) TrueSheetViewController *viewController;
27
+
26
28
  // TurboModule methods
27
29
  - (void)presentAtIndex:(NSInteger)index
28
30
  animated:(BOOL)animated
@@ -32,6 +34,8 @@ typedef void (^TrueSheetCompletionBlock)(BOOL success, NSError *_Nullable error)
32
34
 
33
35
  - (void)resizeToIndex:(NSInteger)index completion:(nullable TrueSheetCompletionBlock)completion;
34
36
 
37
+ - (void)dismissAllAnimated:(BOOL)animated completion:(nullable TrueSheetCompletionBlock)completion;
38
+
35
39
  @end
36
40
 
37
41
  NS_ASSUME_NONNULL_END