@lodev09/react-native-true-sheet 3.7.4-beta.2 → 3.8.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 (34) hide show
  1. package/README.md +9 -48
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +24 -0
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +162 -1
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +2 -3
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +76 -8
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +70 -74
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +5 -0
  8. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +63 -0
  9. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +1 -0
  10. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +2 -19
  11. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetKeyboardObserver.kt +34 -6
  12. package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +12 -0
  13. package/ios/TrueSheetContainerView.h +15 -4
  14. package/ios/TrueSheetContainerView.mm +45 -6
  15. package/ios/TrueSheetContentView.h +6 -2
  16. package/ios/TrueSheetContentView.mm +88 -2
  17. package/ios/TrueSheetFooterView.h +4 -3
  18. package/ios/TrueSheetFooterView.mm +16 -63
  19. package/ios/TrueSheetView.mm +31 -7
  20. package/ios/core/TrueSheetKeyboardObserver.h +38 -0
  21. package/ios/core/TrueSheetKeyboardObserver.mm +90 -0
  22. package/lib/module/TrueSheet.js +3 -1
  23. package/lib/module/TrueSheet.js.map +1 -1
  24. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +5 -0
  25. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  26. package/lib/typescript/src/TrueSheet.types.d.ts +15 -0
  27. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  28. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +4 -0
  29. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/src/TrueSheet.tsx +3 -1
  32. package/src/TrueSheet.types.ts +17 -0
  33. package/src/fabric/TrueSheetViewNativeComponent.ts +5 -0
  34. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +0 -266
@@ -8,11 +8,13 @@ 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
11
12
  import androidx.activity.OnBackPressedCallback
12
13
  import androidx.appcompat.app.AppCompatActivity
13
14
  import androidx.core.graphics.createBitmap
14
15
  import androidx.core.view.isNotEmpty
15
16
  import com.facebook.react.R
17
+ import com.facebook.react.bridge.ReadableMap
16
18
  import com.facebook.react.uimanager.JSPointerDispatcher
17
19
  import com.facebook.react.uimanager.JSTouchDispatcher
18
20
  import com.facebook.react.uimanager.PixelUtil.dpToPx
@@ -24,7 +26,6 @@ import com.facebook.react.util.RNLog
24
26
  import com.facebook.react.views.view.ReactViewGroup
25
27
  import com.google.android.material.bottomsheet.BottomSheetBehavior
26
28
  import com.lodev09.truesheet.core.GrabberOptions
27
- import com.lodev09.truesheet.core.RNScreensFragmentObserver
28
29
  import com.lodev09.truesheet.core.TrueSheetBottomSheetView
29
30
  import com.lodev09.truesheet.core.TrueSheetBottomSheetViewDelegate
30
31
  import com.lodev09.truesheet.core.TrueSheetCoordinatorLayout
@@ -131,7 +132,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
131
132
 
132
133
  private var interactionState: InteractionState = InteractionState.Idle
133
134
  private var isDismissing = false
134
- internal var wasHiddenByScreen = false
135
+ var wasHiddenByScreen = false
135
136
  private var shouldAnimatePresent = false
136
137
  private var isPresentAnimating = false
137
138
 
@@ -145,6 +146,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
145
146
 
146
147
  // Promises
147
148
  var presentPromise: (() -> Unit)? = null
149
+ var resizePromise: (() -> Unit)? = null
148
150
  var dismissPromise: (() -> Unit)? = null
149
151
 
150
152
  // For stacked sheets
@@ -152,7 +154,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
152
154
 
153
155
  // Helper Objects
154
156
  private var keyboardObserver: TrueSheetKeyboardObserver? = null
155
- private var rnScreensObserver: RNScreensFragmentObserver? = null
156
157
  internal val detentCalculator = TrueSheetDetentCalculator(reactContext).apply {
157
158
  delegate = this@TrueSheetViewController
158
159
  }
@@ -175,12 +176,15 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
175
176
  override var grabberOptions: GrabberOptions? = null
176
177
  override var sheetBackgroundColor: Int? = null
177
178
  var insetAdjustment: String = "automatic"
