@lodev09/react-native-true-sheet 3.9.9 → 3.10.0-beta.0

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 (38) hide show
  1. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +1 -2
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +41 -14
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +10 -0
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +8 -4
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +49 -43
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +11 -2
  7. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +36 -0
  8. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +14 -3
  9. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +3 -3
  10. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +3 -1
  11. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +51 -0
  12. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +8 -5
  13. package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +17 -0
  14. package/ios/TrueSheetContainerView.h +8 -1
  15. package/ios/TrueSheetContainerView.mm +14 -7
  16. package/ios/TrueSheetModule.mm +5 -0
  17. package/ios/TrueSheetView.mm +25 -26
  18. package/ios/TrueSheetViewController.h +3 -1
  19. package/ios/TrueSheetViewController.mm +67 -18
  20. package/ios/core/TrueSheetGrabberView.h +20 -0
  21. package/ios/core/TrueSheetGrabberView.mm +58 -12
  22. package/lib/module/TrueSheet.js +27 -7
  23. package/lib/module/TrueSheet.js.map +1 -1
  24. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +2 -1
  25. package/lib/module/specs/NativeTrueSheetModule.js.map +1 -1
  26. package/lib/typescript/src/TrueSheet.d.ts +5 -1
  27. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  28. package/lib/typescript/src/TrueSheet.types.d.ts +9 -3
  29. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  30. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +4 -1
  31. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  32. package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts +6 -0
  33. package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts.map +1 -1
  34. package/package.json +2 -2
  35. package/src/TrueSheet.tsx +40 -8
  36. package/src/TrueSheet.types.ts +10 -3
  37. package/src/fabric/TrueSheetViewNativeComponent.ts +2 -1
  38. package/src/specs/NativeTrueSheetModule.ts +7 -0
@@ -2,7 +2,6 @@ package com.lodev09.truesheet
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.view.View
5
- import com.facebook.react.bridge.ReadableMap
6
5
  import com.facebook.react.uimanager.ThemedReactContext
7
6
  import com.facebook.react.uimanager.events.EventDispatcher
8
7
  import com.facebook.react.views.view.ReactViewGroup
@@ -40,7 +39,7 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
40
39
  var insetAdjustment: TrueSheetInsetAdjustment = TrueSheetInsetAdjustment.AUTOMATIC
41
40
  var scrollViewBottomInset: Int = 0
42
41
  var scrollableEnabled: Boolean = false
