@lodev09/react-native-true-sheet 4.0.0-beta.24 → 4.0.0-beta.26

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 (88) hide show
  1. package/README.md +2 -0
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +13 -0
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +12 -1
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +120 -19
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +103 -27
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +18 -10
  7. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +31 -14
  8. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +9 -0
  9. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +3 -16
  10. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +14 -1
  11. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +11 -17
  12. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +2 -4
  13. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +29 -38
  14. package/android/src/main/java/com/lodev09/truesheet/utils/ColorUtils.kt +19 -0
  15. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +6 -2
  16. package/ios/TrueSheetContainerView.h +1 -0
  17. package/ios/TrueSheetContainerView.mm +6 -0
  18. package/ios/TrueSheetContentView.h +1 -0
  19. package/ios/TrueSheetContentView.mm +2 -0
  20. package/ios/TrueSheetFooterView.mm +5 -0
  21. package/ios/TrueSheetModule.mm +11 -2
  22. package/ios/TrueSheetView.h +2 -0
  23. package/ios/TrueSheetView.mm +62 -9
  24. package/ios/TrueSheetViewController.h +14 -0
  25. package/ios/TrueSheetViewController.mm +490 -33
  26. package/ios/core/RNScreensEventObserver.mm +18 -1
  27. package/ios/core/TrueSheetDetentCalculator.h +11 -0
  28. package/ios/core/TrueSheetDetentCalculator.mm +17 -5
  29. package/ios/core/TrueSheetDimView.h +27 -0
  30. package/ios/core/TrueSheetDimView.mm +41 -0
  31. package/ios/core/TrueSheetKeyboardObserver.mm +98 -15
  32. package/lib/module/TrueSheet.js +88 -17
  33. package/lib/module/TrueSheet.js.map +1 -1
  34. package/lib/module/TrueSheet.web.js +150 -37
  35. package/lib/module/TrueSheet.web.js.map +1 -1
  36. package/lib/module/TrueSheetProvider.web.js +13 -9
  37. package/lib/module/TrueSheetProvider.web.js.map +1 -1
  38. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +8 -0
  39. package/lib/module/scrollable.js +28 -0
  40. package/lib/module/scrollable.js.map +1 -0
  41. package/lib/module/web/keyboard.js +69 -0
  42. package/lib/module/web/keyboard.js.map +1 -0
  43. package/lib/module/web/layout.js +1 -1
  44. package/lib/module/web/layout.js.map +1 -1
  45. package/lib/module/web/vaul/context.js.map +1 -1
  46. package/lib/module/web/vaul/index.js +18 -8
  47. package/lib/module/web/vaul/index.js.map +1 -1
  48. package/lib/module/web/vaul/use-snap-points.js +15 -10
  49. package/lib/module/web/vaul/use-snap-points.js.map +1 -1
  50. package/lib/typescript/src/TrueSheet.d.ts +14 -1
  51. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  52. package/lib/typescript/src/TrueSheet.types.d.ts +53 -2
  53. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  54. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  55. package/lib/typescript/src/TrueSheetProvider.web.d.ts +5 -1
  56. package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
  57. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +7 -0
  58. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  59. package/lib/typescript/src/navigation/types.d.ts +1 -1
  60. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  61. package/lib/typescript/src/scrollable.d.ts +2 -0
  62. package/lib/typescript/src/scrollable.d.ts.map +1 -0
  63. package/lib/typescript/src/web/keyboard.d.ts +15 -0
  64. package/lib/typescript/src/web/keyboard.d.ts.map +1 -0
  65. package/lib/typescript/src/web/layout.d.ts +1 -0
  66. package/lib/typescript/src/web/layout.d.ts.map +1 -1
  67. package/lib/typescript/src/web/vaul/context.d.ts +2 -1
  68. package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
  69. package/lib/typescript/src/web/vaul/index.d.ts +8 -1
  70. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  71. package/lib/typescript/src/web/vaul/types.d.ts +9 -0
  72. package/lib/typescript/src/web/vaul/types.d.ts.map +1 -1
  73. package/lib/typescript/src/web/vaul/use-snap-points.d.ts +3 -2
  74. package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
  75. package/package.json +1 -1
  76. package/src/TrueSheet.tsx +91 -14
  77. package/src/TrueSheet.types.ts +60 -2
  78. package/src/TrueSheet.web.tsx +170 -41
  79. package/src/TrueSheetProvider.web.tsx +22 -13
  80. package/src/fabric/TrueSheetViewNativeComponent.ts +8 -0
  81. package/src/navigation/types.ts +3 -0
  82. package/src/scrollable.ts +25 -0
  83. package/src/web/keyboard.ts +79 -0
  84. package/src/web/layout.ts +1 -1
  85. package/src/web/vaul/context.ts +2 -1
  86. package/src/web/vaul/index.tsx +20 -5
  87. package/src/web/vaul/types.ts +10 -0
  88. package/src/web/vaul/use-snap-points.ts +19 -9