179
+
178
180
  var scrollable: Boolean = false
179
181
  set(value) {
180
182
  field = value
181
183
  coordinatorLayout?.scrollable = value
182
184
  }
183
185
 
186
+ var scrollableOptions: ReadableMap? = null
187
+
184
188
  override var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
185
189
  set(value) {
186
190
  field = if (value < 0) DEFAULT_CORNER_RADIUS.dpToPx() else value
@@ -316,7 +320,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
316
320
 
317
321
  private fun cleanupSheet() {
318
322
  cleanupKeyboardObserver()
319
- cleanupModalObserver()
320
323
  cleanupBackCallback()
321
324
  sheetView?.animate()?.cancel()
322
325
 
@@ -332,6 +335,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
332
335
  // Detach content from sheet
333
336
  sheetView?.removeView(this)
334
337
 
338
+ containerView?.cleanupKeyboardHandler()
335
339
  coordinatorLayout = null
336
340
  sheetView = null
337
341
 
@@ -417,6 +421,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
417
421
  sheetView?.let { emitChangePositionDelegate(it.top, realtime = false) }
418
422
  }
419
423
 
424
+ override fun findScrollView(): ScrollView? = containerView?.contentView?.findScrollView()
425
+
420
426
  // =============================================================================
421
427
  // MARK: - TrueSheetDimViewDelegate
422
428
  // =============================================================================
@@ -561,54 +567,15 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
561
567
  }
562
568
 
563
569
  // =============================================================================
564
- // MARK: - Modal Observer (react-native-screens)
570
+ // MARK: - Screen Visibility
565
571
  // =============================================================================
566
572
 
567
- private fun setupModalObserver() {
568
- rnScreensObserver = RNScreensFragmentObserver(
569
- reactContext = reactContext,
570
- onScreenPresented = {
571
- if (isPresented && isTopmostSheet) {
572
- if (isSheetVisible) {
573
- dismissKeyboard()
574
- post { hideForScreen() }
575
- } else {
576
- // Sheet is already hidden, just mark it
577
- wasHiddenByScreen = true
578
- }
579
- }
580
- },
581
- onScreenWillDismiss = {
582
- val hasPushedScreens = rnScreensObserver?.hasPushedScreens == true
583
- if (isPresented && wasHiddenByScreen && isTopmostSheet && !hasPushedScreens) {
584
- showAfterScreen()
585
- delegate?.viewControllerDidDetectScreenDismiss()
586
- }
587
- },
588
- onScreenDidDismiss = {
589
- if (isPresented && wasHiddenByScreen) {
590
- wasHiddenByScreen = false
591
- // Restore parent sheet after this sheet is restored
592
- parentSheetView?.viewController?.let { parent ->
593
- post { parent.showAfterScreen() }
594
- }
595
- }
596
- }
597
- )
598
- rnScreensObserver?.start()
599
- }
600
-
601
- private fun cleanupModalObserver() {
602
- rnScreensObserver?.stop()
603
- rnScreensObserver = null
604
- }
605
-
606
573
  private fun setSheetVisibility(visible: Boolean) {
607
574
  coordinatorLayout?.visibility = if (visible) VISIBLE else GONE
608
575
  dimViews.forEach { it.visibility = if (visible) VISIBLE else INVISIBLE }
609
576
  }
610
577
 