43
- var scrollableOptions: ReadableMap? = null
42
+ var scrollableOptions: ScrollableOptions? = null
44
43
  set(value) {
45
44
  field = value
46
45
  contentView?.scrollableOptions = value
@@ -4,13 +4,18 @@ import android.annotation.SuppressLint
4
4
  import android.view.View
5
5
  import android.view.ViewGroup
6
6
  import android.widget.ScrollView
7
- import com.facebook.react.bridge.ReadableMap
7
+ import androidx.core.widget.NestedScrollView
8
+ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
8
9
  import com.facebook.react.uimanager.PixelUtil.dpToPx
9
10
  import com.facebook.react.uimanager.ThemedReactContext
10
11
  import com.facebook.react.views.view.ReactViewGroup
11
12
  import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
12
13
  import com.lodev09.truesheet.core.TrueSheetKeyboardObserverDelegate
13
14
  import com.lodev09.truesheet.utils.isDescendantOf
15
+ import com.lodev09.truesheet.utils.smoothScrollBy
16
+ import com.lodev09.truesheet.utils.smoothScrollTo
17
+
18
+ data class ScrollableOptions(val keyboardScrollOffset: Float = 0f, val scrollingExpandsSheet: Boolean = true)
14
19
 
15
20
  /**
16
21
  * Delegate interface for content view size changes
@@ -32,17 +37,18 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
32
37
  private var lastWidth = 0
33
38
  private var lastHeight = 0
34
39
 
35
- private var pinnedScrollView: ScrollView? = null
40
+ private var pinnedScrollView: ViewGroup? = null
36
41
  private var originalScrollViewPaddingBottom: Int = 0
37
42
  private var bottomInset: Int = 0
43
+ private var scrollExpansionPadding: Int = 0
38
44
 
39
45
  private var keyboardScrollOffset: Float = 0f
40
46
  private var keyboardObserver: TrueSheetKeyboardObserver? = null
41
47
 
42
- var scrollableOptions: ReadableMap? = null
48
+ var scrollableOptions: ScrollableOptions? = null
43
49
  set(value) {
44
50
  field = value
45
- keyboardScrollOffset = value?.getDouble("keyboardScrollOffset")?.toFloat()?.dpToPx() ?: 0f
51
+ keyboardScrollOffset = value?.keyboardScrollOffset?.dpToPx() ?: 0f
46
52
  }
47
53
 
48
54
  override fun addView(child: View?, index: Int) {
@@ -94,6 +100,9 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
94
100
  originalScrollViewPaddingBottom = scrollView.paddingBottom
95
101
  pinnedScrollView = scrollView
96
102
 
103
+ scrollView.isNestedScrollingEnabled = true
104
+ (scrollView.parent as? SwipeRefreshLayout)?.isNestedScrollingEnabled = false
105
+
97
106
  scrollView.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
98
107
  if (scrollY != oldScrollY) {
99
108
  delegate?.contentViewDidScroll()
@@ -112,6 +121,19 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
112
121
  }
113
122
  }
114
123
 
124
+ // TODO: Replace this workaround with synchronous state layout updates on every sheet resize.
125
+ // The container is currently sized to the largest detent, so at smaller detents the ScrollView
126
+ // viewport extends beyond the visible area, reducing the effective scroll range. This padding
127
+ // compensates for that difference until we can resize the container per-detent synchronously.
128
+ fun updateScrollExpansionPadding(padding: Int) {
129
+ if (scrollExpansionPadding == padding) return
130
+ scrollExpansionPadding = padding
131
+ val keyboardHeight = keyboardObserver?.currentHeight ?: 0
132
+ val basePadding = if (keyboardHeight > 0) keyboardHeight else bottomInset
133
+ setScrollViewPaddingBottom(originalScrollViewPaddingBottom + basePadding)
134
+ nudgeScrollView()
135
+ }
136
+
115
137
  private fun setScrollViewPaddingBottom(paddingBottom: Int) {
116
138
  val scrollView = pinnedScrollView ?: return
117
139
  scrollView.clipToPadding = false
@@ -119,26 +141,29 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
119
141
  scrollView.paddingLeft,
120
142
  scrollView.paddingTop,
121
143
  scrollView.paddingRight,
122
- paddingBottom
144
+ paddingBottom + scrollExpansionPadding
123
145
  )
124
146
  }
125
147
 
126
148
  fun clearScrollable() {
127
149
  pinnedScrollView?.setOnScrollChangeListener(null as View.OnScrollChangeListener?)
150
+ pinnedScrollView?.isNestedScrollingEnabled = false
151
+ (pinnedScrollView?.parent as? SwipeRefreshLayout)?.isNestedScrollingEnabled = true
152
+ scrollExpansionPadding = 0
128
153
  setScrollViewPaddingBottom(originalScrollViewPaddingBottom)
129
154
  pinnedScrollView = null
130
155
  originalScrollViewPaddingBottom = 0
131
156
  bottomInset = 0
132
157
  }
133
158
 
134
- fun findScrollView(): ScrollView? {
159
+ fun findScrollView(): ViewGroup? {
135
160
  if (pinnedScrollView != null) return pinnedScrollView
136
161
  return findScrollView(this as View)
137
162
  }
138
163
 
139
- private fun findScrollView(view: View): ScrollView? {
140
- if (view is ScrollView) {
141
- return view
164
+ private fun findScrollView(view: View): ViewGroup? {
165
+ if (view is ScrollView || view is NestedScrollView) {
166
+ return view as ViewGroup
142
167
  }
143
168
 
144
169
  if (view is ViewGroup) {
@@ -191,11 +216,13 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
191
216
  val totalBottomInset = if (keyboardHeight > 0) keyboardHeight else bottomInset
192
217
  setScrollViewPaddingBottom(originalScrollViewPaddingBottom + totalBottomInset)
193
218
 
194
- // Trigger a scroll to force update
195
- scrollView.post {
196
- scrollView.smoothScrollBy(0, 1)
197
- scrollView.smoothScrollBy(0, -1)
198
- }
219
+ scrollView.post { nudgeScrollView() }
220
+ }
221
+
222
+ private fun nudgeScrollView() {
223
+ val scrollView = pinnedScrollView ?: return
224
+ scrollView.smoothScrollBy(0, 1)
225
+ scrollView.smoothScrollBy(0, -1)
199
226
  }
200
227
 
201
228
  private fun scrollToFocusedInput() {
@@ -140,6 +140,16 @@ class TrueSheetModule(reactContext: ReactApplicationContext) :
140
140
  }
141
141
  }
142
142
 
143
+ @ReactMethod
144
+ fun handleBackPress(viewTag: Double, promise: Promise) {
145
+ val tag = viewTag.toInt()
146
+
147
+ withTrueSheetView(tag, promise) { view ->
148
+ view.handleBackPress()
149
+ promise.resolve(null)
150
+ }
151
+ }
152
+
143
153
  /**
144
154
  * Helper method to get TrueSheetView by tag and execute closure
145
155
  */
@@ -6,7 +6,6 @@ import android.view.ViewGroup
6
6
  import android.view.accessibility.AccessibilityEvent
7
7
  import androidx.annotation.UiThread
8
8
  import com.facebook.react.bridge.LifecycleEventListener
9
- import com.facebook.react.bridge.ReadableMap
10
9
  import com.facebook.react.bridge.WritableNativeMap
11
10
  import com.facebook.react.uimanager.PixelUtil.pxToDp
12
11
  import com.facebook.react.uimanager.StateWrapper
@@ -282,7 +281,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
282
281
  setupScrollable()
283
282
  }