@@ -1,10 +1,8 @@
1
1
  package com.lodev09.truesheet
2
2
 
3
3
  import android.view.WindowManager
4
- import com.facebook.react.bridge.ColorPropConverter
5
4
  import com.facebook.react.bridge.ReadableArray
6
5
  import com.facebook.react.bridge.ReadableMap
7
- import com.facebook.react.bridge.ReadableType
8
6
  import com.facebook.react.module.annotations.ReactModule
9
7
  import com.facebook.react.uimanager.PixelUtil.dpToPx
10
8
  import com.facebook.react.uimanager.PointerEvents
@@ -20,6 +18,7 @@ import com.facebook.react.viewmanagers.TrueSheetViewManagerInterface
20
18
  import com.lodev09.truesheet.core.AccessibilityOptions
21
19
  import com.lodev09.truesheet.core.GrabberOptions
22
20
  import com.lodev09.truesheet.events.*
21
+ import com.lodev09.truesheet.utils.getColorOrNull
23
22
 
24
23
  /**
25
24
  * ViewManager for TrueSheetView - Fabric architecture
@@ -120,13 +119,7 @@ class TrueSheetViewManager :
120
119
  override fun setDetentBackgrounds(view: TrueSheetView, value: ReadableArray?) {
121
120
  view.setDetentBackgrounds(
122
121
  (0 until (value?.size() ?: 0)).map { index ->
123
- val entry = value?.getMap(index)
124
- if (entry != null && entry.hasKey("color") && !entry.isNull("color")) {
125
- val color = if (entry.getType("color") == ReadableType.Map) entry.getMap("color") else entry.getDouble("color")
126
- ColorPropConverter.getColor(color, view.context)
127
- } else {
128
- null
129
- }
122
+ value?.getMap(index)?.getColorOrNull("color", view.context)
130
123
  }
131
124
  )
132
125
  }
@@ -164,7 +157,7 @@ class TrueSheetViewManager :
164
157
  } else {
165
158
  null
166
159
  },
167
- color = if (options.hasKey("color") && !options.isNull("color")) options.getInt("color") else null,
160
+ color = options.getColorOrNull("color", view.context),
168
161
  adaptive = if (options.hasKey("adaptive")) options.getBoolean("adaptive") else true
169
162
  )
170
163
  view.setGrabberOptions(grabberOptions)
@@ -209,6 +202,16 @@ class TrueSheetViewManager :
209
202
  view.setDimmed(dimmed)
210
203
  }
211
204
 
205
+ @ReactProp(name = "dimColor", customType = "Color")
206
+ override fun setDimColor(view: TrueSheetView, color: Int?) {
207
+ view.setDimColor(color)
208
+ }
209
+
210
+ @ReactProp(name = "dimOpacity", defaultDouble = -1.0)
211
+ override fun setDimOpacity(view: TrueSheetView, opacity: Double) {
212
+ view.setDimOpacity(opacity.toFloat())
213
+ }
214
+
212
215
  @ReactProp(name = "dimmedDetentIndex", defaultInt = 0)
213
216
  override fun setDimmedDetentIndex(view: TrueSheetView, index: Int) {
214
217
  view.setDimmedDetentIndex(index)
@@ -250,6 +253,11 @@ class TrueSheetViewManager :
250
253
  // iOS-specific prop - no-op on Android
251
254
  }
252
255
 
256
+ @ReactProp(name = "foldOptions")
257
+ override fun setFoldOptions(view: TrueSheetView, options: ReadableMap?) {
258
+ // iOS-specific prop - no-op on Android
259
+ }
260
+
253
261
  @ReactProp(name = "insetAdjustment")
254
262
  override fun setInsetAdjustment(view: TrueSheetView, insetAdjustment: String?) {
255
263
  view.setInsetAdjustment(insetAdjustment ?: "automatic")
@@ -22,6 +22,15 @@ class RNScreensEventObserver : EventDispatcherListener {
22
22
  private var eventDispatcher: EventDispatcher? = null
23
23
  var presenterScreenTag: Int = 0
24
24
 
25
+ // Screens enclosing the presenter. react-native-screens only dispatches lifecycle
26
+ // events for stack screens, so a presenter inside a plain screen container (e.g. a
27
+ // bottom tab) never receives them and an enclosing stack screen has to stand in.
28
+ var ancestorScreenTags: Set<Int> = emptySet()
29
+
30
+ // Only the screen that hid the sheet brings it back. An enclosing screen reappearing
31
+ // must not reveal a sheet whose own screen is still covered inside it.
32
+ private var disappearedScreenTag: Int = 0
33
+
25
34
  fun startObserving(dispatcher: EventDispatcher?) {
26
35
  if (eventDispatcher != null || dispatcher == null) return
27
36
 
@@ -35,29 +44,37 @@ class RNScreensEventObserver : EventDispatcherListener {
35
44
  }
36
45
 
37
46
  fun capturePresenterScreenFromView(view: View?) {
38
- presenterScreenTag = 0
39
-
40
- var current: View? = view
41
- while (current != null) {
42
- if (isScreenView(current)) {
43
- presenterScreenTag = current.id
44
- break
45
- }
46
- current = (current.parent as? View)
47
- }
47
+ val tags = screenTags(view).toList()
48
+ presenterScreenTag = tags.firstOrNull() ?: 0
49
+ ancestorScreenTags = tags.drop(1).toSet()
48
50
  }
49
51
 
50
52
  override fun onEventDispatch(event: Event<*>) {
51
- // Only process events for the presenter screen
52
- if (presenterScreenTag == 0 || event.viewTag != presenterScreenTag) return
53
+ // Only process events for the presenter screen and the screens enclosing it
54
+ if (presenterScreenTag == 0) return
55
+ if (event.viewTag != presenterScreenTag && event.viewTag !in ancestorScreenTags) return
53
56
 
54
57
  when (event.eventName) {
55
- "topWillDisappear" -> delegate?.presenterScreenWillDisappear()
56
- "topWillAppear" -> delegate?.presenterScreenWillAppear()
58
+ "topWillDisappear" -> {
59
+ if (disappearedScreenTag != 0) return
60
+ disappearedScreenTag = event.viewTag
61
+ delegate?.presenterScreenWillDisappear()
62
+ }
63
+
64
+ "topWillAppear" -> {
65
+ if (event.viewTag != disappearedScreenTag) return
66
+ disappearedScreenTag = 0
67
+ delegate?.presenterScreenWillAppear()
68
+ }
57
69
  }
58
70
  }
59
71
 
60
72
  companion object {
61
73
  private fun isScreenView(view: View): Boolean = view.javaClass.name == RN_SCREENS_VIEW_CLASS
74
+
75
+ // Tags of the screens enclosing the view, innermost first
76
+ private fun screenTags(view: View?): Sequence<Int> = generateSequence(view) { it.parent as? View }.filter(::isScreenView).map { it.id }
77
+
78
+ fun findPresenterScreenTag(view: View?): Int = screenTags(view).firstOrNull() ?: 0
62
79
  }
63
80
  }
@@ -40,6 +40,15 @@ class TrueSheetBottomSheetBehavior<V : View> : BottomSheetBehavior<V>() {
40
40
  hideFriction = HIDE_FRICTION
41
41
  }
42
42
 
43
+ // Material's first layout registers an IME callback that rides the sheet's translationY
44
+ // with the keyboard. TrueSheet positions sheets for the keyboard itself, and the callback
45
+ // overwrites a parent's stack translation when the keyboard rises mid-translation.
46
+ override fun onLayoutChild(parent: CoordinatorLayout, child: V, layoutDirection: Int): Boolean {
47
+ val handled = super.onLayoutChild(parent, child, layoutDirection)
48
+ ViewCompat.setWindowInsetsAnimationCallback(child, null)
49
+ return handled
50
+ }
51
+
43
52
  // With the short threshold, settle at or below the lowest detent like
44
53
  // Compose Material3's sheet instead of Material's half-height rule.
45
54
  private fun releaseState(child: V, dragged: Boolean, velocityY: Float): Int? {
@@ -1,7 +1,6 @@
1
1
  package com.lodev09.truesheet.core
2
2
 
3
3
  import android.annotation.SuppressLint
4
- import android.content.res.Configuration
5
4
  import android.graphics.Color
6
5
  import android.graphics.Outline
7
6
  import android.graphics.Rect
@@ -20,6 +19,7 @@ import com.facebook.react.uimanager.ThemedReactContext
20
19
  import com.google.android.material.bottomsheet.BottomSheetBehavior
21
20
  import com.lodev09.truesheet.TrueSheetPlacement
22
21
  import com.lodev09.truesheet.utils.ScreenUtils
22
+ import com.lodev09.truesheet.utils.isDarkMode
23
23
 
24
24
  interface TrueSheetBottomSheetViewDelegate {
25
25
  val isTopmostSheet: Boolean
@@ -81,14 +81,6 @@ class TrueSheetBottomSheetView(private val reactContext: ThemedReactContext) : F
81
81
  clipToPadding = false
82
82
  }
83
83
 
84
- override fun setTranslationY(translationY: Float) {
85
- // This prevents keyboard inset animations from resetting parent sheet translation
86
- if (translationY == 0f && this.translationY != 0f) {
87
- return
88
- }
89
- super.setTranslationY(translationY)
90
- }
91
-
92
84
  // =============================================================================
93
85
  // MARK: - Layout
94
86
  // =============================================================================
@@ -186,13 +178,8 @@ class TrueSheetBottomSheetView(private val reactContext: ThemedReactContext) : F
186
178
  }
187
179
  }
188
180
 
189
- fun getDefaultBackgroundColor(): Int {
190
- val isNight = (
191
- reactContext.resources.configuration.uiMode and
192
- Configuration.UI_MODE_NIGHT_MASK
193
- ) == Configuration.UI_MODE_NIGHT_YES
194
- return if (isNight) COLOR_SURFACE_CONTAINER_LOW_DARK else COLOR_SURFACE_CONTAINER_LOW_LIGHT
195
- }
181
+ fun getDefaultBackgroundColor(): Int =
182
+ if (reactContext.isDarkMode) COLOR_SURFACE_CONTAINER_LOW_DARK else COLOR_SURFACE_CONTAINER_LOW_LIGHT
196
183
 
197
184
  fun setupElevation() {
198
185
  val value = delegate?.sheetElevation ?: DEFAULT_ELEVATION
@@ -28,6 +28,9 @@ interface TrueSheetCoordinatorLayoutDelegate {
28
28
  * didn't follow, `keyboardWasVisible` whether the keyboard was up when it began.
29
29
  */