611
- private fun hideForScreen() {
578
+ internal fun hideForScreen() {
612
579
  val sheet = sheetView ?: run {
613
580
  RNLog.e(reactContext, "TrueSheet: sheetView is null in hideForScreen")
614
581
  return
@@ -630,7 +597,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
630
597
  parentSheetView?.viewController?.hideForScreen()
631
598
  }
632
599
 
633
- private fun showAfterScreen() {
600
+ internal fun showAfterScreen() {
634
601
  isSheetVisible = true
635
602
  setSheetVisibility(true)
636
603
  sheetView?.alpha = 1f
@@ -662,41 +629,58 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
662
629
  }
663
630
 
664
631
  if (isPresented) {
665
- setupDimmedBackground()
666
- setStateForDetentIndex(detentIndex)
632
+ RNLog.w(reactContext, "TrueSheet: sheet is already presented. Use resize() to change detent.")
633
+ presentPromise?.invoke()
634
+ presentPromise = null
635
+ return
636
+ }
637
+
638
+ shouldAnimatePresent = animated
639
+ currentDetentIndex = detentIndex
640
+ interactionState = InteractionState.Idle
641
+
642
+ // Setup sheet in coordinator layout
643
+ setupSheetInCoordinator(coordinator, sheet)
644
+
645
+ emitWillPresentEvents()
646
+
647
+ setupSheetDetents()
648
+ setupDimmedBackground()
649
+ setupKeyboardObserver()
650
+ setupBackCallback()
651
+
652
+ sheet.setupBackground()
653
+ sheet.setupElevation()
654
+ sheet.setupGrabber()
655
+
656
+ if (shouldAnimatePresent) {
657
+ isPresentAnimating = true
658
+ post { setStateForDetentIndex(currentDetentIndex) }
667
659
  } else {
668
- shouldAnimatePresent = animated
669
- currentDetentIndex = detentIndex
670
- interactionState = InteractionState.Idle
671
-
672
- // Setup sheet in coordinator layout
673
- setupSheetInCoordinator(coordinator, sheet)
674
-
675
- emitWillPresentEvents()
676
-
677
- setupSheetDetents()
678
- setupDimmedBackground()
679
- setupKeyboardObserver()
680
- setupModalObserver()
681
- setupBackCallback()
682
-
683
- sheet.setupBackground()
684
- sheet.setupElevation()
685
- sheet.setupGrabber()
686
-
687
- if (shouldAnimatePresent) {
688
- isPresentAnimating = true
689
- post { setStateForDetentIndex(currentDetentIndex) }
690
- } else {
691
- setStateForDetentIndex(currentDetentIndex)
660
+ setStateForDetentIndex(currentDetentIndex)
661
+ post {
692
662
  emitChangePositionDelegate(detentCalculator.getSheetTopForDetentIndex(currentDetentIndex))
693
663
  updateDimAmount()
694
664
  finishPresent()
695
665
  }
666
+ }
667
+
668
+ isPresented = true
669
+ isSheetVisible = true
670
+ }
696
671
 
697
- isPresented = true
698
- isSheetVisible = true
672
+ fun resize(detentIndex: Int) {
673
+ if (!isPresented) {
674
+ RNLog.w(reactContext, "TrueSheet: Cannot resize. Sheet is not presented.")
675
+ resizePromise?.invoke()
676
+ resizePromise = null
677
+ return
699
678
  }
679
+
680
+ setupDimmedBackground()
681
+ setStateForDetentIndex(detentIndex)
682
+ resizePromise?.invoke()
683
+ resizePromise = null
700
684
  }
701
685
 
702
686
  private fun setupSheetInCoordinator(coordinator: TrueSheetCoordinatorLayout, sheet: TrueSheetBottomSheetView) {
@@ -766,6 +750,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
766
750
  // Restore isHideable to actual value after present animation
767
751
  behavior?.isHideable = dismissible
768
752
 
753
+ containerView?.setupKeyboardHandler()
754
+
769
755
  val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
770
756
  delegate?.viewControllerDidPresent(index, position, detent)
771
757
  parentSheetView?.viewControllerDidBlur()
@@ -1012,6 +998,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1012
998
  if (!shouldHandleKeyboard(checkFocus = !isHandlingKeyboard)) return
1013
999
  positionFooter()
1014
1000
  }
1001
+
1002
+ override fun focusDidChange(newFocus: View) {
1003
+ // Handle case where keyboard is already visible and focus moves into the sheet
1004
+ if (!shouldHandleKeyboard()) return
1005
+ if (detentIndexBeforeKeyboard < 0 && (keyboardObserver?.currentHeight ?: 0) > 0) {
1006
+ detentIndexBeforeKeyboard = currentDetentIndex
1007
+ currentDetentIndex = detents.size - 1
1008
+ setupSheetDetents()
1009
+ }
1010
+ }
1015
1011
  }
1016
1012
  start()
1017
1013
  }
@@ -203,6 +203,11 @@ class TrueSheetViewManager :
203
203
  view.setSheetElevation(elevation.toFloat())
204
204
  }
