@lodev09/react-native-true-sheet 4.0.0-beta.1 → 4.0.0-beta.11

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 (221) hide show
  1. package/README.md +25 -4
  2. package/android/build.gradle +22 -1
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +24 -7
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +211 -182
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +5 -1
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +14 -19
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +106 -0
  8. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayViewManager.kt +45 -0
  9. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +2 -1
  10. package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +18 -0
  11. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +82 -39
  12. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +275 -88
  13. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +26 -1
  14. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +75 -3
  15. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +56 -34
  16. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +137 -0
  17. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +15 -0
  18. package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +1 -1
  19. package/android/src/main/java/com/lodev09/truesheet/utils/TouchEventDeduper.kt +31 -0
  20. package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +19 -7
  21. package/android/src/main/jni/TrueSheetSpec.h +1 -0
  22. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +0 -8
  23. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +52 -29
  24. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +6 -1
  25. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +1 -1
  26. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +6 -4
  27. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +7 -0
  28. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +46 -0
  29. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h +21 -0
  30. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.cpp +17 -0
  31. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h +36 -0
  32. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +4 -35
  33. package/ios/TrueSheetContainerView.h +14 -7
  34. package/ios/TrueSheetContainerView.mm +10 -7
  35. package/ios/TrueSheetContentView.h +31 -15
  36. package/ios/TrueSheetContentView.mm +148 -188
  37. package/ios/TrueSheetFooterView.h +6 -0
  38. package/ios/TrueSheetFooterView.mm +48 -2
  39. package/ios/TrueSheetModule.h +2 -7
  40. package/ios/TrueSheetModule.mm +7 -16
  41. package/ios/TrueSheetOverlayView.h +32 -0
  42. package/ios/TrueSheetOverlayView.mm +161 -0
  43. package/ios/TrueSheetView.mm +171 -29
  44. package/ios/TrueSheetViewController.h +10 -0
  45. package/ios/TrueSheetViewController.mm +256 -74
  46. package/ios/core/TrueSheetGrabberView.mm +13 -3
  47. package/ios/core/TrueSheetOverlayContainerView.h +31 -0
  48. package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
  49. package/ios/events/TrueSheetLifecycleEvents.h +2 -0
  50. package/ios/events/TrueSheetLifecycleEvents.mm +8 -0
  51. package/ios/tvos/TrueSheetStubs.mm +12 -0
  52. package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
  53. package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
  54. package/lib/module/TrueSheet.js +149 -34
  55. package/lib/module/TrueSheet.js.map +1 -1
  56. package/lib/module/TrueSheet.web.js +122 -53
  57. package/lib/module/TrueSheet.web.js.map +1 -1
  58. package/lib/module/TrueSheetOverlay.js +30 -0
  59. package/lib/module/TrueSheetOverlay.js.map +1 -0
  60. package/lib/module/TrueSheetOverlay.web.js +35 -0
  61. package/lib/module/TrueSheetOverlay.web.js.map +1 -0
  62. package/lib/module/TrueSheetPeek.web.js +3 -3
  63. package/lib/module/TrueSheetPeek.web.js.map +1 -1
  64. package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  65. package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  66. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +5 -0
  67. package/lib/module/index.js +1 -0
  68. package/lib/module/index.js.map +1 -1
  69. package/lib/module/mocks/index.js +10 -0
  70. package/lib/module/mocks/index.js.map +1 -1
  71. package/lib/module/mocks/navigation.js +31 -21
  72. package/lib/module/mocks/navigation.js.map +1 -1
  73. package/lib/module/mocks/navigationExpoRouter.js +21 -0
  74. package/lib/module/mocks/navigationExpoRouter.js.map +1 -0
  75. package/lib/module/mocks/reanimated.js +1 -1
  76. package/lib/module/mocks/reanimated.js.map +1 -1
  77. package/lib/module/navigation/{TrueSheetView.js → TrueSheetNavigatorContent.js} +14 -12
  78. package/lib/module/navigation/TrueSheetNavigatorContent.js.map +1 -0
  79. package/lib/module/navigation/actions.js +50 -0
  80. package/lib/module/navigation/actions.js.map +1 -0
  81. package/lib/module/navigation/base-types.js +2 -0
  82. package/lib/module/navigation/base-types.js.map +1 -0
  83. package/lib/module/navigation/{TrueSheetRouter.js → createTrueSheetRouter.js} +11 -24
  84. package/lib/module/navigation/createTrueSheetRouter.js.map +1 -0
  85. package/lib/module/navigation/expo-router/base-types.js +2 -0
  86. package/lib/module/navigation/expo-router/base-types.js.map +1 -0
  87. package/lib/module/navigation/expo-router/index.js +50 -0
  88. package/lib/module/navigation/expo-router/index.js.map +1 -0
  89. package/lib/module/navigation/expo-router/types.js +4 -0
  90. package/lib/module/navigation/expo-router/types.js.map +1 -0
  91. package/lib/module/navigation/index.js +43 -2
  92. package/lib/module/navigation/index.js.map +1 -1
  93. package/lib/module/navigation/navigator.js +10 -0
  94. package/lib/module/navigation/navigator.js.map +1 -0
  95. package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js +2 -2
  96. package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js.map +1 -1
  97. package/lib/module/navigation/screen/TrueSheetScreen.js +2 -2
  98. package/lib/module/navigation/screen/TrueSheetScreen.js.map +1 -1
  99. package/lib/module/navigation/screen/useSheetScreenState.js +5 -4
  100. package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
  101. package/lib/module/navigation/useTrueSheetNavigation.js +1 -1
  102. package/lib/module/navigation/useTrueSheetNavigation.js.map +1 -1
  103. package/lib/module/web/dom.js +10 -0
  104. package/lib/module/web/dom.js.map +1 -0
  105. package/lib/module/web/vaul/constants.js +4 -0
  106. package/lib/module/web/vaul/constants.js.map +1 -1
  107. package/lib/module/web/vaul/context.js.map +1 -1
  108. package/lib/module/web/vaul/helpers.js +10 -4
  109. package/lib/module/web/vaul/helpers.js.map +1 -1
  110. package/lib/module/web/vaul/index.js +35 -2
  111. package/lib/module/web/vaul/index.js.map +1 -1
  112. package/lib/typescript/src/TrueSheet.d.ts +6 -0
  113. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  114. package/lib/typescript/src/TrueSheet.types.d.ts +72 -1
  115. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  116. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  117. package/lib/typescript/src/TrueSheetOverlay.d.ts +8 -0
  118. package/lib/typescript/src/TrueSheetOverlay.d.ts.map +1 -0
  119. package/lib/typescript/src/TrueSheetOverlay.web.d.ts +8 -0
  120. package/lib/typescript/src/TrueSheetOverlay.web.d.ts.map +1 -0
  121. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
  122. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts +6 -0
  123. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts.map +1 -0
  124. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +4 -0
  125. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  126. package/lib/typescript/src/index.d.ts +1 -0
  127. package/lib/typescript/src/index.d.ts.map +1 -1
  128. package/lib/typescript/src/mocks/index.d.ts +4 -0
  129. package/lib/typescript/src/mocks/index.d.ts.map +1 -1
  130. package/lib/typescript/src/mocks/navigation.d.ts +5 -1
  131. package/lib/typescript/src/mocks/navigation.d.ts.map +1 -1
  132. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +22 -0
  133. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -0
  134. package/lib/typescript/src/mocks/reanimated.d.ts.map +1 -1
  135. package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts +5 -0
  136. package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts.map +1 -0
  137. package/lib/typescript/src/navigation/actions.d.ts +71 -0
  138. package/lib/typescript/src/navigation/actions.d.ts.map +1 -0
  139. package/lib/typescript/src/navigation/base-types.d.ts +9 -0
  140. package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
  141. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +3 -0
  142. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -0
  143. package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
  144. package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
  145. package/lib/typescript/src/navigation/expo-router/index.d.ts +37 -0
  146. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -0
  147. package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
  148. package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
  149. package/lib/typescript/src/navigation/index.d.ts +42 -3
  150. package/lib/typescript/src/navigation/index.d.ts.map +1 -1
  151. package/lib/typescript/src/navigation/navigator.d.ts +7 -0
  152. package/lib/typescript/src/navigation/navigator.d.ts.map +1 -0
  153. package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts +1 -1
  154. package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts.map +1 -1
  155. package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts +1 -1
  156. package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts.map +1 -1
  157. package/lib/typescript/src/navigation/screen/types.d.ts +3 -4
  158. package/lib/typescript/src/navigation/screen/types.d.ts.map +1 -1
  159. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +5 -6
  160. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
  161. package/lib/typescript/src/navigation/types.d.ts +65 -5
  162. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  163. package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts +1 -1
  164. package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts.map +1 -1
  165. package/lib/typescript/src/web/dom.d.ts +7 -0
  166. package/lib/typescript/src/web/dom.d.ts.map +1 -0
  167. package/lib/typescript/src/web/vaul/constants.d.ts +1 -0
  168. package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
  169. package/lib/typescript/src/web/vaul/context.d.ts +1 -0
  170. package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
  171. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
  172. package/lib/typescript/src/web/vaul/index.d.ts +7 -1
  173. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  174. package/package.json +32 -9
  175. package/react-native.config.js +1 -0
  176. package/src/TrueSheet.tsx +184 -46
  177. package/src/TrueSheet.types.ts +77 -1
  178. package/src/TrueSheet.web.tsx +135 -64
  179. package/src/TrueSheetOverlay.tsx +24 -0
  180. package/src/TrueSheetOverlay.web.tsx +33 -0
  181. package/src/TrueSheetPeek.web.tsx +4 -3
  182. package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  183. package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  184. package/src/fabric/TrueSheetViewNativeComponent.ts +5 -0
  185. package/src/index.ts +1 -0
  186. package/src/mocks/index.ts +7 -0
  187. package/src/mocks/navigation.ts +32 -25
  188. package/src/mocks/navigationExpoRouter.ts +41 -0
  189. package/src/mocks/reanimated.ts +8 -9
  190. package/src/navigation/{TrueSheetView.tsx → TrueSheetNavigatorContent.tsx} +24 -17
  191. package/src/navigation/actions.ts +78 -0
  192. package/src/navigation/base-types.ts +20 -0
  193. package/src/navigation/createTrueSheetRouter.ts +194 -0
  194. package/src/navigation/expo-router/base-types.ts +18 -0
  195. package/src/navigation/expo-router/index.ts +90 -0
  196. package/src/navigation/expo-router/types.ts +116 -0
  197. package/src/navigation/index.ts +68 -2
  198. package/src/navigation/navigator.ts +18 -0
  199. package/src/navigation/screen/ReanimatedTrueSheetScreen.tsx +2 -2
  200. package/src/navigation/screen/TrueSheetScreen.tsx +2 -2
  201. package/src/navigation/screen/types.ts +4 -6
  202. package/src/navigation/screen/useSheetScreenState.ts +10 -15
  203. package/src/navigation/types.ts +99 -16
  204. package/src/navigation/useTrueSheetNavigation.ts +1 -1
  205. package/src/web/dom.ts +9 -0
  206. package/src/web/vaul/constants.ts +4 -0
  207. package/src/web/vaul/context.ts +1 -0
  208. package/src/web/vaul/helpers.ts +13 -6
  209. package/src/web/vaul/index.tsx +46 -1
  210. package/lib/module/navigation/TrueSheetRouter.js.map +0 -1
  211. package/lib/module/navigation/TrueSheetView.js.map +0 -1
  212. package/lib/module/navigation/createTrueSheetNavigator.js +0 -63
  213. package/lib/module/navigation/createTrueSheetNavigator.js.map +0 -1
  214. package/lib/typescript/src/navigation/TrueSheetRouter.d.ts +0 -57
  215. package/lib/typescript/src/navigation/TrueSheetRouter.d.ts.map +0 -1
  216. package/lib/typescript/src/navigation/TrueSheetView.d.ts +0 -10
  217. package/lib/typescript/src/navigation/TrueSheetView.d.ts.map +0 -1
  218. package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts +0 -35
  219. package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts.map +0 -1
  220. package/src/navigation/TrueSheetRouter.ts +0 -234
  221. package/src/navigation/createTrueSheetNavigator.tsx +0 -93