30
30
  fun coordinatorLayoutDidPullDown(overdragPx: Int, keyboardWasVisible: Boolean)
31
+
32
+ // A touch that began inside the sheet ended, after the behavior handled the release
33
+ fun coordinatorLayoutDidEndTouch()
31
34
  }
32
35
 
33
36
  /**
@@ -86,6 +89,10 @@ class TrueSheetCoordinatorLayout(context: Context) :
86
89
  private var pullOverdrag = 0f
87
90
  private var pullKeyboardVisible = false
88
91
 
92
+ // A touch that began inside the sheet is still down
93
+ val isTouchingSheet: Boolean
94
+ get() = pullTracking
95
+
89
96
  init {
90
97
  layoutParams = LayoutParams(
91
98
  LayoutParams.MATCH_PARENT,
@@ -150,10 +157,16 @@ class TrueSheetCoordinatorLayout(context: Context) :
150
157
  if (dy > dx && pullOverdrag > PULL_DOWN_THRESHOLD_DP.dpToPx()) {
151
158
  delegate?.coordinatorLayoutDidPullDown(pullOverdrag.toInt(), pullKeyboardVisible)
152
159
  }
160
+ delegate?.coordinatorLayoutDidEndTouch()
153
161
  return handled
154
162
  }
155
163
 
156
- MotionEvent.ACTION_CANCEL -> pullTracking = false
164
+ MotionEvent.ACTION_CANCEL -> if (pullTracking) {
165
+ pullTracking = false
166
+ val handled = super.dispatchTouchEvent(ev)
167
+ delegate?.coordinatorLayoutDidEndTouch()
168
+ return handled
169
+ }
157
170
  }
158
171
  return super.dispatchTouchEvent(ev)
159
172
  }
@@ -34,10 +34,6 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
34
34
  View(reactContext),
35
35
  ReactPointerEventsView {
36
36
 
37
- companion object {
38
- private const val MAX_ALPHA = 0.5f
39
- }
40
-
41
37
  var delegate: TrueSheetDimViewDelegate? = null
42
38
 
43
39
  private var targetView: ViewGroup? = null
@@ -48,6 +44,10 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
48
44
  private val blockGestures: Boolean
49
45
  get() = alpha > 0f
50
46
 
47
+ fun setDimColor(color: Int) {
48
+ setBackgroundColor(color)
49
+ }
50
+
51
51
  init {
52
52
  layoutParams = ViewGroup.LayoutParams(
53
53
  ViewGroup.LayoutParams.MATCH_PARENT,
@@ -109,30 +109,24 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
109
109
  }
110
110
 
111
111
  // =============================================================================
112
- // MARK: - Alpha Calculation
112
+ // MARK: - Progress Calculation
113
113
  // =============================================================================
114
114
 
115
- fun calculateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int): Float {
115
+ /**
116
+ * How far the sheet is into its dimmed range, from 0 (undimmed) to 1 (fully dimmed).
117
+ */
118
+ fun calculateProgress(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int): Float {
116
119
  val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
117
120
  val dimmedDetentTop = getSheetTopForDetentIndex(dimmedDetentIndex)
118
121
  val belowDimmedTop = if (dimmedDetentIndex > 0) getSheetTopForDetentIndex(dimmedDetentIndex - 1) else realHeight
119
122
 
120
123
  return when {
121
- sheetTop <= dimmedDetentTop -> MAX_ALPHA
122
-
124
+ sheetTop <= dimmedDetentTop -> 1f
123
125
  sheetTop >= belowDimmedTop -> 0f
124
-
125
- else -> {
126
- val progress = 1f - (sheetTop - dimmedDetentTop).toFloat() / (belowDimmedTop - dimmedDetentTop)
127
- (progress * MAX_ALPHA).coerceIn(0f, MAX_ALPHA)
128
- }
126
+ else -> (1f - (sheetTop - dimmedDetentTop).toFloat() / (belowDimmedTop - dimmedDetentTop)).coerceIn(0f, 1f)
129
127
  }
130
128
  }