205
205
 
206
+ @ReactProp(name = "scrollableOptions")
207
+ override fun setScrollableOptions(view: TrueSheetView, options: ReadableMap?) {
208
+ view.setScrollableOptions(options)
209
+ }
210
+
206
211
  companion object {
207
212
  const val REACT_CLASS = "TrueSheetView"
208
213
  const val TAG_NAME = "TrueSheet"
@@ -0,0 +1,63 @@
1
+ package com.lodev09.truesheet.core
2
+
3
+ import android.view.View
4
+ import com.facebook.react.uimanager.events.Event
5
+ import com.facebook.react.uimanager.events.EventDispatcher
6
+ import com.facebook.react.uimanager.events.EventDispatcherListener
7
+
8
+ private const val RN_SCREENS_VIEW_CLASS = "com.swmansion.rnscreens.Screen"
9
+
10
+ interface RNScreensEventObserverDelegate {
11
+ fun presenterScreenWillDisappear()
12
+ fun presenterScreenWillAppear()
13
+ }
14
+
15
+ /**
16
+ * Observes react-native-screens lifecycle events via EventDispatcherListener.
17
+ * Detects when the presenting screen unmounts while sheet is presented.
18
+ */
19
+ class RNScreensEventObserver : EventDispatcherListener {
20
+ var delegate: RNScreensEventObserverDelegate? = null
21
+
22
+ private var eventDispatcher: EventDispatcher? = null
23
+ var presenterScreenTag: Int = 0
24
+
25
+ fun startObserving(dispatcher: EventDispatcher?) {
26
+ if (eventDispatcher != null || dispatcher == null) return
27
+
28
+ eventDispatcher = dispatcher
29
+ dispatcher.addListener(this)
30
+ }
31
+
32
+ fun stopObserving() {
33
+ eventDispatcher?.removeListener(this)
34
+ eventDispatcher = null
35
+ }
36
+
37
+ fun capturePresenterScreenFromView(view: View?) {
38
+ presenterScreenTag = 0
39
+
40
+ var current: View? = view
41
+ while (current != null) {
42
+ if (isScreenView(current)) {
43
+ presenterScreenTag = current.id
44
+ break
45
+ }
46
+ current = (current.parent as? View)
47
+ }
48
+ }
49
+
50
+ override fun onEventDispatch(event: Event<*>) {
51
+ // Only process events for the presenter screen
52
+ if (presenterScreenTag == 0 || event.viewTag != presenterScreenTag) return
53
+
54
+ when (event.eventName) {
55
+ "topWillDisappear" -> delegate?.presenterScreenWillDisappear()
56
+ "topWillAppear" -> delegate?.presenterScreenWillAppear()
57
+ }
58
+ }
59
+
60
+ companion object {
61
+ private fun isScreenView(view: View): Boolean = view.javaClass.name == RN_SCREENS_VIEW_CLASS
62
+ }
63
+ }
@@ -52,6 +52,7 @@ class TrueSheetBottomSheetView(private val reactContext: ThemedReactContext) : F
52
52
  var delegate: TrueSheetBottomSheetViewDelegate? = null
53
53
 
54
54
  // Behavior reference (set after adding to CoordinatorLayout)
55
+ @Suppress("UNCHECKED_CAST")
55
56
  val behavior: BottomSheetBehavior<TrueSheetBottomSheetView>?
56
57
  get() = (layoutParams as? CoordinatorLayout.LayoutParams)
57
58
  ?.behavior as? BottomSheetBehavior<TrueSheetBottomSheetView>
@@ -5,7 +5,6 @@ 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.view.ViewGroup
9
8
  import android.widget.ScrollView
10
9
  import androidx.coordinatorlayout.widget.CoordinatorLayout
11
10
  import com.facebook.react.uimanager.PointerEvents
@@ -14,6 +13,7 @@ import com.facebook.react.uimanager.ReactPointerEventsView
14
13
  interface TrueSheetCoordinatorLayoutDelegate {
15
14
  fun coordinatorLayoutDidLayout(changed: Boolean)
16
15
  fun coordinatorLayoutDidChangeConfiguration()
16
+ fun findScrollView(): ScrollView?
17
17
  }
18
18
 
19
19
  /**
@@ -75,7 +75,7 @@ class TrueSheetCoordinatorLayout(context: Context) :
75
75
  return super.onInterceptTouchEvent(ev)
76
76
  }
77
77
 
78
- val scrollView = findScrollView(this)
78
+ val scrollView = delegate?.findScrollView()
79
79
  val cannotScroll = scrollView != null &&
80
80
  scrollView.scrollY == 0 &&
81
81
  !scrollView.canScrollVertically(1)
@@ -126,21 +126,4 @@ class TrueSheetCoordinatorLayout(context: Context) :
126
126
  }
127
127
  return super.onTouchEvent(ev)
128
128
  }
129
-
130
- private fun findScrollView(view: android.view.View): ScrollView? {
131
- if (view is ScrollView) {
132
- return view
133
- }
134
-
135
- if (view is ViewGroup) {
136
- for (i in 0 until view.childCount) {
137
- val scrollView = findScrollView(view.getChildAt(i))
138
- if (scrollView != null) {
139
- return scrollView
140
- }
141
- }
142
- }
143
-
144
- return null
145
- }
146
129
  }
@@ -9,12 +9,15 @@ import androidx.core.view.WindowInsetsAnimationCompat
9
9
  import androidx.core.view.WindowInsetsCompat
10
10
  import com.facebook.react.uimanager.ThemedReactContext
11
11
  import com.lodev09.truesheet.utils.KeyboardUtils
12
+ import com.lodev09.truesheet.utils.isDescendantOf
12
13
 
13
14
  interface TrueSheetKeyboardObserverDelegate {
14
- fun keyboardWillShow(height: Int)
15
- fun keyboardWillHide()
16
- fun keyboardDidHide()
17
- fun keyboardDidChangeHeight(height: Int)
15
+ fun keyboardWillShow(height: Int) {}
16
+ fun keyboardDidShow(height: Int) {}
17
+ fun keyboardWillHide() {}
18
+ fun keyboardDidHide() {}
19
+ fun keyboardDidChangeHeight(height: Int) {}
20
+ fun focusDidChange(newFocus: View) {}
18
21
  }
19
22
 
20
23
  /**
@@ -45,8 +48,10 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
45
48
  return false
46
49
  }
47
50
 
48
- private var isHiding: Boolean = false
51
+ var isHiding: Boolean = false
52
+ private set
49
53
  private var globalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
54
+ private var focusChangeListener: ViewTreeObserver.OnGlobalFocusChangeListener? = null
50
55
  private var activityRootView: View? = null
51
56
 
52
57
  fun start() {
@@ -55,14 +60,19 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
55
60
  } else {
56
61
  setupLegacyListener()
57
62
  }
63
+ setupFocusChangeListener()
58
64
  }
59
65
 
60
66
  fun stop() {
61
67
  globalLayoutListener?.let { listener ->
62
68
  activityRootView?.viewTreeObserver?.removeOnGlobalLayoutListener(listener)
63
69
  globalLayoutListener = null
64
- activityRootView = null
65
70
  }
71
+ focusChangeListener?.let { listener ->
72
+ activityRootView?.viewTreeObserver?.removeOnGlobalFocusChangeListener(listener)
73
+ focusChangeListener = null
74
+ }
75
+ activityRootView = null
66
76
  ViewCompat.setWindowInsetsAnimationCallback(targetView, null)
67
77
  }
68
78
 
@@ -121,6 +131,8 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
121
131
  if (isHiding) {
122
132
  delegate?.keyboardDidHide()
123
133
  isHiding = false
134
+ } else if (finalHeight > 0) {
135
+ delegate?.keyboardDidShow(finalHeight)
124
136
  }
125
137
  }
126
138
  }
@@ -164,9 +176,25 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
164
176
  if (isHiding && newHeight == 0) {
165
177
  delegate?.keyboardDidHide()
166
178
  isHiding = false
179
+ } else if (newHeight > 0) {
180
+ delegate?.keyboardDidShow(newHeight)
167
181
  }
168
182
  }
169
183
 
170
184
  rootView.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
171
185
  }
186
+
187
+ private fun setupFocusChangeListener() {
188
+ if (focusChangeListener != null) return
189
+
190
+ val rootView = targetView.rootView ?: return
191
+ activityRootView = rootView
192
+
193
+ focusChangeListener = ViewTreeObserver.OnGlobalFocusChangeListener { _, newFocus ->
194
+ if (currentHeight > 0 && newFocus != null && newFocus.isDescendantOf(targetView)) {
195
+ delegate?.focusDidChange(newFocus)
196
+ }
197
+ }
198
+ rootView.viewTreeObserver.addOnGlobalFocusChangeListener(focusChangeListener)
199
+ }
172
200
  }
@@ -0,0 +1,12 @@
1
+ package com.lodev09.truesheet.utils
2
+
3
+ import android.view.View
4
+
5
+ fun View.isDescendantOf(ancestor: View): Boolean {
6
+ var current: View? = this
7
+ while (current != null) {
8
+ if (current === ancestor) return true
9
+ current = (current.parent as? View)
10
+ }
11
+ return false
12
+ }
@@ -43,6 +43,16 @@ NS_ASSUME_NONNULL_BEGIN
43
43
  */
44
44
  @property (nonatomic, assign) BOOL scrollViewPinningEnabled;
45
45
 
46
+ /**
47
+ * Inset adjustment mode for pinned ScrollView
48
+ */
49
+ @property (nonatomic, copy, nullable) NSString *insetAdjustment;
50
+
51
+ /**
52
+ * Options for scrollable behavior
53
+ */
54
+ @property (nonatomic, strong, nullable) NSDictionary *scrollableOptions;
55
+
46
56
  /**
47
57
  * Returns the current content height
48
58
  */
@@ -64,14 +74,15 @@ NS_ASSUME_NONNULL_BEGIN
64
74
  - (void)setupContentScrollViewPinning;
65
75
 
66
76
  /**
67
- * Setup keyboard handler for footer
77
+ * Setup keyboard observer for content and footer
78
+ * @param viewController The sheet view controller to observe keyboard events for
68
79
  */
69
- - (void)setupKeyboardHandler;
80
+ - (void)setupKeyboardObserverWithViewController:(UIViewController *)viewController;
70
81
 
71
82
  /**
72
- * Cleanup keyboard handler for footer
83
+ * Cleanup keyboard observer
73
84
  */
74
- - (void)cleanupKeyboardHandler;
85
+ - (void)cleanupKeyboardObserver;
75
86
 
76
87
  @end
77
88
 
@@ -12,6 +12,9 @@
12
12
  #import "TrueSheetContentView.h"
13
13
  #import "TrueSheetFooterView.h"
14
14
  #import "TrueSheetHeaderView.h"
15
+ #import "TrueSheetViewController.h"
16
+ #import "core/TrueSheetKeyboardObserver.h"
17
+ #import "utils/WindowUtil.h"
15
18
 
16
19
  #import <react/renderer/components/TrueSheetSpec/ComponentDescriptors.h>
17
20
  #import <react/renderer/components/TrueSheetSpec/EventEmitters.h>
@@ -30,6 +33,7 @@ using namespace facebook::react;
30
33
  TrueSheetContentView *_contentView;
31
34
  TrueSheetHeaderView *_headerView;
32
35
  TrueSheetFooterView *_footerView;
36
+ TrueSheetKeyboardObserver *_keyboardObserver;
33
37
  BOOL _scrollViewPinningSet;
34
38
  }