284
283
 
285
- fun setScrollableOptions(options: ReadableMap?) {
284
+ fun setScrollableOptions(options: ScrollableOptions?) {
286
285
  viewController.scrollableOptions = options
287
286
  setupScrollable()
288
287
  }
@@ -379,6 +378,11 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
379
378
  viewController.present(detentIndex, animated)
380
379
  }
381
380
 
381
+ @UiThread
382
+ fun handleBackPress() {
383
+ viewController.handleBackPress()
384
+ }
385
+
382
386
  @UiThread
383
387
  fun dismiss(animated: Boolean = true, promiseCallback: () -> Unit) {
384
388
  if (viewController.isBeingDismissed || !viewController.isPresented) {
@@ -577,9 +581,9 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
577
581
  eventDispatcher?.dispatchEvent(BlurEvent(surfaceId, id))
578
582
  }
579
583
 
580
- override fun viewControllerDidBackPress() {
584
+ override fun viewControllerDidChangeVisibility(visible: Boolean) {
581
585
  val surfaceId = UIManagerHelper.getSurfaceId(this)
582
- eventDispatcher?.dispatchEvent(BackPressEvent(surfaceId, id))
586
+ eventDispatcher?.dispatchEvent(VisibilityChangeEvent(surfaceId, id, visible))
583
587
  }
584
588
 
585
589
  // ==================== TrueSheetContainerViewDelegate ====================
@@ -8,14 +8,10 @@ import android.view.View
8
8
  import android.view.ViewGroup
9
9
  import android.view.accessibility.AccessibilityNodeInfo
10
10
  import android.widget.ImageView
11
- import android.widget.ScrollView
12
- import androidx.activity.OnBackPressedCallback
13
- import androidx.appcompat.app.AppCompatActivity
14
11
  import androidx.coordinatorlayout.widget.CoordinatorLayout
15
12
  import androidx.core.graphics.createBitmap
16
13
  import androidx.core.view.isNotEmpty
17
14
  import com.facebook.react.R
18
- import com.facebook.react.bridge.ReadableMap
19
15
  import com.facebook.react.uimanager.JSPointerDispatcher
20
16
  import com.facebook.react.uimanager.JSTouchDispatcher
21
17
  import com.facebook.react.uimanager.PixelUtil.dpToPx
@@ -27,6 +23,7 @@ import com.facebook.react.util.RNLog
27
23
  import com.facebook.react.views.view.ReactViewGroup
28
24
  import com.google.android.material.bottomsheet.BottomSheetBehavior
29
25
  import com.lodev09.truesheet.core.GrabberOptions
26
+ import com.lodev09.truesheet.core.TrueSheetBottomSheetBehavior
30
27
  import com.lodev09.truesheet.core.TrueSheetBottomSheetView
31
28
  import com.lodev09.truesheet.core.TrueSheetBottomSheetViewDelegate
32
29
  import com.lodev09.truesheet.core.TrueSheetCoordinatorLayout
@@ -64,7 +61,7 @@ interface TrueSheetViewControllerDelegate {
64
61
  fun viewControllerDidFocus()
65
62
  fun viewControllerWillBlur()
66
63
  fun viewControllerDidBlur()
67
- fun viewControllerDidBackPress()
64
+ fun viewControllerDidChangeVisibility(visible: Boolean)
68
65
  }
69
66
 
70
67
  // =============================================================================
@@ -146,9 +143,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
146
143
  private var dimView: TrueSheetDimView? = null
147
144
  private var parentDimView: TrueSheetDimView? = null
148
145
 
149
- // Back button handling
150
- private var backCallback: OnBackPressedCallback? = null
151
-
152
146
  // Presentation State
153
147
  var isPresented = false
154
148
  private set
@@ -213,7 +207,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
213
207
 
214
208
  var scrollable: Boolean = false
215
209
 
216
- var scrollableOptions: ReadableMap? = null
210
+ var scrollableOptions: ScrollableOptions? = null
211
+ set(value) {
212
+ field = value
213
+ behavior?.scrollingExpandsSheet = value?.scrollingExpandsSheet ?: true
214
+ if (isPresented) sheetView?.let { updateScrollExpansionPadding(it.top) }
215
+ }
217
216
 
218
217
  override var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
219
218
  set(value) {
@@ -245,7 +244,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
245
244
  // =============================================================================
246
245
 
247
246
  // Behavior
248
- private val behavior: BottomSheetBehavior<TrueSheetBottomSheetView>?
247
+ private val behavior: TrueSheetBottomSheetBehavior<TrueSheetBottomSheetView>?
249
248
  get() = sheetView?.behavior
250
249
 
251
250
  internal val containerView: TrueSheetContainerView?
@@ -349,7 +348,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
349
348
 
350
349
  private fun cleanupSheet() {
351
350
  cleanupKeyboardObserver()
352
- cleanupBackCallback()
353
351
  sheetView?.animate()?.cancel()
354
352
 
355
353
  // Cleanup dim views
@@ -411,28 +409,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
411
409
  snapshotView = null
412
410
  }
413
411
 
414
- // =============================================================================
415
- // MARK: - Back Button Handling
416
- // =============================================================================
417
-
418
- private fun setupBackCallback() {
419
- val activity = reactContext.currentActivity as? AppCompatActivity ?: return
420
-
421
- backCallback = object : OnBackPressedCallback(true) {
422
- override fun handleOnBackPressed() {
423
- delegate?.viewControllerDidBackPress()
424
- dismissOrCollapseToLowest()
425
- }
426
- }
427
-
428
- activity.onBackPressedDispatcher.addCallback(backCallback!!)
429
- }
430
-
431
- private fun cleanupBackCallback() {
432
- backCallback?.remove()
433
- backCallback = null
434
- }
435
-
436
412
  // =============================================================================
437
413
  // MARK: - TrueSheetCoordinatorLayout.Delegate
438
414
  // =============================================================================
@@ -453,7 +429,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
453
429
  sheetView?.let { emitChangePositionDelegate(it.top, realtime = false) }
454
430
  }
455
431
 
456
- override fun findScrollView(): ScrollView? = containerView?.contentView?.findScrollView()
432
+ override fun findScrollView(): ViewGroup? = containerView?.contentView?.findScrollView()
457
433
  override fun findSheetView(): TrueSheetBottomSheetView? = sheetView
458
434
 
459
435
  // =============================================================================
@@ -480,7 +456,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
480
456
  children.forEach { it.viewController.dismiss(animated = true) }
481
457
  }
482
458
 
483
- dismissOrCollapseToLowest()
459
+ if (dismissible) {
460
+ dismiss(animated = true)
461
+ } else if (parentSheetView == null && isDimmedAtCurrentDetent && dimmedDetentIndex > 0) {
462
+ setStateForDetentIndex(dimmedDetentIndex - 1)
463
+ }
484
464
  }