@@ -37,8 +37,10 @@ import com.lodev09.truesheet.core.TrueSheetDimViewDelegate
37
37
  import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
38
38
  import com.lodev09.truesheet.core.TrueSheetKeyboardObserverDelegate
39
39
  import com.lodev09.truesheet.core.TrueSheetStackManager
40
+ import com.lodev09.truesheet.utils.Insets
40
41
  import com.lodev09.truesheet.utils.KeyboardUtils
41
42
  import com.lodev09.truesheet.utils.ScreenUtils
43
+ import com.lodev09.truesheet.utils.TouchEventDeduper
42
44
 
43
45
  // =============================================================================
44
46
  // MARK: - Data Types & Delegate Protocol
@@ -53,6 +55,7 @@ interface TrueSheetViewControllerDelegate {
53
55
  fun viewControllerDidPresent(index: Int, position: Float, detent: Float)
54
56
  fun viewControllerWillDismiss()
55
57
  fun viewControllerDidDismiss(parent: TrueSheetView?)
58
+ fun viewControllerDidAttemptDismiss()
56
59
  fun viewControllerDidChangeDetent(index: Int, position: Float, detent: Float)
57
60
  fun viewControllerDidDragBegin(index: Int, position: Float, detent: Float)
58
61
  fun viewControllerDidDragChange(index: Int, position: Float, detent: Float)
@@ -121,6 +124,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
121
124
  private const val TRANSLATE_ANIMATION_DURATION = 200L
122
125
  private const val DISMISS_DURATION = 200L
123
126
  private const val SCREEN_FADE_DURATION = 150L
127
+
128
+ // Mirrors BottomSheetBehavior.HIDE_THRESHOLD — how far past the lowest stop,
129
+ // as a fraction of its height, a release hides a dismissible sheet
130
+ private const val HIDE_THRESHOLD = 0.5f
124
131
  }
