@lodev09/react-native-true-sheet 4.0.0-beta.14 → 4.0.0-beta.15

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 (54) hide show
  1. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +28 -6
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +99 -7
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +12 -0
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +18 -7
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +40 -39
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +18 -8
  7. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +14 -8
  8. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +8 -1
  9. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +32 -5
  10. package/ios/TrueSheetContainerView.h +17 -4
  11. package/ios/TrueSheetContainerView.mm +23 -6
  12. package/ios/TrueSheetContentView.h +13 -1
  13. package/ios/TrueSheetContentView.mm +93 -35
  14. package/ios/TrueSheetFooterView.mm +19 -5
  15. package/ios/TrueSheetView.mm +29 -28
  16. package/ios/TrueSheetViewController.h +21 -1
  17. package/ios/TrueSheetViewController.mm +54 -16
  18. package/ios/core/TrueSheetDetentCalculator.h +6 -1
  19. package/ios/core/TrueSheetDetentCalculator.mm +9 -4
  20. package/ios/core/TrueSheetKeyboardObserver.mm +2 -0
  21. package/lib/module/TrueSheet.js +8 -5
  22. package/lib/module/TrueSheet.js.map +1 -1
  23. package/lib/module/TrueSheet.web.js +135 -32
  24. package/lib/module/TrueSheet.web.js.map +1 -1
  25. package/lib/module/TrueSheetPeek.web.js +20 -15
  26. package/lib/module/TrueSheetPeek.web.js.map +1 -1
  27. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +5 -3
  28. package/lib/module/navigation/expo-router/index.js +5 -4
  29. package/lib/module/navigation/expo-router/index.js.map +1 -1
  30. package/lib/module/web/constants.js +1 -1
  31. package/lib/module/web/constants.js.map +1 -1
  32. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  33. package/lib/typescript/src/TrueSheet.types.d.ts +64 -25
  34. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  35. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  36. package/lib/typescript/src/TrueSheetPeek.web.d.ts +8 -4
  37. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
  38. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +5 -3
  39. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  40. package/lib/typescript/src/navigation/expo-router/index.d.ts +5 -3
  41. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
  42. package/lib/typescript/src/navigation/types.d.ts +1 -1
  43. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  44. package/lib/typescript/src/web/constants.d.ts +1 -1
  45. package/lib/typescript/src/web/constants.d.ts.map +1 -1
  46. package/package.json +7 -6
  47. package/src/TrueSheet.tsx +7 -4
  48. package/src/TrueSheet.types.ts +67 -25
  49. package/src/TrueSheet.web.tsx +154 -30
  50. package/src/TrueSheetPeek.web.tsx +25 -23
  51. package/src/fabric/TrueSheetViewNativeComponent.ts +5 -3
  52. package/src/navigation/expo-router/index.ts +8 -11
  53. package/src/navigation/types.ts +3 -2
  54. package/src/web/constants.ts +1 -1
@@ -5,7 +5,6 @@ import android.view.View
5
5
  import android.view.ViewGroup
6
6
  import com.facebook.react.uimanager.ThemedReactContext
7
7
  import com.facebook.react.uimanager.events.EventDispatcher
8
- import com.facebook.react.util.RNLog
9
8
  import com.facebook.react.views.view.ReactViewGroup
10
9
 
11
10
  interface TrueSheetContainerViewDelegate {
@@ -15,6 +14,7 @@ interface TrueSheetContainerViewDelegate {
15
14
  fun containerViewScrollViewDidChange()
16
15
  fun containerViewHeaderDidChangeSize(width: Int, height: Int)
17
16
  fun containerViewFooterDidChangeSize(width: Int, height: Int)
17
+ fun containerViewFooterDidLayout()
18
18
  fun containerViewPeekDidChangeSize(width: Int, height: Int)
19
19
  }
20
20
 
@@ -62,6 +62,18 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
62
62
  }
63
63
 
64
64
  var absoluteFooter: Boolean = false
65
+ set(value) {
66
+ if (field == value) return
67
+ field = value
68
+ contentView?.updateContentInset()
69
+ }
70
+
71
+ override var footerAvoidsKeyboard: Boolean = true
72
+ set(value) {
73
+ if (field == value) return
74
+ field = value
75
+ contentView?.updateContentInset()
76
+ }
65
77
 