35
39
 
@@ -77,9 +81,23 @@ using namespace facebook::react;
77
81
  _scrollViewPinningSet = YES;
78
82
  }
79
83
 
84
+ - (void)setScrollableOptions:(NSDictionary *)scrollableOptions {
85
+ _scrollableOptions = scrollableOptions;
86
+ if (scrollableOptions) {
87
+ NSNumber *offset = scrollableOptions[@"keyboardScrollOffset"];
88
+ _contentView.keyboardScrollOffset = offset ? [offset floatValue] : 0;
89
+ } else {
90
+ _contentView.keyboardScrollOffset = 0;
91
+ }
92
+ }
93
+
80
94
  - (void)setupContentScrollViewPinning {
81
95
  if (_scrollViewPinningSet && _contentView) {
82
- [_contentView setupScrollViewPinning:_scrollViewPinningEnabled];
96
+ CGFloat bottomInset = 0;
97
+ if ([_insetAdjustment isEqualToString:@"automatic"]) {
98
+ bottomInset = [WindowUtil keyWindow].safeAreaInsets.bottom;
99
+ }
100
+ [_contentView setupScrollViewPinning:_scrollViewPinningEnabled bottomInset:bottomInset];
83
101
  }
84
102
  }
85
103
 
@@ -159,14 +177,35 @@ using namespace facebook::react;
159
177
  [self.delegate containerViewHeaderDidChangeSize:newSize];