125
132
 
126
133
  // =============================================================================
@@ -161,6 +168,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
161
168
  private var pendingDetentIndex: Int = -1
162
169
 
163
170
  private var interactionState: InteractionState = InteractionState.Idle
171
+
172
+ // A content/header/footer size change arrived mid-drag — reconfiguring then
173
+ // would reset behavior state and kill the gesture, so it's applied on settle.
174
+ private var hasPendingSizeChange = false
164
175
  internal var isBeingDismissed = false
165
176
  private set
166
177
  var wasHiddenByScreen = false
@@ -174,6 +185,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
174
185
  // Keyboard State
175
186
  private var detentIndexBeforeKeyboard: Int = -1
176
187
  private var isKeyboardDismissProgrammatic = false
188
+
189
+ // Sheet top when a keyboard-floor drag was released; -1 once the settle target is known
190
+ private var keyboardSettleStartTop = -1
177
191
  private var focusedViewBeforeBlur: View? = null
178
192
 
179
193
  // Promises
@@ -199,6 +213,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
199
213
  // the finger drifts outside the footer's rect mid-gesture.
200
214
  private var footerOwnsTouchStream = false
201
215
 
216
+ private val touchDeduper = TouchEventDeduper()
217
+
202
218
  private val eventDispatcher
203
219
  get() = delegate?.eventDispatcher
204
220
 
@@ -224,6 +240,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
224
240
  behavior?.scrollingExpandsSheet = value?.scrollingExpandsSheet ?: true
225
241
  }
226
242
 
243
+ // React tag of the user-provided scrollable within the content (see the scrollableRef prop)
244
+ var scrollableHandle: Int = -1
245
+
227
246
  override var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
228
247
  set(value) {
229
248
  field = if (value < 0) DEFAULT_CORNER_RADIUS.dpToPx() else value
@@ -267,9 +286,29 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
267
286
  val screenWidth: Int
268
287
  get() = ScreenUtils.getScreenWidth(reactContext)
269
288
 
289
+ // Cached per layout/configuration pass — the WindowManager and
290
+ // rootWindowInsets queries behind these allocate, and they're read many
291
+ // times per animation frame (position emission, detent interpolation,
292
+ // stacked sheet sync)
293
+ private var cachedRealScreenHeight: Int = 0
294
+ private var cachedInsets: Insets? = null
295
+
296
+ private fun invalidateScreenMetrics() {
297
+ cachedRealScreenHeight = 0
298
+ cachedInsets = null
299
+ }
300
+
270
301
  // Includes system bars for accurate positioning
271
302
  override val realScreenHeight: Int
272
- get() = ScreenUtils.getRealScreenHeight(reactContext)
303
+ get() {
304
+ if (cachedRealScreenHeight == 0) {
305
+ cachedRealScreenHeight = ScreenUtils.getRealScreenHeight(reactContext)
306
+ }
307
+ return cachedRealScreenHeight
308
+ }
309
+
310
+ private val insets: Insets
311
+ get() = cachedInsets ?: ScreenUtils.getInsets(reactContext).also { cachedInsets = it }
273
312
 
274
313
  // Content Measurements
275
314
  // Cached values used during dismiss when container is unmounted
@@ -306,16 +345,30 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
306
345
  private val isKeyboardTransitioning: Boolean
307
346
  get() = keyboardObserver?.isTransitioning ?: false
308
347
 
348
+ // Keyboard is up or animating — detents are (or are about to be) keyboard-shifted
349
+ val isKeyboardActive: Boolean
350
+ get() = keyboardInset > 0 || isKeyboardTransitioning
351
+
352
+ // While the keyboard holds the sheet up, the keyboard-free first detent is the
353
+ // collapsed stop — landing there dismisses the keyboard, and (when dismissible)
354
+ // dragging more than half its height below it hides the sheet too, like iOS.
355
+ override val hasKeyboardFloor: Boolean
356
+ get() = keyboardInset > 0
357
+
358
+ // The detent JS last heard about — the keyboard expansion is never reported
359
+ private val knownDetentIndex: Int
360
+ get() = if (detentIndexBeforeKeyboard >= 0) detentIndexBeforeKeyboard else currentDetentIndex
361
+
309
362
  fun isFocusedViewWithinSheet(): Boolean {
310
363
  val sheet = sheetView ?: return false
311
364
  return keyboardObserver?.isFocusedViewWithinSheet(sheet) ?: false
312
365
  }
313
366
 
314
367
  val bottomInset: Int
315
- get() = if (edgeToEdgeEnabled) ScreenUtils.getInsets(reactContext).bottom else 0
368
+ get() = if (edgeToEdgeEnabled) insets.bottom else 0
316
369
 
317
370
  override val topInset: Int
318
- get() = if (edgeToEdgeEnabled) ScreenUtils.getInsets(reactContext).top else 0
371
+ get() = if (edgeToEdgeEnabled) insets.top else 0
319
372
 
320
373
  override val contentBottomInset: Int
321
374
  get() = if (insetAdjustment == TrueSheetInsetAdjustment.AUTOMATIC) bottomInset else 0
@@ -328,6 +381,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
328
381
  }