131
129
 
132
- fun interpolateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int) {
133
- alpha = calculateAlpha(sheetTop, dimmedDetentIndex, getSheetTopForDetentIndex)
134
- }
135
-
136
130
  // =============================================================================
137
131
  // MARK: - Touch Handling
138
132
  // =============================================================================
@@ -2,7 +2,6 @@ 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
6
5
  import android.graphics.Color
7
6
  import android.graphics.drawable.GradientDrawable
8
7
  import android.os.Bundle
@@ -13,6 +12,7 @@ import android.widget.FrameLayout
13
12
  import androidx.core.graphics.ColorUtils
14
13
  import androidx.core.view.ViewCompat
15
14
  import com.facebook.react.uimanager.PixelUtil.dpToPx
15
+ import com.lodev09.truesheet.utils.isDarkMode
16
16
 
17
17
  /**
18
18
  * Options for customizing the grabber appearance.
@@ -165,9 +165,7 @@ class TrueSheetGrabberView(
165
165
  }
166
166
 
167
167
  private fun getAdaptiveColor(baseColor: Int? = null): Int {
168
- val nightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
169
- val isDarkMode = nightMode == Configuration.UI_MODE_NIGHT_YES
170
- val modeColor = if (isDarkMode) Color.WHITE else Color.BLACK
168
+ val modeColor = if (context.isDarkMode) Color.WHITE else Color.BLACK
171
169
 
172
170
  return if (baseColor != null) {
173
171
  // Blend user color with mode color for adaptive effect
@@ -12,16 +12,6 @@ object TrueSheetStackManager {
12
12
 
13
13
  private val presentedSheetStack = mutableListOf<TrueSheetView>()
14
14
 
15
- /**
16
- * Gets the parent sheet at the given index, if any.
17
- * Only returns a parent if it's in the same container.
18
- * Must be called within synchronized block.
19
- */
20
- private fun getParentSheetAt(index: Int, rootContainer: ViewGroup?): TrueSheetView? {
21
- if (index <= 0) return null
22
- return presentedSheetStack[index - 1].takeIf { it.rootContainerView == rootContainer }
23
- }
24
-
25
15
  /**
26
16
  * Returns the topmost presented and visible sheet, skipping sheets that are dismissing.
27
17
  * A dismissing sheet can't parent a new one: its dim would suppress the child's own dim
@@ -33,6 +23,22 @@ object TrueSheetStackManager {
33
23
  it.viewController.run { isPresented && isSheetVisible && !isBeingDismissed }
34
24
  }
35
25
 
26
+ /**
27
+ * Returns the topmost sheet hidden by screen navigation that was presented from the same
28
+ * screen as the given sheet. Sheets presented on the covering screen must not parent to it,
29
+ * or the hidden sheet would receive focus and blur events while it can't be seen.
30
+ * Must be called within synchronized block.
31
+ */
32
+ private fun findScreenHiddenSheet(sheetView: TrueSheetView): TrueSheetView? {
33
+ val screenTag = RNScreensEventObserver.findPresenterScreenTag(sheetView)
34
+ if (screenTag == 0) return null
35
+
36
+ return presentedSheetStack.lastOrNull {
37
+ it.viewController.run { isPresented && wasHiddenByScreen && !isBeingDismissed } &&
38
+ it.screensEventObserver?.presenterScreenTag == screenTag
39
+ }
40
+ }
41
+
36
42
  /**
37
43
  * Registers a sheet in the stack and returns its parent sheet if any.
38
44
  * Only returns a parent if it's in the same container (e.g., same Screen).
@@ -40,7 +46,8 @@ object TrueSheetStackManager {
40
46
  @JvmStatic
41
47
  fun registerSheet(sheetView: TrueSheetView): TrueSheetView? {
42
48
  synchronized(presentedSheetStack) {
43
- val parentSheet = findTopmostSheet()?.takeIf { it.rootContainerView == sheetView.rootContainerView }
49
+ val parentSheet = (findTopmostSheet() ?: findScreenHiddenSheet(sheetView))
50
+ ?.takeIf { it.rootContainerView == sheetView.rootContainerView }
44
51
 
45
52
  if (!presentedSheetStack.contains(sheetView)) {
46
53
  presentedSheetStack.add(sheetView)
@@ -62,24 +69,20 @@ object TrueSheetStackManager {
62
69
 
63
70
  /**
64
71
  * Updates parent sheet translation based on the child sheet's position.
65
- * Only affects parent sheets in the same container.
66
72
  */