485
465
 
486
466
  // =============================================================================
@@ -496,6 +476,20 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
496
476
  }
497
477
  }
498
478
 
479
+ override fun bottomSheetViewDidAccessibilityIncrement() {
480
+ if (currentDetentIndex < detents.size - 1) {
481
+ setStateForDetentIndex(currentDetentIndex + 1)
482
+ }
483
+ }
484
+
485
+ override fun bottomSheetViewDidAccessibilityDecrement() {
486
+ if (currentDetentIndex > 0) {
487
+ setStateForDetentIndex(currentDetentIndex - 1)
488
+ } else if (dismissible) {
489
+ dismiss(animated = true)
490
+ }
491
+ }
492
+
499
493
  // =============================================================================
500
494
  // MARK: - BottomSheetCallback
501
495
  // =============================================================================
@@ -548,6 +542,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
548
542
  else -> { }
549
543
  }
550
544
 
545
+ updateScrollExpansionPadding(sheetView.top)
551
546
  emitChangePositionDelegate(sheetView.top)
552
547
 
553
548
  // On older APIs, use onSlide for footer positioning during keyboard transitions
@@ -561,6 +556,15 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
561
556
  }
562
557
  }
563
558
 
559
+ private fun updateScrollExpansionPadding(sheetTop: Int) {
560
+ if (!scrollable) {
561
+ containerView?.contentView?.updateScrollExpansionPadding(0)
562
+ return
563
+ }
564
+ val expandedOffset = behavior?.expandedOffset ?: return
565
+ containerView?.contentView?.updateScrollExpansionPadding(maxOf(0, sheetTop - expandedOffset))
566
+ }
567
+
564
568
  private fun handleStateSettled(sheetView: View, newState: Int) {
565
569
  if (interactionState is InteractionState.Reconfiguring) return
566
570
 
@@ -586,6 +590,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
586
590
  currentDetentIndex = detentInfo.index
587
591
  setupDimmedBackground()
588
592
  delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
593
+ this@TrueSheetViewController.sheetView?.updateGrabberAccessibilityValue(detentInfo.index, detents.size)
589
594
  }