329
382
 
330
383
  // Sheet State
384
+ val isSettling: Boolean
385
+ get() = behavior?.state == BottomSheetBehavior.STATE_SETTLING
386
+
331
387
  val isExpanded: Boolean
332
388
  get() {
333
389
  val sheetTop = sheetView?.top ?: return false
@@ -389,6 +445,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
389
445
  sheetView = null
390
446
 
391
447
  interactionState = InteractionState.Idle
448
+ hasPendingSizeChange = false
392
449
  isBeingDismissed = false
393
450
  isPresented = false
394
451
  isSheetVisible = false
@@ -437,6 +494,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
437
494
  // =============================================================================
438
495
 
439
496
  override fun coordinatorLayoutDidLayout(changed: Boolean) {
497
+ if (changed) invalidateScreenMetrics()
498
+
440
499
  // Reposition footer when layout changes
441
500
  if (isPresented && changed) {
442
501
  positionFooter()
@@ -444,6 +503,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
444
503
  }
445
504
 
446
505
  override fun coordinatorLayoutDidChangeConfiguration() {
506
+ invalidateScreenMetrics()
447
507
  if (!isPresented) return
448
508
 
449
509
  sheetView?.updateGravity()
@@ -455,6 +515,28 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
455
515
  override fun findScrollView(): ViewGroup? = containerView?.contentView?.findScrollView()
456
516
  override fun findSheetView(): TrueSheetBottomSheetView? = sheetView
457
517
 
518
+ override fun coordinatorLayoutDidPullDown(overdragPx: Int, keyboardWasVisible: Boolean) {
519
+ if (!isPresented || dismissible || !draggable) return
520
+ val sheet = sheetView ?: return
521
+
522
+ // Only a pull clamped at the lowest stop is a blocked drag-to-dismiss — a
523
+ // child that swallowed the gesture higher up isn't
524
+ val lowestTop = detentCalculator.getLowestSheetTop()
525
+ if (sheet.top < lowestTop) return
526
+
527
+ if (keyboardWasVisible) {
528
+ // A sheet that couldn't move at all (first detent already full height) never
529
+ // began a drag, so the keyboard is still up — dismiss it here
530
+ if (hasKeyboardFloor && !isKeyboardDismissProgrammatic) commitDetentAndDismissKeyboard(0, sheet)
531
+
532
+ // Past the floor by the distance that would hide a dismissible sheet, like iOS
533
+ val floorHeight = realScreenHeight - lowestTop
534
+ if (overdragPx < floorHeight * HIDE_THRESHOLD) return
535
+ }
536
+
537
+ delegate?.viewControllerDidAttemptDismiss()
538
+ }
539
+
458
540
  // =============================================================================
459
541
  // MARK: - TrueSheetDimViewDelegate
460
542
  // =============================================================================
@@ -492,8 +574,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
492
574
 
493
575
  override fun bottomSheetViewDidTapGrabber() {
494
576
  val nextIndex = (currentDetentIndex + 1) % detents.size
495
- if (nextIndex == 0 && detents.size == 1 && dismissible) {
496
- dismiss(animated = true)
577
+ if (nextIndex == 0 && detents.size == 1) {
578
+ if (dismissible) {
579
+ dismiss(animated = true)
580
+ } else {
581
+ delegate?.viewControllerDidAttemptDismiss()
582
+ }
497
583
  } else {
498
584
  setStateForDetentIndex(nextIndex)
499
585
  }
@@ -510,6 +596,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
510
596
  setStateForDetentIndex(currentDetentIndex - 1)
511
597
  } else if (dismissible) {
512
598
  dismiss(animated = true)
599
+ } else {
600
+ delegate?.viewControllerDidAttemptDismiss()
513
601
  }
514
602
  }
515
603
 
@@ -523,7 +611,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
523
611
  }
524
612
 
525
613
  override fun onSlide(sheetView: View, slideOffset: Float) {
526
- handleSlide(sheetView, slideOffset)
614
+ handleSlide(sheetView)
527
615
  }
528
616
  }
529
617
 
@@ -552,27 +640,40 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
552
640
  }
553
641
  }
554
642
 