67
73
  @JvmStatic
68
74
  fun updateParentTranslation(sheetView: TrueSheetView) {
69
- synchronized(presentedSheetStack) {
70
- val index = presentedSheetStack.indexOf(sheetView)
71
- val parentSheet = getParentSheetAt(index, sheetView.rootContainerView) ?: return
72
-
73
- // Post to ensure layout is complete before reading position
74
- sheetView.viewController.post {
75
- val childMinSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(0)
76
- val childCurrentSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(
77
- sheetView.viewController.currentDetentIndex
78
- )
79
- // Cap to minimum detent position
80
- val childSheetTop = maxOf(childMinSheetTop, childCurrentSheetTop)
81
- parentSheet.updateTranslationForChild(childSheetTop)
82
- }
75
+ val parentSheet = sheetView.viewController.parentSheetView ?: return
76
+
77
+ // Post to ensure layout is complete before reading position
78
+ sheetView.viewController.post {
79
+ val childMinSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(0)
80
+ val childCurrentSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(
81
+ sheetView.viewController.currentDetentIndex
82
+ )
83
+ // Cap to minimum detent position
84
+ val childSheetTop = maxOf(childMinSheetTop, childCurrentSheetTop)
85
+ parentSheet.updateTranslationForChild(childSheetTop)
83
86
  }
84
87
  }
85
88
 
@@ -113,18 +116,6 @@ object TrueSheetStackManager {
113
116
  }
114
117
  }
