@lodev09/react-native-true-sheet 3.7.0 → 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 -8
  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
  */
@@ -109,6 +109,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
109
109
  }
110
110
 
111
111
  override fun addView(child: View?, index: Int) {
112
+ if (child is TrueSheetContainerView) {
113
+ viewController.removeSheetSnapshot()
114
+ }
115
+
112
116
  viewController.addView(child, index)
113
117
 
114
118
  if (child is TrueSheetContainerView) {
@@ -128,10 +132,11 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
128
132
  val child = getChildAt(index)
129
133
  if (child is TrueSheetContainerView) {
130
134
  child.delegate = null
135
+ viewController.createSheetSnapshot()
131
136
 
132
- // Dismiss the sheet when container is removed
137
+ // Dismiss when container is removed
133
138
  if (viewController.isPresented) {
134
- viewController.dismiss(animated = false)
139
+ dismissAll(true) {}
135
140
  }
136
141
  }
137
142
  viewController.removeView(child)
@@ -157,13 +162,19 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
157
162
  fun onDropInstance() {
158
163
  reactContext.removeLifecycleEventListener(this)
159
164
 
160
- viewController.dismiss()
161
- viewController.delegate = null
162
-
163
165
  TrueSheetModule.unregisterView(id)
164
166
  TrueSheetStackManager.removeSheet(this)
165
167
 
166
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
+ }
167
178
  }
168
179
 
169
180
  /**
@@ -310,6 +321,16 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
310
321
  viewController.dismiss(animated)
311
322
  }
312
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
+
313
334
  @UiThread
314
335
  fun resize(detentIndex: Int, promiseCallback: () -> Unit) {
315
336
  if (!viewController.isPresented) {
@@ -326,12 +347,13 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
326
347
  * Uses post() to ensure all layout passes complete before reconfiguring.
327
348
  */
328
349
  fun updateSheetIfNeeded() {
329
- if (!viewController.isPresented) return
330
- if (isSheetUpdatePending) return
350
+ if (!viewController.isPresented || isSheetUpdatePending) return
331
351
 
332
352
  isSheetUpdatePending = true
333
353
  viewController.post {
334
354
  isSheetUpdatePending = false
355
+ if (viewController.containerView == null) return@post
356
+
335
357
  viewController.setupSheetDetentsForSizeChange()
336
358
  TrueSheetStackManager.onSheetSizeChanged(this)
337
359
  }
@@ -466,6 +488,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
466
488
  eventDispatcher?.dispatchEvent(BackPressEvent(surfaceId, id))
467
489
  }
468
490
 
491
+ override fun viewControllerDidDetectScreenDisappear() {
492
+ dismissAll(animated = true) {}
493
+ }
494
+
469
495
  // ==================== TrueSheetContainerViewDelegate ====================
470
496
 
471
497
  override fun containerViewContentDidChangeSize(width: Int, height: Int) {
@@ -489,7 +515,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
489
515
  * of whichever window this view is in - whether that's the activity's window or a
490
516
  * Modal's dialog window.
491
517
  */
492
- private fun findRootContainerView(): ViewGroup? {
518
+ override fun findRootContainerView(): ViewGroup? {
493
519
  var current: android.view.ViewParent? = parent
494
520
 
495
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
@@ -45,6 +45,7 @@ using namespace facebook::react;
45
45
  TrueSheetViewController *_controller;
46
46
  RCTSurfaceTouchHandler *_touchHandler;
47
47
  TrueSheetViewShadowNode::ConcreteState::Shared _state;
48
+ UIView *_snapshotView;
48
49
  CGSize _lastStateSize;
49
50
  NSInteger _initialDetentIndex;
50
51
  BOOL _scrollable;
@@ -69,6 +70,7 @@ using namespace facebook::react;
69
70
 
70
71
  _touchHandler = [[RCTSurfaceTouchHandler alloc] init];
71
72
  _containerView = nil;
73
+ _snapshotView = nil;
72
74
  _lastStateSize = CGSizeZero;
73
75
  _initialDetentIndex = -1;
74
76
  _initialDetentAnimated = YES;
@@ -107,6 +109,9 @@ using namespace facebook::react;
107
109
  _controller.delegate = nil;
108
110
  _controller = nil;
109
111
 
112
+ [_snapshotView removeFromSuperview];
113
+ _snapshotView = nil;
114
+
110
115
  [TrueSheetModule unregisterViewWithTag:@(self.tag)];
111
116
  }
112
117
 
@@ -280,6 +285,11 @@ using namespace facebook::react;
280
285
  return;
281
286
  }
282
287
 
288
+ if (_snapshotView) {
289
+ [_snapshotView removeFromSuperview];
290
+ _snapshotView = nil;
291
+ }
292
+
283
293
  _containerView = (TrueSheetContainerView *)childComponentView;
284
294
  _containerView.delegate = self;
285
295
 
@@ -308,6 +318,14 @@ using namespace facebook::react;
308
318
  if (![childComponentView isKindOfClass:[TrueSheetContainerView class]])
309
319
  return;
310
320
 
321
+ UIView *superView = _containerView.superview;
322
+ UIView *snapshot = [_containerView snapshotViewAfterScreenUpdates:NO];
323
+ if (snapshot) {
324
+ snapshot.frame = _containerView.frame;
325
+ [superView insertSubview:snapshot belowSubview:_containerView];
326
+ _snapshotView = snapshot;
327
+ }
328
+
311
329
  _containerView.delegate = nil;
312
330
 
313
331
  if (_touchHandler) {
@@ -396,6 +414,30 @@ using namespace facebook::react;
396
414
  [self presentAtIndex:index animated:YES completion:completion];
397
415
  }
398
416
 
417
+ - (TrueSheetViewController *)viewController {
418
+ return _controller;
419
+ }
420
+
421
+ - (void)dismissAllAnimated:(BOOL)animated completion:(nullable TrueSheetCompletionBlock)completion {
422
+ if (!_controller.isPresented) {
423
+ if (completion) {
424
+ completion(YES, nil);
425
+ }
426
+ return;
427
+ }
428
+
429
+ [self viewControllerDidChangePosition:-1 position:_controller.screenHeight detent:0 realtime:NO];
430
+
431
+ // Dismiss from the presenting view controller to dismiss this sheet and all its children
432
+ UIViewController *presenter = _controller.presentingViewController;
433
+ [presenter dismissViewControllerAnimated:animated
434
+ completion:^{
435
+ if (completion) {
436
+ completion(YES, nil);
437
+ }
438
+ }];
439
+ }
440
+
399
441
  #pragma mark - TrueSheetContainerViewDelegate
400
442
 
401
443
  /**
@@ -409,6 +451,9 @@ using namespace facebook::react;
409
451
 
410
452
  dispatch_async(dispatch_get_main_queue(), ^{
411
453
  self->_isSheetUpdatePending = NO;
454
+ if (!self->_containerView)
455
+ return;
456
+
412
457
  [self->_controller setupSheetDetentsForSizeChange];
413
458
  });
414
459
  }
@@ -499,6 +544,10 @@ using namespace facebook::react;
499
544
  [TrueSheetFocusEvents emitDidBlur:_eventEmitter];
500
545
  }
501
546
 
547
+ - (void)viewControllerDidDetectScreenDisappear {
548
+ [self dismissAllAnimated:YES completion:nil];
549
+ }
550
+
502
551
  #pragma mark - Private Helpers
503
552
 
504
553
  - (UIViewController *)findPresentingViewController {