555
- private fun handleSlide(sheetView: View, slideOffset: Float) {
643
+ private fun handleSlide(sheetView: View) {
556
644
  // Skip during dismiss animation
557
645
  if (isBeingDismissed) return
558
646
 
559
647
  val behavior = behavior ?: return
560
648
 
561
649
  when (behavior.state) {
562
- BottomSheetBehavior.STATE_DRAGGING,
563
- BottomSheetBehavior.STATE_SETTLING -> handleDragChange(sheetView)
650
+ BottomSheetBehavior.STATE_DRAGGING -> handleDragChange(sheetView)
651
+
652
+ BottomSheetBehavior.STATE_SETTLING -> {
653
+ handleDragChange(sheetView)
654
+ dismissKeyboardForSettleTarget(sheetView)
655
+ }
564
656
 
565
657
  else -> { }
566
658
  }
567
659
 
568
660
  emitChangePositionDelegate(sheetView.top)
569
661
 
662
+ // Drive the parent's stack translation from our actual frame position —
663
+ // an independently-timed animation lags behind (e.g. auto detent shrink)
664
+ // and reveals the parent above this sheet's top edge. Skipped while
665
+ // dragging so the parent stays put during drag-to-dismiss, and while the
666
+ // keyboard is active so growing for it doesn't move the parent (like iOS).
667
+ if (!isPresentAnimating && interactionState !is InteractionState.Dragging && !isKeyboardActive) {
668
+ syncParentTranslation(sheetView.top)
669
+ }
670
+
570
671
  // Position on every slide frame — during keyboard transitions the IME
571
672
  // animation callbacks are sparser than the sheet's settle frames, and the
572
673
  // footer rides the sheet between them, jittering against the keyboard.
573
674
  // positionFooter derives from current state, so double-driving it with
574
675
  // keyboardDidChangeHeight is harmless.
575
- positionFooter(slideOffset)
676
+ positionFooter()
576
677
 
577
678
  if (!isKeyboardTransitioning) {
578
679
  updateDimAmount(sheetView.top)
@@ -582,11 +683,22 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
582
683
  private fun handleStateSettled(sheetView: View, newState: Int) {
583
684
  if (interactionState is InteractionState.Reconfiguring) return
584
685
 
686
+ // A release already at a stop settles without a slide frame
687
+ keyboardSettleStartTop = -1
688
+
585
689
  // Sheet has reached a stable state — any in-flight resize target is now stale,
586
690
  // whether settled at the target, interrupted by drag, or superseded.
587
691
  pendingDetentIndex = -1
588
692
 
589
- val index = detentCalculator.getDetentIndexForState(newState) ?: return
693
+ val index = detentCalculator.getDetentIndexForState(newState) ?: run {
694
+ // Unmapped settle — end any drag anyway so deferred size changes aren't
695
+ // dropped forever (a leaked Dragging state skips every reconfigure)
696
+ if (interactionState is InteractionState.Dragging) {
697
+ interactionState = InteractionState.Idle
698
+ }
699
+ flushPendingSizeChange()
700
+ return
701
+ }
590
702
  val position = getPositionDpForView(sheetView)
591
703
  val detentInfo = DetentInfo(index, position)
592
704
 
@@ -597,18 +709,26 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
597
709
  return
598
710
  }
599
711
 
712
+ val wasDragging = interactionState is InteractionState.Dragging
713
+
600
714
  when (interactionState) {
601
715
  is InteractionState.Dragging -> {
602
716
  val detent = detentCalculator.getDetentValueForIndex(detentInfo.index)
603
717
  delegate?.viewControllerDidDragEnd(detentInfo.index, detentInfo.position, detent)
604
718
 
605
- // Skip detent change if keyboard inset is still active — detent mapping is unreliable.
606
- // keyboardWillHide will recalculate detents and settle at the correct index.
607
- if (detentInfo.index != currentDetentIndex && keyboardInset == 0) {
608
- currentDetentIndex = detentInfo.index
609
- setupDimmedBackground()
610
- delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
611
- this@TrueSheetViewController.sheetView?.updateGrabberAccessibilityValue(detentInfo.index, detents.size)
719
+ // Skip detent change if keyboard inset is still active — the keyboard floor
720
+ // landing below commits the detent instead.
721
+ if (keyboardInset == 0) {
722
+ val changed = detentInfo.index != knownDetentIndex
723
+ detentIndexBeforeKeyboard = -1
724
+ if (detentInfo.index != currentDetentIndex) {
725
+ currentDetentIndex = detentInfo.index
726
+ setupDimmedBackground()
727
+ }
728
+ if (changed) {
729
+ delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
730
+ this@TrueSheetViewController.sheetView?.updateGrabberAccessibilityValue(detentInfo.index, detents.size)
731
+ }
612
732
  }
613
733
 
614
734
  interactionState = InteractionState.Idle
@@ -626,14 +746,29 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
626
746
  }
627
747
  }
628
748
 
629
- // Settled resize the container to the settled detent (applies deferred shrinks)
630
- updateStateDimensions()
749
+ if (hasKeyboardFloor && wasDragging && newState != BottomSheetBehavior.STATE_EXPANDED) {
750
+ // Dragged down onto a keyboard-free stop (the floor or the keyboard detent)
751
+ // dismiss the keyboard and commit that detent. keyboardWillHide reconfigures
752
+ // the detents (and the container) to it.
753
+ commitDetentAndDismissKeyboard(index, sheetView)
754
+ } else {
755
+ // Settled — resize the container to the settled detent (applies deferred shrinks)
756
+ updateStateDimensions()
757
+ }
631
758
 
632
759
  if (!isKeyboardTransitioning) {
633
760
  updateDimAmount(animated = true)
634
761
  }
635
762
 
636
763
  TrueSheetStackManager.updateBackgroundAccessibility()
764
+
765
+ flushPendingSizeChange()
766
+ }
767
+
768
+ private fun flushPendingSizeChange() {
769
+ if (!hasPendingSizeChange) return
770
+ hasPendingSizeChange = false
771
+ setupSheetDetentsForSizeChange()
637
772
  }
638
773
 
639
774
  // =============================================================================
@@ -695,6 +830,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
695
830
  val sheet = this.sheetView ?: return
696
831
  if (isPresented) return
697
832
 
833
+ invalidateScreenMetrics()
698
834
  shouldAnimatePresent = animated
699
835
  currentDetentIndex = detentIndex
700
836
  interactionState = InteractionState.Idle
@@ -732,9 +868,15 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
732
868
  fun resize(detentIndex: Int) {
733
869
  if (!isPresented) return
734
870
 
735
- // Commit the new index so keyboardWillHide restores to it instead of the stale one
871
+ // Commit the new index so keyboardWillHide restores to it instead of the stale
872
+ // one; the sheet keeps avoiding the keyboard meanwhile
736
873
  if (detentIndexBeforeKeyboard >= 0) {
737
874
  detentIndexBeforeKeyboard = detentIndex
875
+ if (hasKeyboardFloor) {
876
+ resizePromise?.invoke()
877
+ resizePromise = null
878
+ return
879
+ }
738
880
  }
739
881
 
740
882
  pendingDetentIndex = detentIndex
@@ -863,11 +1005,15 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
863
1005
 
864
1006
  val maxAvailableHeight = realScreenHeight - topInset
865
1007
 
866
- val peekHeight = minOf(detentCalculator.getDetentHeight(detents[0]), maxAvailableHeight)
1008
+ // With a keyboard floor only the last detent stays keyboard-shifted; the
1009
+ // collapsed and half stops are the keyboard-free first and second detents,
1010
+ // so a short drag down from the keyboard-expanded sheet rests at the second
1011
+ // and a long one at the first, like iOS
1012
+ val peekHeight = minOf(detentCalculator.getDetentHeight(detents[0], includeKeyboard = !hasKeyboardFloor), maxAvailableHeight)
867
1013
 
868
1014
  val halfExpandedDetentHeight = when (detents.size) {
869
1015
  1 -> peekHeight
870
- else -> detentCalculator.getDetentHeight(detents[1])
1016
+ else -> detentCalculator.getDetentHeight(detents[1], includeKeyboard = !hasKeyboardFloor)
871
1017
  }
872
1018
 
873
1019
  val maxDetentHeight = minOf(detentCalculator.getDetentHeight(detents.last()), maxAvailableHeight)
@@ -877,8 +1023,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
877
1023
 
878
1024
  val expandedOffset = realScreenHeight - maxDetentHeight
879
1025
 
880
- // fitToContents works better with <= 2 detents when no expanded offset
881
- val fitToContents = detents.size < 3 && expandedOffset == 0
1026
+ // fitToContents works better with <= 2 detents when no expanded offset, but
1027
+ // it drops the half stop the keyboard floor relies on
1028
+ val fitToContents = !hasKeyboardFloor && detents.size < 3 && expandedOffset == 0
882
1029
 
883
1030
  configureDetents(
884
1031
  behavior = behavior,
@@ -919,10 +1066,15 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
919
1066
  }
920
1067
 
921
1068
  fun setupSheetDetentsForSizeChange() {
922
- // Skip while dragging — the size change is driven by the drag itself (the
1069
+ // Defer while dragging — the size change is driven by the drag itself (the
923
1070
  // container tracks the sheet's visible height), and reconfiguring resets
924
- // behavior state, killing the gesture.
925
- if (interactionState is InteractionState.Dragging) return
1071
+ // behavior state, killing the gesture. Applied on settle so a real content
1072
+ // change mid-drag (e.g. items removed from a list) isn't lost.
1073
+ if (interactionState is InteractionState.Dragging) {
1074
+ hasPendingSizeChange = true
1075
+ return
1076
+ }
1077
+ hasPendingSizeChange = false
926
1078
 
927
1079
  setupSheetDetents()
928
1080
  positionFooter()
@@ -978,7 +1130,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
978
1130
  if (contentHeight == 0) return
979
1131
 
980
1132
  // While keyboard is active or transitioning, use the target detent position for dim
981
- val top = if (keyboardInset > 0 || isKeyboardTransitioning) {
1133
+ val top = if (isKeyboardActive) {
982
1134
  detentCalculator.getSheetTopForDetentIndex(currentDetentIndex)
983
1135
  } else {
984
1136
  val keyboardOffset = if (isBeingDismissed) 0 else currentKeyboardInset
@@ -1003,13 +1155,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1003
1155
 
1004
1156
  var footerKeyboardOffset: Int = 0
1005
1157
 
1006
- fun positionFooter(slideOffset: Float? = null) {
1158
+ fun positionFooter() {
1007
1159
  if (!isPresented) return
1008
1160
  val footerView = containerView?.footerView ?: return
1009
1161
  val sheet = sheetView ?: return
1010
1162
 
1011
- val keyboardShift = if (currentKeyboardInset > 0) maxOf(0, currentKeyboardInset + footerKeyboardOffset) else 0
1012
-
1013
1163
  // A relative footer is laid out by Yoga (pinned to the container's bottom
1014
1164
  // edge) and stays in the layout flow behind the keyboard — only an
1015
1165
  // absolute footer floats above it
@@ -1018,20 +1168,23 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1018
1168
  return
1019
1169
  }
1020
1170
 
1171
+ // The footer's absorbed safe-area inset rides below the keyboard's top
1172
+ // edge — its content stays flush with the keyboard without dropping the
1173
+ // inset padding from the layout, which would jump at the transition edges
1174
+ val keyboardShift = if (currentKeyboardInset > 0) {
1175
+ maxOf(0, currentKeyboardInset + footerKeyboardOffset - footerView.bottomInset)
1176
+ } else {
1177
+ 0
1178
+ }
1179
+
1021
1180
  val footerHeight = footerView.height
1022
1181
  val sheetHeight = sheet.height
1023
- val sheetTop = sheet.top
1024
1182
 
1025
- var footerY = (sheetHeight - sheetTop - footerHeight - keyboardShift).toFloat()
1026
-
1027
- // Adjust during dismiss animation when slideOffset is negative. Skipped
1028
- // while the keyboard is open — the detents collapse to the expanded
1029
- // position, so any downward drag reads as a negative slideOffset and the
1030
- // adjustment would sink the footer behind the keyboard, snapping back up
1031
- // when the keyboard detents reset on release.
1032
- if (slideOffset != null && slideOffset < 0 && keyboardShift == 0) {
1033
- footerY -= (footerHeight * slideOffset)
1034
- }
1183
+ // Pinned to the screen bottom (above the keyboard) while the sheet sits at
1184
+ // or above its lowest stop; past it the footer rides down with the sheet
1185
+ // instead, like iOS
1186
+ val anchorTop = minOf(sheet.top, detentCalculator.getLowestSheetTop())
1187
+ val footerY = (sheetHeight - anchorTop - footerHeight - keyboardShift).toFloat()
1035
1188
 
1036
1189
  // Clamp to prevent footer going above safe area
1037
1190
  val maxAllowedY = (sheetHeight - topInset - footerHeight).toFloat()
@@ -1076,9 +1229,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1076
1229
  delegate = object : TrueSheetKeyboardObserverDelegate {
1077
1230
  override fun keyboardWillShow(height: Int) {
1078
1231
  if (!shouldHandleKeyboard()) return
1079
- // An absolute footer rises above the keyboard, so it drops the bottom
1080
- // inset padding — before detents are configured against its height
1081
- if (absoluteFooter) containerView?.footerView?.keyboardVisible = true
1082
1232
  // If a resize is in flight, restore to its target — not the stale current
1083
1233
  detentIndexBeforeKeyboard = if (pendingDetentIndex >= 0) pendingDetentIndex else currentDetentIndex
1084
1234
  pendingDetentIndex = -1
@@ -1091,7 +1241,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1091
1241
 
1092
1242
  override fun keyboardWillHide() {
1093
1243
  if (!shouldHandleKeyboard(checkFocus = false)) return
1094
- containerView?.footerView?.keyboardVisible = false
1244
+ // Mid-drag (the drag itself dismissed the keyboard) the release decides
1245
+ // the detent and a reconfigure would kill the gesture — the floor stops
1246
+ // stay put until the settle flushes the pending reconfigure
1247
+ if (interactionState is InteractionState.Dragging) {
1248
+ hasPendingSizeChange = true
1249
+ return
1250
+ }
1095
1251
  val restoring = !isBeingDismissed && detentIndexBeforeKeyboard >= 0
1096
1252
 
1097
1253
  // Skip reconfigure during interactive keyboard dismiss (e.g. keyboardDismissMode="on-drag")
@@ -1116,9 +1272,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1116
1272
 
1117
1273
  override fun keyboardDidHide() {
1118
1274
  if (!shouldHandleKeyboard(checkFocus = false)) return
1119
- containerView?.footerView?.keyboardVisible = false
1120
- detentIndexBeforeKeyboard = -1
1121
1275
  isKeyboardDismissProgrammatic = false
1276
+ // Keep detentIndexBeforeKeyboard for the settle's detent-change check
1277
+ if (interactionState is InteractionState.Dragging) {
1278
+ hasPendingSizeChange = true
1279
+ return
1280
+ }
1281
+ detentIndexBeforeKeyboard = -1
1122
1282
  setupSheetDetents(applyState = false)
1123
1283
  positionFooter()
1124
1284
  updateDimAmount(
@@ -1138,7 +1298,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1138
1298
  // Handle case where keyboard is already visible and focus moves into the sheet
1139
1299
  if (!shouldHandleKeyboard()) return
1140
1300
  if (detentIndexBeforeKeyboard < 0 && (keyboardObserver?.currentHeight ?: 0) > 0) {
1141
- if (absoluteFooter) containerView?.footerView?.keyboardVisible = true
1142
1301
  detentIndexBeforeKeyboard = currentDetentIndex
1143
1302
  currentDetentIndex = detents.size - 1
1144
1303
  setupSheetDetents()
@@ -1166,8 +1325,28 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1166
1325
  detentIndexBeforeKeyboard = -1
1167
1326
  }
1168
1327
 
1328
+ private fun handleSettling(sheetView: View) {
1329
+ // A keyboard-floor drag was released — the first settle frame reveals the target
1330
+ keyboardSettleStartTop = if (hasKeyboardFloor && interactionState is InteractionState.Dragging) sheetView.top else -1
1331
+ }
1332
+
1333
+ /**
1334
+ * Dismisses the keyboard as soon as a released drag is known to land on a
1335
+ * keyboard-free stop, rather than waiting for the landing. BottomSheetBehavior
1336
+ * keeps its target private, so read it off the first settle frame: heading
1337
+ * down means the half stop or the floor; heading up from below the half stop
1338
+ * means the half stop; only heading up from above it means the expanded stop.
1339
+ */
1340
+ private fun dismissKeyboardForSettleTarget(sheetView: View) {
1341
+ if (keyboardSettleStartTop < 0) return
1342
+ val startTop = keyboardSettleStartTop
1343
+ keyboardSettleStartTop = -1
1344
+
1345
+ val headingToKeyboardFreeStop = sheetView.top > startTop || startTop > detentCalculator.getKeyboardHalfStopTop()
1346
+ if (headingToKeyboardFreeStop && shouldHandleKeyboard()) dismissKeyboard()
1347
+ }
1348
+
1169
1349
  private fun handleDragBegin(sheetView: View) {
1170
- detentIndexBeforeKeyboard = -1
1171
1350
  pendingDetentIndex = -1
1172
1351
 
1173
1352
  val position = getPositionDpForView(sheetView)
@@ -1176,36 +1355,20 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1176
1355
  interactionState = InteractionState.Dragging(startTop = sheetView.top)
1177
1356
  }
1178
1357
 
1179
- private fun handleSettling(sheetView: View) {
1180
- if (interactionState !is InteractionState.Dragging) return
1181
- if (keyboardInset <= 0) return
1358
+ // Commits a non-keyboard detent and hides the keyboard; keyboardWillHide then
1359
+ // reconfigures the detents so the sheet settles at the committed index.
1360
+ private fun commitDetentAndDismissKeyboard(index: Int, sheetView: View) {
1361
+ val changed = index != knownDetentIndex
1362
+ detentIndexBeforeKeyboard = -1
1363
+ currentDetentIndex = index
1364
+ dismissKeyboard()
1365
+ setupDimmedBackground()
1182
1366
 
1183
- // After drag release, check if the sheet was dragged past the midpoint between the
1184
- // keyboard-expanded position and a non-keyboard detent position. If so, dismiss
1185
- // keyboard and commit to that detent.
1186
- val maxAvailableHeight = realScreenHeight - topInset
1187
- val keyboardTop = detentCalculator.getSheetTopForDetentIndex(detents.size - 1)
1188
-
1189
- for (i in detents.indices) {
1190
- val nonKeyboardHeight = minOf(detentCalculator.getDetentHeight(detents[i], includeKeyboard = false), maxAvailableHeight)
1191
- val nonKeyboardTop = realScreenHeight - nonKeyboardHeight
1192
- val midpoint = keyboardTop + (nonKeyboardTop - keyboardTop) / 2
1193
-
1194
- if (sheetView.top >= midpoint) {
1195
- // Target position matches the keyboard-expanded position — keep keyboard open
1196
- if (nonKeyboardTop == keyboardTop) break
1197
-
1198
- detentIndexBeforeKeyboard = -1
1199
- currentDetentIndex = i
1200
- dismissKeyboard()
1201
- setupDimmedBackground()
1202
-
1203
- val position = getPositionDpForView(sheetView)
1204
- val detent = detentCalculator.getDetentValueForIndex(i)
1205
- delegate?.viewControllerDidChangeDetent(i, position, detent)
1206
- break
1207
- }
1208
- }
1367
+ if (!changed) return
1368
+ val position = getPositionDpForView(sheetView)
1369
+ val detent = detentCalculator.getDetentValueForIndex(index)
1370
+ delegate?.viewControllerDidChangeDetent(index, position, detent)
1371
+ this.sheetView?.updateGrabberAccessibilityValue(index, detents.size)
1209
1372
  }
1210
1373
 
1211
1374
  private fun handleDragChange(sheetView: View) {
@@ -1253,8 +1416,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1253
1416
  lastEmittedPositionPx = currentTop
1254
1417
  val visibleHeight = realScreenHeight - currentTop
1255
1418
  val position = detentCalculator.getPositionDp(visibleHeight)
1256
- val interpolatedIndex = detentCalculator.getInterpolatedIndexForPosition(currentTop)
1257
- val detent = detentCalculator.getInterpolatedDetentForPosition(currentTop)
1419
+ val (interpolatedIndex, detent) = detentCalculator.getInterpolatedPositionInfo(currentTop)
1258
1420
  delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
1259
1421
  }
1260
1422
 
@@ -1262,9 +1424,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1262
1424
  * Updates position emission, footer, and dim amount together.
1263
1425
  * This pattern is commonly used during animations and state changes.
1264
1426
  */
1265
- private fun updateSheetVisuals(effectiveTop: Int, slideOffset: Float? = null) {
1427
+ private fun updateSheetVisuals(effectiveTop: Int) {
1266
1428
  emitChangePositionDelegate(effectiveTop)
1267
- positionFooter(slideOffset)
1429
+ positionFooter()
1268
1430
  updateDimAmount(effectiveTop)
1269
1431
  }
1270
1432
 
@@ -1313,7 +1475,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1313
1475
  if (detents.isEmpty()) return
1314
1476
 
1315
1477
  val maxAvailableHeight = realScreenHeight - topInset
1316
- val minHeight = minOf(detentCalculator.getDetentHeight(detents.first()), maxAvailableHeight)
1478
+ val minHeight = minOf(
1479
+ detentCalculator.getDetentHeight(detents.first(), includeKeyboard = !hasKeyboardFloor),
1480
+ maxAvailableHeight
1481
+ )
1317
1482
  val newHeight = detentCalculator.getVisibleSheetHeight(sheetTop).coerceIn(minHeight, maxAvailableHeight)
1318
1483
 
1319
1484
  setStateDimensions(getStateWidth(), newHeight)
@@ -1333,9 +1498,27 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1333
1498
  delegate?.viewControllerDidChangeSize(width, height)
1334
1499
  }
1335
1500
 
1336
- fun translateSheet(translationY: Int, onEnd: (() -> Unit)? = null) {
1501
+ /**
1502
+ * Realtime (non-animated) counterpart of TrueSheetStackManager.updateParentTranslation,
1503
+ * driven by onSlide so the parent tracks this sheet's actual position.
1504
+ */
1505
+ private fun syncParentTranslation(sheetTop: Int) {
1506
+ val parent = parentSheetView ?: return
1507
+ val minSheetTop = detentCalculator.getSheetTopForDetentIndex(0)
1508
+ parent.syncTranslationForChild(sheetTop, minSheetTop)
1509
+ }
1510
+
1511
+ fun translateSheet(translationY: Int, animated: Boolean = true, onEnd: (() -> Unit)? = null) {
1337
1512
  val sheet = sheetView ?: return
1338
1513
 
1514
+ if (!animated) {
1515
+ sheet.animate().cancel()
1516
+ sheet.translationY = translationY.toFloat()
1517
+ emitChangePositionDelegate(sheet.top + translationY)
1518
+ onEnd?.invoke()
1519
+ return
1520
+ }
1521
+
1339
1522
  sheet.animate()
1340
1523
  .translationY(translationY.toFloat())
1341
1524
  .setDuration(TRANSLATE_ANIMATION_DURATION)
@@ -1419,7 +1602,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1419
1602
 
1420
1603
  override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
1421
1604
  eventDispatcher?.let {
1422
- jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
1605
+ if (touchDeduper.shouldDispatch(event)) {
1606
+ jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
1607
+ }
1423
1608
  jsPointerDispatcher.handleMotionEvent(event, it, true)
1424
1609
  }
1425
1610
  return super.onInterceptTouchEvent(event)
@@ -1433,7 +1618,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1433
1618
 
1434
1619
  override fun onTouchEvent(event: MotionEvent): Boolean {
1435
1620
  eventDispatcher?.let {
1436
- jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
1621
+ if (touchDeduper.shouldDispatch(event)) {
1622
+ jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
1623
+ }
1437
1624
  jsPointerDispatcher.handleMotionEvent(event, it, false)
1438
1625
  }
1439
1626
  super.onTouchEvent(event)