115
118
 
116
- /**
117
- * Gets the parent sheet of the given sheet, if any.
118
- * Only returns a parent if it's in the same container.
119
- */
120
- @JvmStatic
121
- fun getParentSheet(sheetView: TrueSheetView): TrueSheetView? {
122
- synchronized(presentedSheetStack) {
123
- val index = presentedSheetStack.indexOf(sheetView)
124
- return getParentSheetAt(index, sheetView.rootContainerView)
125
- }
126
- }
127
-
128
119
  /**
129
120
  * Returns true if the given sheet is the topmost presented sheet in its container.
130
121
  */
@@ -0,0 +1,19 @@
1
+ package com.lodev09.truesheet.utils
2
+
3
+ import android.content.Context
4
+ import android.content.res.Configuration
5
+ import com.facebook.react.bridge.ColorPropConverter
6
+ import com.facebook.react.bridge.ReadableMap
7
+ import com.facebook.react.bridge.ReadableType
8
+
9
+ /**
10
+ * Resolves a processed color (number or `PlatformColor` map) at [key], or null when missing.
11
+ */
12
+ fun ReadableMap.getColorOrNull(key: String, context: Context): Int? {
13
+ if (!hasKey(key) || isNull(key)) return null
14
+ val value = if (getType(key) == ReadableType.Map) getMap(key) else getDouble(key)
15
+ return ColorPropConverter.getColor(value, context)
16
+ }
17
+
18
+ val Context.isDarkMode: Boolean
19
+ get() = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
@@ -1,6 +1,5 @@
1
1
  #include "TrueSheetContentViewShadowNode.h"