66
78
  /**
67
79
  * Bottom safe-area inset the footer absorbs as padding — the footer
@@ -189,10 +201,9 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
189
201
  fun attachPeekView(view: TrueSheetPeekView) {
190
202
  if (peekView === view) return
191
203
 
192
- if (peekView != null) {
193
- RNLog.w(context as ThemedReactContext, "TrueSheet: Sheet can only have one peek component.")
194
- return
195
- }
204
+ // Last-attached wins. During a screen transition the incoming peek mounts
205
+ // before the outgoing one detaches, and the outgoing detach is a no-op.
206
+ peekView?.delegate = null
196
207
 
197
208
  peekView = view
198
209
  view.delegate = this
@@ -217,6 +228,12 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
217
228
  override val footerKeyboardOcclusion: Int
218
229
  get() = if (absoluteFooter) footerView?.keyboardOcclusionHeight ?: 0 else -(footerView?.height ?: 0)
219
230
 
231
+ override val absoluteFooterHeight: Int
232
+ get() = if (absoluteFooter) footerView?.height ?: 0 else 0
233
+
234
+ override val absoluteFooterTop: Int
235
+ get() = footerView?.y?.toInt() ?: height
236
+
220
237
  override val hasRelativeFooter: Boolean
221
238
  get() = footerView != null && !absoluteFooter
222
239
 
@@ -236,10 +253,15 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
236
253
  override fun footerViewDidChangeSize(width: Int, height: Int) {
237
254
  footerHeight = height
238
255
  delegate?.containerViewFooterDidChangeSize(width, height)
239
- // A relative footer takes over the bottom inset — drop the scrollable's
256
+ // A relative footer takes over the bottom inset — drop the scrollable's;
257
+ // an absolute one may pad the scrollable by its height
240
258
  contentView?.updateContentInset()
241
259
  }
242
260
 
261
+ override fun footerViewDidLayout() {
262
+ delegate?.containerViewFooterDidLayout()
263
+ }
264
+
243
265
  override fun peekViewDidChangeSize(width: Int, height: Int) {
244
266
  delegate?.containerViewPeekDidChangeSize(width, height)
245
267
  }
@@ -18,11 +18,32 @@ import com.lodev09.truesheet.utils.ScreenUtils
18
18
  import com.lodev09.truesheet.utils.isDescendantOf
19
19
  import com.lodev09.truesheet.utils.smoothScrollTo
20
20
 
21
+ /**
22
+ * What the sheet insets the bottom of the scroll content for — the safe area
23
+ * while it can scroll, and/or an absolute footer floating over it.
24
+ */
25
+ enum class TrueSheetContentInsetAdjustment(val safeArea: Boolean, val footer: Boolean) {
26
+ AUTOMATIC(safeArea = true, footer = true),
27
+ SAFE_AREA(safeArea = true, footer = false),
28
+ FOOTER(safeArea = false, footer = true),
29
+ NEVER(safeArea = false, footer = false);
30
+
31
+ companion object {
32
+ fun fromString(value: String?): TrueSheetContentInsetAdjustment =
33
+ when (value) {
34
+ "safe-area" -> SAFE_AREA
35
+ "footer" -> FOOTER
36
+ "never" -> NEVER
37
+ else -> AUTOMATIC
38
+ }
39
+ }
40
+ }
41
+
21
42
  data class ScrollableOptions(
22
43
  val keyboardScrollOffset: Float = 0f,
23
44
  val keyboardOffset: Float = 0f,
24
45
  val scrollingExpandsSheet: Boolean = true,
25
- val contentInsetAdjustment: Boolean = true
46
+ val contentInsetAdjustment: TrueSheetContentInsetAdjustment = TrueSheetContentInsetAdjustment.AUTOMATIC
26
47
  )
27
48
 
28
49
  /**
@@ -41,6 +62,21 @@ interface TrueSheetContentViewDelegate {
41
62
  */
42
63
  val footerKeyboardOcclusion: Int
43
64
 
65
+ /**
66
+ * Layout height of an absolute footer floating over the content, including
67
+ * its absorbed safe-area inset — 0 for a relative footer.
68
+ */
69
+ val absoluteFooterHeight: Int
70
+
71
+ // Includes the controller's translation during sheet transitions.
72
+ val absoluteFooterTop: Int
73
+
74
+ /**
75
+ * false when an absolute footer stays behind the keyboard instead of rising
76
+ * above it.
77
+ */
78
+ val footerAvoidsKeyboard: Boolean
79
+
44
80
  /**
45
81
  * A relative footer sits below the content and absorbs the bottom safe-area
46
82
  * inset — the scrollable doesn't reach the sheet's bottom edge, so no
@@ -122,6 +158,13 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
122
158
  private var baseBottomInset = 0
123
159
  private var keyboardBottomInset = 0
124
160
 
161
+ /**
162
+ * Height of the absolute footer the scroll content is padded by (see
163
+ * ScrollableOptions.contentInsetAdjustment), applied on top of the
164
+ * safe-area or keyboard inset.
165
+ */
166
+ private var footerBottomInset = 0
167
+
125
168
  private val scrollableLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