160
178
  }
161
179
 
162
- #pragma mark - Keyboard Handling
180
+ #pragma mark - Keyboard Observer
163
181
 
164
- - (void)setupKeyboardHandler {
165
- [_footerView setupKeyboardHandler];
182
+ - (void)setupKeyboardObserverWithViewController:(UIViewController *)viewController {
183
+ [self cleanupKeyboardObserver];
184
+
185
+ _keyboardObserver = [[TrueSheetKeyboardObserver alloc] init];
186
+ _keyboardObserver.viewController = (TrueSheetViewController *)viewController;
187
+
188
+ if (_contentView) {
189
+ _contentView.keyboardObserver = _keyboardObserver;
190
+ [_keyboardObserver addDelegate:_contentView];
191
+ }
192
+
193
+ if (_footerView) {
194
+ _footerView.keyboardObserver = _keyboardObserver;
195
+ [_keyboardObserver addDelegate:_footerView];
196
+ }
197
+
198
+ [_keyboardObserver start];
166
199
  }
167
200
 
168
- - (void)cleanupKeyboardHandler {
169
- [_footerView cleanupKeyboardHandler];
201
+ - (void)cleanupKeyboardObserver {
202
+ if (_keyboardObserver) {
203
+ [_keyboardObserver stop];
204
+ _keyboardObserver = nil;
205
+ }
206
+
207
+ _contentView.keyboardObserver = nil;
208
+ _footerView.keyboardObserver = nil;
170
209
  }
171
210
 
172
211
  @end
@@ -12,6 +12,7 @@
12
12
  #import <React/RCTViewComponentView.h>
13
13
  #import <UIKit/UIKit.h>
14
14
  #import <react/renderer/core/LayoutMetrics.h>
15
+ #import "core/TrueSheetKeyboardObserver.h"
15
16
 
16
17
  @class TrueSheetViewController;
17
18
  @class RCTScrollViewComponentView;
@@ -26,17 +27,20 @@ NS_ASSUME_NONNULL_BEGIN
26
27
 
27
28
  @end
28
29
 
29
- @interface TrueSheetContentView : RCTViewComponentView
30
+ @interface TrueSheetContentView : RCTViewComponentView <TrueSheetKeyboardObserverDelegate>
30
31
 
31
32
  @property (nonatomic, weak, nullable) id<TrueSheetContentViewDelegate> delegate;
33
+ @property (nonatomic, assign) CGFloat keyboardScrollOffset;
34
+ @property (nonatomic, weak, nullable) TrueSheetKeyboardObserver *keyboardObserver;
32
35
 
33
36
  - (RCTScrollViewComponentView *_Nullable)findScrollView:(UIView *_Nullable *_Nullable)outTopSibling;
34
37
 
35
38
  /**
36
39
  * Setup ScrollView pinning
37
40
  * @param pinned Whether to pin the scroll view
41
+ * @param bottomInset Bottom content inset for the scroll view
38
42
  */
39
- - (void)setupScrollViewPinning:(BOOL)pinned;
43
+ - (void)setupScrollViewPinning:(BOOL)pinned bottomInset:(CGFloat)bottomInset;
40
44
 
41
45
  @end
42
46