2
2
 
3
- #include <folly/ScopeGuard.h>
4
3
  #include <react/renderer/components/view/conversions.h>
5
4
  #include <react/renderer/core/ComponentDescriptor.h>
6
5
  #include <react/renderer/core/LayoutConstraints.h>
@@ -95,7 +94,12 @@ void TrueSheetContentViewShadowNode::updateNaturalHeightIfNeeded(const LayoutCon
95
94
  if (contentChanged_ || !measurement_ || !(measurement_->constraints == constraints) ||
96
95
  !(measurement_->context == measurementContext) || measurement_->errata != errata) {
97
96
  gIsMeasuringNaturalHeight = true;
98
- auto restoreMeasurement = folly::makeGuard([] { gIsMeasuringNaturalHeight = false; });
97
+ // Not folly::makeGuard: its terminate() isn't linked into RN < 0.86 on Android.
98
+ struct RestoreMeasurement {
99
+ ~RestoreMeasurement() {
100
+ gIsMeasuringNaturalHeight = false;
101
+ }
102
+ } restoreMeasurement;
99
103
  auto measurementNode =
100
104
  measurement_ ? cloneForMeasurement(*this, *measurementSourceChildren_, measurement_->root->getChildren())
101
105
  : cloneForMeasurement(*this, {}, {});
@@ -37,6 +37,7 @@ NS_ASSUME_NONNULL_BEGIN
37
37
 
38
38
  - (void)containerViewContentDidChangeSize:(CGSize)newSize;
39
39
  - (void)containerViewScrollViewDidChange;
40
+ - (void)containerViewDidMountTransaction;
40
41
 
41
42
  @optional
42
43
 
@@ -287,6 +287,12 @@ using namespace facebook::react;
287
287
  [self.delegate containerViewScrollViewDidChange];
288
288
  }
289
289
 
290
+ // Footer controls can mount, unmount, or hide deep in its subtree without
291
+ // passing through its own mount hooks or changing its size.
292
+ - (void)contentViewDidMountTransaction {
293
+ [self.delegate containerViewDidMountTransaction];
294
+ }
295
+
290
296
  #pragma mark - TrueSheetHeaderViewDelegate
291
297
 
292
298
  - (void)headerViewDidChangeSize:(CGSize)newSize {
@@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
26
26
 
27
27
  - (void)contentViewDidChangeSize:(CGSize)newSize;
28
28
  - (void)contentViewScrollViewDidChange;
29
+ - (void)contentViewDidMountTransaction;
29
30
 
30
31
  @end
31
32
 
@@ -181,6 +181,8 @@ using namespace facebook::react;
181
181
  // Layout has settled for the whole tree — the footer overlap (sheet height,
182
182
  // scroll view frame) and the keyboard inset are current here
183
183
  [self updateBottomInset];
184
+
185
+ [self.delegate contentViewDidMountTransaction];
184
186
  }
185
187
 
186
188
  #pragma mark - Scrollable
@@ -123,6 +123,11 @@ using namespace facebook::react;
123
123
 
124
124
  - (void)collectAccessibilityElementsFromView:(UIView *)view into:(NSMutableArray *)elements {
125
125
  for (UIView *subview in view.subviews) {
126
+ // Fabric hides `display: none` views instead of unmounting them
127
+ if (subview.hidden) {
128
+ continue;
129
+ }
130
+
126
131
  if (subview.isAccessibilityElement || subview.accessibilityLabel || subview.accessibilityIdentifier) {
127
132
  [elements addObject:subview];
128
133
  } else if (subview.accessibilityElements.count > 0) {
@@ -149,11 +149,17 @@ RCT_EXPORT_MODULE(TrueSheetModule)
149
149
  - (void)dismissAll:(BOOL)animated resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
150
150
  RCTExecuteOnMainQueue(^{
151
151
  @synchronized(viewRegistry) {
152
+ // Drop presents still waiting on another sheet's dismissal
153
+ for (TrueSheetView *view in viewRegistry.objectEnumerator) {
154
+ [view cancelPendingPresent];
155
+ }
156
+
152
157
  // Find the root presented sheet (one without a parent TrueSheet)
153
158
  TrueSheetView *rootSheet = nil;
154
159
 
155
160
  for (TrueSheetView *view in viewRegistry.objectEnumerator) {
156
- if (!view.viewController.isPresented) {
161
+ // A sheet still animating in counts too — dismissAnimated waits for it to land
162
+ if (!view.viewController.isPresented && !view.viewController.isBeingPresented) {
157
163
  continue;
158
164
  }
159
165
 
@@ -171,7 +177,10 @@ RCT_EXPORT_MODULE(TrueSheetModule)
171
177
  return;
172
178
  }
173
179
 
174
- [rootSheet emitDismissedPosition];
180
+ // Mid-present, the deferred dismiss tracks its own position like a regular dismiss
181
+ if (rootSheet.viewController.isPresented) {
182
+ [rootSheet emitDismissedPosition];
183
+ }
175
184
  [rootSheet dismissAnimated:animated
176
185
  completion:^(BOOL success, NSError *_Nullable error) {
177
186
  if (success) {
@@ -34,6 +34,8 @@ typedef void (^TrueSheetCompletionBlock)(BOOL success, NSError *_Nullable error)
34
34
 
35
35
  - (void)emitDismissedPosition;
36
36
 
37
+ - (BOOL)cancelPendingPresent;
38
+
37
39
  - (void)resizeToIndex:(NSInteger)index completion:(nullable TrueSheetCompletionBlock)completion;
38
40
 
39
41
  - (void)dismissStackAnimated:(BOOL)animated completion:(nullable TrueSheetCompletionBlock)completion;