126
169
  updateContentInset()
127
170
  }
@@ -203,12 +246,12 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
203
246
  /**
204
247
  * Re-applies the base content inset — the bottom safe-area inset while the
205
248
  * content can scroll, no relative footer absorbs it, and
206
- * `contentInsetAdjustmentBehavior` isn't disabled.
249
+ * `contentInsetAdjustment` includes the safe area.
207
250
  */
208
251
  fun updateContentInset() {
209
252
  val scrollView = detectedScrollView ?: return
210
253
 
211
- val enabled = scrollableOptions?.contentInsetAdjustment ?: true
254
+ val enabled = scrollableOptions?.contentInsetAdjustment?.safeArea ?: true
212
255
  baseBottomInset = if (enabled && delegate?.hasRelativeFooter != true && canScrollContent(scrollView)) {
213
256
  bottomInset
214
257
  } else {
@@ -226,7 +269,44 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
226
269
 
227
270
  // The keyboard covers the safe-area region, so the larger inset wins over a sum
228
271
  private fun applyBottomInset() {
229
- setScrollContentInset(maxOf(baseBottomInset, keyboardBottomInset))
272
+ val safeAreaInset = maxOf(baseBottomInset, keyboardBottomInset)
273
+ footerBottomInset = footerInsetAbove(safeAreaInset)
274
+ setScrollContentInset(safeAreaInset + footerBottomInset)
275
+ }
276
+
277
+ // A pinned footer uses its positioned top so overlap follows the sheet's
278
+ // animation, even before the container reaches its final height. A rising
279
+ // footer uses its layout top because the keyboard inset carries its rise.
280
+ // The safe-area inset baked into the footer is already
281
+ // covered by the safe-area or keyboard inset when one applies — only the
282
+ // footer's occlusion above it stacks on top. A pinned footer adds only the
283
+ // overlap not already covered by the safe-area or keyboard inset.
284
+ private fun footerInsetAbove(safeAreaInset: Int): Int {
285
+ if (scrollableOptions?.contentInsetAdjustment?.footer == false) return 0
286
+ val scrollView = detectedScrollView ?: return 0
287
+ val container = parent as? View ?: return 0
288
+ val footerHeight = delegate?.absoluteFooterHeight ?: 0
289
+ if (footerHeight <= 0) return 0
290
+
291
+ var scrollBottom = scrollView.bottom
292
+ var view: View = scrollView.parent as? View ?: return 0
293
+ while (view !== container) {
294
+ scrollBottom += view.top
295
+ view = view.parent as? View ?: return 0
296
+ }
297
+
298
+ val footerTop = if (delegate?.footerAvoidsKeyboard == false) {
299
+ delegate?.absoluteFooterTop ?: return 0
300
+ } else {
301
+ container.height - footerHeight
302
+ }
303
+ var inset = scrollBottom - footerTop
304
+ if (delegate?.footerAvoidsKeyboard == false) {
305
+ inset -= safeAreaInset
306
+ } else if (safeAreaInset > 0) {
307
+ inset -= footerHeight - maxOf(0, delegate?.footerKeyboardOcclusion ?: 0)
308
+ }
309
+ return maxOf(0, inset)
230
310
  }
231
311
 
232
312
  private fun setScrollContentInset(inset: Int) {
@@ -247,6 +327,7 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
247
327
  detectedScrollView = null
248
328
  baseBottomInset = 0
249
329
  keyboardBottomInset = 0
330
+ footerBottomInset = 0
250
331
  appliedKeyboardOffset = 0
251
332
  }
252
333
 
@@ -344,6 +425,10 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
344
425
  val baseInset = maxOf(0, location[1] + scrollView.height - keyboardTop)
345
426
  val adjustedInset = maxOf(0, baseInset + keyboardOffset.toInt())
346
427
  appliedKeyboardOffset = adjustedInset - baseInset
428
+ if (delegate?.footerAvoidsKeyboard == false) {
429
+ appliedKeyboardOffset += footerInsetAbove(maxOf(baseBottomInset, adjustedInset)) -
430
+ footerInsetAbove(maxOf(baseBottomInset, baseInset))
431
+ }
347
432
  adjustedInset
348
433
  } else {
349
434
  appliedKeyboardOffset = 0
@@ -394,11 +479,18 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
394
479
  }
395
480
 
396
481
  // An absolute footer floats over the viewport's bottom edge — extend the
397
- // caret target so it clears the footer, not just the keyboard.
398
- val footerOcclusion = maxOf(0, delegate?.footerKeyboardOcclusion ?: 0)
482
+ // caret target so it clears the footer, not just the keyboard. Not needed
483
+ // while the footer inset is applied (the visible range already ends above
484
+ // it) or when the footer stays behind the keyboard.
485
+ val footerOcclusion = if (footerBottomInset > 0 || delegate?.footerAvoidsKeyboard == false) {
486
+ 0
487
+ } else {
488
+ maxOf(0, delegate?.footerKeyboardOcclusion ?: 0)
489
+ }
399
490
 
400
491
  val offset = keyboardScrollOffset.toInt()
401
- val visibleHeight = scrollView.height - scrollView.paddingBottom - maxOf(baseBottomInset, keyboardBottomInset) + appliedKeyboardOffset
492
+ val appliedInset = maxOf(baseBottomInset, keyboardBottomInset) + footerBottomInset
493
+ val visibleHeight = scrollView.height - scrollView.paddingBottom - appliedInset + appliedKeyboardOffset
402
494
  val visibleTop = scrollView.scrollY
403
495
  val visibleBottom = scrollView.scrollY + visibleHeight
404
496
 
@@ -22,6 +22,7 @@ import com.lodev09.truesheet.utils.TouchEventDeduper
22
22
  */
23
23
  interface TrueSheetFooterViewDelegate {
24
24
  fun footerViewDidChangeSize(width: Int, height: Int)
25
+ fun footerViewDidLayout()
25
26
  val eventDispatcher: EventDispatcher?
26
27
  }
27
28
 
@@ -97,6 +98,17 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
97
98
  }