590
595
 
591
596
  interactionState = InteractionState.Idle
@@ -598,6 +603,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
598
603
  val detent = detentCalculator.getDetentValueForIndex(detentInfo.index)
599
604
  delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
600
605
  }
606
+ this@TrueSheetViewController.sheetView?.updateGrabberAccessibilityValue(detentInfo.index, detents.size)
601
607
  }
602
608
  }
603
609
  }
@@ -624,8 +630,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
624
630
 
625
631
  isSheetVisible = false
626
632
  wasHiddenByScreen = true
627
- backCallback?.isEnabled = false
628
-
633
+ delegate?.viewControllerDidChangeVisibility(false)
629
634
  dimViews.forEach { it.animate().alpha(0f).setDuration(SCREEN_FADE_DURATION).start() }
630
635
  sheet.animate()
631
636
  .alpha(0f)
@@ -641,10 +646,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
641
646
 
642
647
  internal fun showAfterScreen() {
643
648
  isSheetVisible = true
649
+ delegate?.viewControllerDidChangeVisibility(true)
644
650
  setSheetVisibility(true)
645
651
  sheetView?.alpha = 1f
646
652
  updateDimAmount(animated = true)
647
- backCallback?.isEnabled = true
648
653
  }
649
654
 
650
655
  /**
@@ -677,7 +682,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
677
682
  setupSheetDetents()
678
683
  setupDimmedBackground()
679
684
  setupKeyboardObserver()
680
- setupBackCallback()
681
685
 
682
686
  sheet.setupBackground()
683
687
  sheet.setupElevation()
@@ -722,11 +726,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
722
726
  val params = sheet.createLayoutParams()
723
727
 
724
728
  @Suppress("UNCHECKED_CAST")
725
- val behavior = params.behavior as BottomSheetBehavior<TrueSheetBottomSheetView>
729
+ val behavior = params.behavior as TrueSheetBottomSheetBehavior<TrueSheetBottomSheetView>
726
730
 
727
731
  // Configure behavior
728
732
  behavior.isHideable = true
729
733
  behavior.isDraggable = draggable
734
+ behavior.scrollingExpandsSheet = scrollableOptions?.scrollingExpandsSheet ?: true
730
735
  behavior.state = BottomSheetBehavior.STATE_HIDDEN
731
736
  behavior.addBottomSheetCallback(sheetCallback)
732
737
 
@@ -754,11 +759,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
754
759
  KeyboardUtils.dismiss(reactContext)
755
760
  }
756
761
 
757
- private fun dismissOrCollapseToLowest() {
762
+ fun handleBackPress() {
758
763
  if (dismissible) {
759
764
  dismiss(animated = true)
760
- } else if (parentSheetView == null && isDimmedAtCurrentDetent && dimmedDetentIndex > 0) {
761
- setStateForDetentIndex(dimmedDetentIndex - 1)
765
+ } else if (currentDetentIndex > 0) {
766
+ setStateForDetentIndex(0)
762
767
  }
763
768
  }
764
769
 
@@ -787,6 +792,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
787
792
  delegate?.viewControllerDidPresent(index, position, detent)
788
793
  parentSheetView?.viewControllerDidBlur()
789
794
  delegate?.viewControllerDidFocus()
795
+ sheetView?.updateGrabberAccessibilityValue(index, detents.size)
790
796
 
791
797
  presentPromise?.invoke()
792
798
  presentPromise = null
@@ -74,7 +74,7 @@ class TrueSheetViewManager :
74
74
  FocusEvent.EVENT_NAME to hashMapOf("registrationName" to FocusEvent.REGISTRATION_NAME),
75
75
  WillBlurEvent.EVENT_NAME to hashMapOf("registrationName" to WillBlurEvent.REGISTRATION_NAME),
76
76
  BlurEvent.EVENT_NAME to hashMapOf("registrationName" to BlurEvent.REGISTRATION_NAME),
77
- BackPressEvent.EVENT_NAME to hashMapOf("registrationName" to BackPressEvent.REGISTRATION_NAME)
77
+ VisibilityChangeEvent.EVENT_NAME to hashMapOf("registrationName" to VisibilityChangeEvent.REGISTRATION_NAME)
78
78
  )
79
79
 
80
80
  // ==================== Props ====================
@@ -218,7 +218,16 @@ class TrueSheetViewManager :
218
218
 
219
219
  @ReactProp(name = "scrollableOptions")
220
220
  override fun setScrollableOptions(view: TrueSheetView, options: ReadableMap?) {
221
- view.setScrollableOptions(options)
221
+ if (options == null) {
222
+ view.setScrollableOptions(null)
223
+ return
224
+ }
225
+
226
+ val scrollableOptions = ScrollableOptions(
227
+ keyboardScrollOffset = if (options.hasKey("keyboardScrollOffset")) options.getDouble("keyboardScrollOffset").toFloat() else 0f,
228
+ scrollingExpandsSheet = if (options.hasKey("scrollingExpandsSheet")) options.getBoolean("scrollingExpandsSheet") else true
229
+ )
230
+ view.setScrollableOptions(scrollableOptions)
222
231
  }
223
232
 
224
233
  companion object {
@@ -0,0 +1,36 @@
1
+ package com.lodev09.truesheet.core
2
+
3
+ import android.view.View
4
+ import androidx.coordinatorlayout.widget.CoordinatorLayout
5
+ import com.google.android.material.bottomsheet.BottomSheetBehavior
6
+
7
+ class TrueSheetBottomSheetBehavior<V : View> : BottomSheetBehavior<V>() {
8
+ var scrollingExpandsSheet: Boolean = true
9
+
10
+ override fun onNestedPreScroll(
11
+ coordinatorLayout: CoordinatorLayout,
12
+ child: V,
13
+ target: View,
14
+ dx: Int,
15
+ dy: Int,
16
+ consumed: IntArray,
17
+ type: Int
18
+ ) {
19
+ // dy > 0 = user swiping up = sheet expanding
20
+ // Block expansion from scroll, but allow if sheet is already being dragged
21
+ if (!scrollingExpandsSheet && dy > 0 && state != STATE_DRAGGING) return
22
+ super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
23
+ }
24
+
25
+ override fun onNestedPreFling(
26
+ coordinatorLayout: CoordinatorLayout,
27
+ child: V,
28
+ target: View,
29
+ velocityX: Float,
30
+ velocityY: Float
31
+ ): Boolean {
32
+ // Don't consume flings — let the ScrollView decelerate naturally
33
+ if (!scrollingExpandsSheet) return false
34
+ return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY)
35
+ }
36
+ }
@@ -14,6 +14,7 @@ import android.view.View
14
14
  import android.view.ViewOutlineProvider
15
15
  import android.widget.FrameLayout
16
16
  import androidx.coordinatorlayout.widget.CoordinatorLayout
17
+ import androidx.core.view.ViewCompat
17
18
  import com.facebook.react.uimanager.PixelUtil.dpToPx
18
19
  import com.facebook.react.uimanager.ThemedReactContext
19
20
  import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -32,6 +33,8 @@ interface TrueSheetBottomSheetViewDelegate {
32
33
  val grabberOptions: GrabberOptions?
33
34
  val draggable: Boolean
34
35
  fun bottomSheetViewDidTapGrabber()
36
+ fun bottomSheetViewDidAccessibilityIncrement()
37
+ fun bottomSheetViewDidAccessibilityDecrement()
35
38
  }
36
39
 
37
40
  /**
@@ -63,9 +66,9 @@ class TrueSheetBottomSheetView(private val reactContext: ThemedReactContext) : F
63
66
 
64
67
  // Behavior reference (set after adding to CoordinatorLayout)
65
68
  @Suppress("UNCHECKED_CAST")
66
- val behavior: BottomSheetBehavior<TrueSheetBottomSheetView>?
69
+ val behavior: TrueSheetBottomSheetBehavior<TrueSheetBottomSheetView>?
67
70
  get() = (layoutParams as? CoordinatorLayout.LayoutParams)
68
- ?.behavior as? BottomSheetBehavior<TrueSheetBottomSheetView>
71
+ ?.behavior as? TrueSheetBottomSheetBehavior<TrueSheetBottomSheetView>
69
72
 
70
73
  // =============================================================================
71
74
  // MARK: - Initialization
@@ -75,6 +78,8 @@ class TrueSheetBottomSheetView(private val reactContext: ThemedReactContext) : F
75
78
  // Allow content to extend beyond bounds (for footer positioning)
76
79
  clipChildren = false
77
80
  clipToPadding = false
81
+
82
+ ViewCompat.setAccessibilityPaneTitle(this, "Bottom sheet")
78
83
  }
79
84
 
80
85
  override fun setTranslationY(translationY: Float) {
@@ -106,7 +111,7 @@ class TrueSheetBottomSheetView(private val reactContext: ThemedReactContext) : F
106
111
  fun createLayoutParams(): CoordinatorLayout.LayoutParams {
107
112
  val applyMaxWidth = delegate?.maxContentWidth != null && !ScreenUtils.isPortraitPhone(reactContext)
108
113
  val effectiveMaxWidth = if (applyMaxWidth) delegate!!.maxContentWidth!! else DEFAULT_MAX_WIDTH.dpToPx().toInt()
109
- val behavior = BottomSheetBehavior<TrueSheetBottomSheetView>().apply {
114
+ val behavior = TrueSheetBottomSheetBehavior<TrueSheetBottomSheetView>().apply {
110
115
  isHideable = true
111
116
  maxWidth = effectiveMaxWidth
112
117
  }
@@ -196,11 +201,17 @@ class TrueSheetBottomSheetView(private val reactContext: ThemedReactContext) : F
196
201
 
197
202
  val grabberView = TrueSheetGrabberView(reactContext, delegate?.grabberOptions).apply {
198
203
  tag = GRABBER_TAG
204
+ onAccessibilityIncrement = { delegate?.bottomSheetViewDidAccessibilityIncrement() }
205
+ onAccessibilityDecrement = { delegate?.bottomSheetViewDidAccessibilityDecrement() }
199
206
  }
200
207
 
201
208
  addView(grabberView)
202
209
  }
203
210
 
211
+ fun updateGrabberAccessibilityValue(index: Int, detentCount: Int) {
212
+ findViewWithTag<TrueSheetGrabberView>(GRABBER_TAG)?.updateAccessibilityValue(index, detentCount)
213
+ }
214
+
204
215
  // =============================================================================
205
216
  // MARK: - Grabber Tap Detection
206
217
  // =============================================================================
@@ -5,7 +5,7 @@ import android.content.Context
5
5
  import android.content.res.Configuration
6
6
  import android.view.MotionEvent
7
7
  import android.view.ViewConfiguration
8
- import android.widget.ScrollView
8
+ import android.view.ViewGroup
9
9
  import androidx.coordinatorlayout.widget.CoordinatorLayout
10
10
  import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
11
11
  import com.facebook.react.uimanager.PointerEvents
@@ -15,7 +15,7 @@ import com.lodev09.truesheet.utils.isDescendantOf
15
15
  interface TrueSheetCoordinatorLayoutDelegate {
16
16
  fun coordinatorLayoutDidLayout(changed: Boolean)
17
17
  fun coordinatorLayoutDidChangeConfiguration()
18
- fun findScrollView(): ScrollView?
18
+ fun findScrollView(): ViewGroup?
19
19
  fun findSheetView(): TrueSheetBottomSheetView?
20
20
  }
21
21
 
@@ -75,7 +75,7 @@ class TrueSheetCoordinatorLayout(context: Context) :
75
75
  val sheet = delegate?.findSheetView() ?: return
76
76
  val behavior = sheet.behavior ?: return
77
77
  try {
78
- val field = behavior.javaClass.getDeclaredField("nestedScrollingChildRef")
78
+ val field = behavior.javaClass.superclass.getDeclaredField("nestedScrollingChildRef")
79
79
  field.isAccessible = true
80
80
  @Suppress("UNCHECKED_CAST")
81
81
  val ref = field.get(behavior) as? java.lang.ref.WeakReference<android.view.View> ?: return
@@ -29,7 +29,7 @@ interface TrueSheetDimViewDelegate {
29
29
  * This implements the "dimmedDetentIndex" equivalent functionality:
30
30
  * the view only becomes interactive when the sheet is at or above the dimmed detent.
31
31
  */
32
- @SuppressLint("ViewConstructor", "ClickableViewAccessibility")
32
+ @SuppressLint("ViewConstructor")
33
33
  class TrueSheetDimView(private val reactContext: ThemedReactContext) :
34
34
  View(reactContext),
35
35
  ReactPointerEventsView {
@@ -60,6 +60,8 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
60
60
  setOnClickListener {
61
61
  delegate?.dimViewDidTap()
62
62
  }
63
+
64
+ importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
63
65
  }
64
66
 
65
67
  // =============================================================================