98
99
  }
99
100
 
101
+ override fun onLayout(
102
+ changed: Boolean,
103
+ left: Int,
104
+ top: Int,
105
+ right: Int,
106
+ bottom: Int
107
+ ) {
108
+ super.onLayout(changed, left, top, right, bottom)
109
+ delegate?.footerViewDidLayout()
110
+ }
111
+
100
112
  // ==================== RootView Implementation ====================
101
113
 
102
114
  override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
@@ -224,15 +224,15 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
224
224
  viewController.maxContentWidth = width
225
225
  }
226
226
 
227
- fun setAnchor(anchor: String?) {
228
- val value = TrueSheetAnchor.fromString(anchor)
229
- if (viewController.anchor == value) return
230
- viewController.anchor = value
227
+ fun setPlacement(placement: String?) {
228
+ val value = TrueSheetPlacement.fromString(placement)
229
+ if (viewController.placement == value) return
230
+ viewController.placement = value
231
231
  }
232
232
 
233
- fun setAnchorOffset(offset: Int) {
234
- if (viewController.anchorOffset == offset) return
235
- viewController.anchorOffset = offset
233
+ fun setPlacementOffset(offset: Int) {
234
+ if (viewController.placementOffset == offset) return
235
+ viewController.placementOffset = offset
236
236
  }
237
237
 
238
238
  fun setDimmed(dimmed: Boolean) {
@@ -323,6 +323,12 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
323
323
  viewController.positionFooter()
324
324
  }
325
325
 
326
+ fun setFooterAvoidsKeyboard(avoids: Boolean) {
327
+ viewController.footerAvoidsKeyboard = avoids
328
+ viewController.containerView?.footerAvoidsKeyboard = avoids
329
+ viewController.positionFooter()
330
+ }
331
+
326
332
  private fun setupScrollable() {
327
333
  viewController.containerView?.let {
328
334
  it.scrollableOptions = viewController.scrollableOptions
@@ -330,6 +336,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
330
336
  it.scrollableBottomInset = viewController.contentBottomInset
331
337
  it.footerBottomInset = viewController.contentBottomInset
332
338
  it.absoluteFooter = viewController.absoluteFooter
339
+ it.footerAvoidsKeyboard = viewController.footerAvoidsKeyboard
333
340
  it.setupScrollable()
334
341
  }
335
342
 
@@ -697,6 +704,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
697
704
  viewController.positionFooter()
698
705
  }
699
706
 
707
+ override fun containerViewFooterDidLayout() {
708
+ viewController.positionFooter()
709
+ }
710
+
700
711
  override fun containerViewPeekDidChangeSize(width: Int, height: Int) {
701
712
  // Peek content height affects peek detents
702
713
  updateSheetIfNeeded()
@@ -80,17 +80,19 @@ interface TrueSheetViewControllerDelegate {
80
80
  * enabling touch pass-through to underlying views. Handles detent configuration, drag interactions,
81
81
  * keyboard avoidance, dimmed backgrounds, back button, and lifecycle events for stacked sheets.
82
82
  */
83
- enum class TrueSheetAnchor {
84
- LEFT,
83
+ enum class TrueSheetPlacement {
84
+ AUTOMATIC,
85
+ LEADING,
85
86
  CENTER,
86
- RIGHT;
87
+ TRAILING;
87
88
 
88
89
  companion object {
89
- fun fromString(value: String?): TrueSheetAnchor =
90
+ fun fromString(value: String?): TrueSheetPlacement =
90
91
  when (value) {
91
- "left" -> LEFT
92
- "right" -> RIGHT
93
- else -> CENTER
92
+ "leading" -> LEADING
93
+ "center" -> CENTER
94
+ "trailing" -> TRAILING
95
+ else -> AUTOMATIC
94
96
  }
95
97
  }
96
98
  }
@@ -120,7 +122,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
120
122
  companion object {
121
123
  private const val DEFAULT_MAX_WIDTH = 640 // dp
122
124
  private const val DEFAULT_CORNER_RADIUS = 16 // dp
123
- private const val DEFAULT_ANCHOR_OFFSET = 16 // dp
125
+ private const val DEFAULT_PLACEMENT_OFFSET = 16 // dp
124
126
  private const val TRANSLATE_ANIMATION_DURATION = 200L
125
127
  private const val DISMISS_DURATION = 200L
126
128
  private const val SCREEN_FADE_DURATION = 150L
@@ -221,8 +223,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
221
223
  // Detent Configuration
222
224
  override var maxContentHeight: Int? = null
223
225
  override var maxContentWidth: Int? = null
224
- override var anchor: TrueSheetAnchor = TrueSheetAnchor.CENTER
225
- override var anchorOffset: Int = DEFAULT_ANCHOR_OFFSET.dpToPx().toInt()
226
+ override var placement: TrueSheetPlacement = TrueSheetPlacement.AUTOMATIC
227
+ override var placementOffset: Int = DEFAULT_PLACEMENT_OFFSET.dpToPx().toInt()
226
228
  override var detents: MutableList<Double> = mutableListOf(0.5, 1.0)
227
229
 
228
230
  // Appearance Configuration
@@ -330,6 +332,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
330
332
 
331
333
  override var absoluteFooter: Boolean = false
332
334
 
335
+ override val footerInsetAdjustment: Boolean
336
+ get() = absoluteFooter && scrollableHandle > 0 && scrollableOptions?.contentInsetAdjustment?.footer != false
337
+
333
338
  override val peekContentHeight: Int
334
339
  get() = containerView?.peekContentHeight ?: cachedPeekContentHeight
335
340
 
@@ -496,9 +501,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
496
501
  override fun coordinatorLayoutDidLayout(changed: Boolean) {
497
502
  if (changed) invalidateScreenMetrics()
498
503
 
499
- // Reposition footer when layout changes
500
504
  if (isPresented && changed) {
501
- positionFooter()
505
+ setupSheetDetentsForSizeChange()
502
506
  }
503
507
  }
504
508
 
@@ -581,19 +585,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
581
585
  delegate?.viewControllerDidAttemptDismiss()
582
586
  }
583
587
  } else {
584
- setStateForDetentIndex(nextIndex)
588
+ resize(nextIndex)
585
589
  }
586
590
  }
587
591
 
588
592
  override fun bottomSheetViewDidAccessibilityIncrement() {
589
593
  if (currentDetentIndex < detents.size - 1) {
590
- setStateForDetentIndex(currentDetentIndex + 1)
594
+ resize(currentDetentIndex + 1)
591
595
  }
592
596
  }
593
597
 
594
598
  override fun bottomSheetViewDidAccessibilityDecrement() {
595
599
  if (currentDetentIndex > 0) {
596
- setStateForDetentIndex(currentDetentIndex - 1)
600
+ resize(currentDetentIndex - 1)
597
601
  } else if (dismissible) {
598
602
  dismiss(animated = true)
599
603
  } else {
@@ -646,6 +650,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
646
650
 
647
651
  val behavior = behavior ?: return
648
652
 
653
+ if (interactionState is InteractionState.Dragging || pendingDetentIndex >= 0) {
654
+ updateStateDimensionsForPosition(sheetView.top)
655
+ }
656
+
649
657
  when (behavior.state) {
650
658
  BottomSheetBehavior.STATE_DRAGGING -> handleDragChange(sheetView)
651
659
 
@@ -752,7 +760,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
752
760
  // the detents (and the container) to it.
753
761
  commitDetentAndDismissKeyboard(index, sheetView)
754
762
  } else {
755
- // Settled — resize the container to the settled detent (applies deferred shrinks)
756
763
  updateStateDimensions()
757
764
  }
758
765
 
@@ -880,7 +887,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
880
887
  }
881
888
 
882
889
  pendingDetentIndex = detentIndex
883
- updateStateDimensions(deferShrink = true)
884
890
  setupDimmedBackground()
885
891
  setStateForDetentIndex(detentIndex)
886
892
  resizePromise?.invoke()
@@ -1066,11 +1072,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1066
1072
  }
1067
1073
 
1068
1074
  fun setupSheetDetentsForSizeChange() {
1069
- // Defer while dragging — the size change is driven by the drag itself (the
1070
- // container tracks the sheet's visible height), and reconfiguring resets
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) {
1075
+ // Reconfiguring while the container tracks the sheet can interrupt its
1076
+ // motion. Apply content size changes once the drag or resize settles.
1077
+ if (interactionState is InteractionState.Dragging || isSettling) {
1074
1078
  hasPendingSizeChange = true
1075
1079
  return
1076
1080
  }
@@ -1127,7 +1131,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1127
1131
 
1128
1132
  fun updateDimAmount(sheetTop: Int? = null, animated: Boolean = false) {
1129
1133
  if (!dimmed) return
1130
- if (contentHeight == 0) return
1131
1134
 
1132
1135
  // While keyboard is active or transitioning, use the target detent position for dim
1133
1136
  val top = if (isKeyboardActive) {
@@ -1155,6 +1158,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1155
1158
 
1156
1159
  var footerKeyboardOffset: Int = 0
1157
1160
 
1161
+ /**
1162
+ * false keeps an absolute footer pinned to the sheet's bottom edge behind
1163
+ * the keyboard instead of rising above it.
1164
+ */
1165
+ override var footerAvoidsKeyboard: Boolean = true
1166
+
1158
1167
  fun positionFooter() {
1159
1168
  if (!isPresented) return
1160
1169
  val footerView = containerView?.footerView ?: return
@@ -1171,7 +1180,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1171
1180
  // The footer's absorbed safe-area inset rides below the keyboard's top
1172
1181
  // edge — its content stays flush with the keyboard without dropping the
1173
1182
  // inset padding from the layout, which would jump at the transition edges
1174
- val keyboardShift = if (currentKeyboardInset > 0) {
1183
+ val keyboardShift = if (currentKeyboardInset > 0 && footerAvoidsKeyboard) {
1175
1184
  maxOf(0, currentKeyboardInset + footerKeyboardOffset - footerView.bottomInset)
1176
1185
  } else {
1177
1186
  0
@@ -1189,6 +1198,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1189
1198
  // Clamp to prevent footer going above safe area
1190
1199
  val maxAllowedY = (sheetHeight - topInset - footerHeight).toFloat()
1191
1200
  footerView.y = minOf(footerY, maxAllowedY)
1201
+ if (!footerAvoidsKeyboard) {
1202
+ containerView?.contentView?.updateContentInset()
1203
+ }
1192
1204
  }
1193
1205
 
1194
1206
  // =============================================================================
@@ -1253,11 +1265,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1253
1265
  // Skip reconfigure during interactive keyboard dismiss (e.g. keyboardDismissMode="on-drag")
1254
1266
  // to prevent the sheet from jumping. keyboardDidHide will reconfigure after.
1255
1267
  if (restoring || isKeyboardDismissProgrammatic) {
1256
- setupSheetDetents()
1257
1268
  if (restoring) {
1258
1269
  currentDetentIndex = detentIndexBeforeKeyboard
1259
- setStateForDetentIndex(currentDetentIndex)
1260
1270
  }
1271
+ setupSheetDetents()
1261
1272
  }
1262
1273
 
1263
1274
  // Same-frame reposition — the reconfigure above shrinks the container
@@ -1374,8 +1385,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1374
1385
  private fun handleDragChange(sheetView: View) {
1375
1386
  if (interactionState !is InteractionState.Dragging) return
1376
1387
 
1377
- updateStateDimensionsForDrag(sheetView.top)
1378
-
1379
1388
  val position = getPositionDpForView(sheetView)
1380
1389
  val detent = detentCalculator.getDetentValueForIndex(currentDetentIndex)
1381
1390
  delegate?.viewControllerDidDragChange(currentDetentIndex, position, detent)
@@ -1446,13 +1455,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1446
1455
  /**
1447
1456
  * Updates the Fabric state with the current/target detent size so Yoga sizes
1448
1457
  * the container to the sheet's visible height.
1449
- *
1450
- * Unlike iOS, resize animations run over multiple frames (ViewDragHelper), so
1451
- * shrinking is deferred to settle ([deferShrink]) when a resize animation is
1452
- * about to run — growing applies immediately so content is laid out before
1453
- * the sheet reveals it.
1454
1458
  */
1455
- private fun updateStateDimensions(deferShrink: Boolean = false) {
1459
+ private fun updateStateDimensions() {
1456
1460
  if (detents.isEmpty()) return
1457
1461
 
1458
1462
  val targetIndex = (if (pendingDetentIndex >= 0) pendingDetentIndex else currentDetentIndex)
@@ -1461,17 +1465,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1461
1465
  val newHeight = minOf(detentCalculator.getDetentHeight(detents[targetIndex]), maxAvailableHeight)
1462
1466
  val newWidth = getStateWidth()
1463
1467
 
1464
- if (deferShrink && newWidth == lastStateWidth && newHeight < lastStateHeight) return
1465
-
1466
1468
  setStateDimensions(newWidth, newHeight)
1467
1469
  }
1468
1470
 
1469
1471
  /**
1470
- * Tracks the sheet's visible height during drag (and the post-release settle
1471
- * animation) so the container resizes with the finger, like iOS. Clamped to the
1472
+ * Tracks the sheet's visible height during drags and resize animations. Clamped to the
1472
1473
  * smallest detent — dragging below it slides the sheet out without resizing.
1473
1474
  */
1474
- private fun updateStateDimensionsForDrag(sheetTop: Int) {
1475
+ private fun updateStateDimensionsForPosition(sheetTop: Int) {
1475
1476
  if (detents.isEmpty()) return
1476
1477
 
1477
1478
  val maxAvailableHeight = realScreenHeight - topInset
@@ -213,14 +213,14 @@ class TrueSheetViewManager :
213
213
  view.setMaxContentWidth(if (width > 0) width.dpToPx().toInt() else null)
214
214
  }
215
215
 
216
- @ReactProp(name = "anchor")
217
- override fun setAnchor(view: TrueSheetView, anchor: String?) {
218
- view.setAnchor(anchor)
216
+ @ReactProp(name = "placement")
217
+ override fun setPlacement(view: TrueSheetView, placement: String?) {
218
+ view.setPlacement(placement)
219
219
  }
220
220
 
221
- @ReactProp(name = "anchorOffset", defaultDouble = 0.0)
222
- override fun setAnchorOffset(view: TrueSheetView, offset: Double) {
223
- view.setAnchorOffset(if (offset > 0) offset.dpToPx().toInt() else 0)
221
+ @ReactProp(name = "placementOffset", defaultDouble = 0.0)
222
+ override fun setPlacementOffset(view: TrueSheetView, offset: Double) {
223
+ view.setPlacementOffset(if (offset > 0) offset.dpToPx().toInt() else 0)
224
224
  }
225
225
 
226
226
  @ReactProp(name = "backgroundBlur")
@@ -233,6 +233,11 @@ class TrueSheetViewManager :
233
233
  // iOS-specific prop - no-op on Android
234
234
  }
235
235
 
236
+ @ReactProp(name = "glass", defaultBoolean = true)
237
+ override fun setGlass(view: TrueSheetView, glass: Boolean) {
238
+ // iOS-specific prop - no-op on Android
239
+ }
240
+
236
241
  @ReactProp(name = "insetAdjustment")
237
242
  override fun setInsetAdjustment(view: TrueSheetView, insetAdjustment: String?) {
238
243
  view.setInsetAdjustment(insetAdjustment ?: "automatic")
@@ -264,8 +269,9 @@ class TrueSheetViewManager :
264
269
  keyboardScrollOffset = if (options.hasKey("keyboardScrollOffset")) options.getDouble("keyboardScrollOffset").toFloat() else 0f,
265
270
  keyboardOffset = if (options.hasKey("keyboardOffset")) options.getDouble("keyboardOffset").toFloat() else 0f,
266
271
  scrollingExpandsSheet = if (options.hasKey("scrollingExpandsSheet")) options.getBoolean("scrollingExpandsSheet") else true,
267
- contentInsetAdjustment = !options.hasKey("contentInsetAdjustmentBehavior") ||
268
- options.getBoolean("contentInsetAdjustmentBehavior")
272
+ contentInsetAdjustment = TrueSheetContentInsetAdjustment.fromString(
273
+ if (options.hasKey("contentInsetAdjustment")) options.getString("contentInsetAdjustment") else null
274
+ )
269
275
  )
270
276
  view.setScrollableOptions(scrollableOptions)
271
277
  }
@@ -284,6 +290,10 @@ class TrueSheetViewManager :
284
290
  val keyboardOffset =
285
291
  if (options != null && options.hasKey("keyboardOffset")) options.getDouble("keyboardOffset").toFloat() else 0f
286
292
  view.setFooterKeyboardOffset(keyboardOffset)
293
+
294
+ val avoidKeyboard =
295
+ if (options != null && options.hasKey("avoidKeyboard")) options.getBoolean("avoidKeyboard") else true
296
+ view.setFooterAvoidsKeyboard(avoidKeyboard)
287
297
  }
288
298
 
289
299
  companion object {
@@ -18,7 +18,7 @@ import androidx.core.view.ViewCompat
18
18
  import com.facebook.react.uimanager.PixelUtil.dpToPx
19
19
  import com.facebook.react.uimanager.ThemedReactContext
20
20
  import com.google.android.material.bottomsheet.BottomSheetBehavior
21
- import com.lodev09.truesheet.TrueSheetAnchor
21
+ import com.lodev09.truesheet.TrueSheetPlacement
22
22
  import com.lodev09.truesheet.utils.ScreenUtils
23
23
 
24
24
  interface TrueSheetBottomSheetViewDelegate {
@@ -27,8 +27,8 @@ interface TrueSheetBottomSheetViewDelegate {
27
27
  val sheetElevation: Float
28
28
  val sheetBackgroundColor: Int?
29
29
  val maxContentWidth: Int?
30
- val anchor: TrueSheetAnchor
31
- val anchorOffset: Int
30
+ val placement: TrueSheetPlacement
31
+ val placementOffset: Int
32
32
  val grabber: Boolean
33
33
  val grabberOptions: GrabberOptions?
34
34
  val accessibilityOptions: AccessibilityOptions?
@@ -94,13 +94,19 @@ class TrueSheetBottomSheetView(private val reactContext: ThemedReactContext) : F
94
94
  // =============================================================================
95
95
 
96
96
  private fun resolveAnchor(): Pair<Int, Int> {
97
- val anchor = if (ScreenUtils.isPortraitPhone(reactContext)) null else delegate?.anchor
98
- val gravity = when (anchor) {
99
- TrueSheetAnchor.LEFT -> Gravity.START
100
- TrueSheetAnchor.RIGHT -> Gravity.END
97
+ val placement = if (ScreenUtils.isPortraitPhone(reactContext)) null else delegate?.placement
98
+ val gravity = when (placement) {
99
+ TrueSheetPlacement.LEADING -> Gravity.START
100
+ TrueSheetPlacement.TRAILING -> Gravity.END
101
101
  else -> Gravity.CENTER_HORIZONTAL
102
102
  } or Gravity.BOTTOM
103
- val margin = if (anchor == TrueSheetAnchor.LEFT || anchor == TrueSheetAnchor.RIGHT) delegate?.anchorOffset ?: 0 else 0
103
+ val margin = if (placement == TrueSheetPlacement.LEADING ||
104
+ placement == TrueSheetPlacement.TRAILING
105
+ ) {
106
+ delegate?.placementOffset ?: 0
107
+ } else {
108
+ 0
109
+ }
104
110
  return Pair(gravity, margin)
105
111
  }
106
112