@lodev09/react-native-true-sheet 3.3.0-beta.0 → 3.3.0-beta.2

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 (33) hide show
  1. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +3 -6
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +41 -35
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +0 -9
  4. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +0 -14
  5. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetKeyboardHandler.kt +120 -0
  6. package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +9 -9
  7. package/ios/TrueSheetContainerView.h +10 -0
  8. package/ios/TrueSheetContainerView.mm +10 -0
  9. package/ios/TrueSheetFooterView.h +2 -0
  10. package/ios/TrueSheetFooterView.mm +65 -5
  11. package/ios/TrueSheetView.mm +2 -0
  12. package/ios/TrueSheetViewController.mm +0 -54
  13. package/lib/module/TrueSheet.js +0 -2
  14. package/lib/module/TrueSheet.js.map +1 -1
  15. package/lib/module/TrueSheet.web.js +57 -39
  16. package/lib/module/TrueSheet.web.js.map +1 -1
  17. package/lib/module/__mocks__/index.js +81 -0
  18. package/lib/module/__mocks__/index.js.map +1 -0
  19. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +1 -1
  20. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  21. package/lib/typescript/src/TrueSheet.types.d.ts +25 -5
  22. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  23. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  24. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +0 -1
  25. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  26. package/lib/typescript/src/navigation/types.d.ts +1 -1
  27. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  28. package/package.json +19 -12
  29. package/src/TrueSheet.tsx +1 -2
  30. package/src/TrueSheet.types.ts +26 -5
  31. package/src/TrueSheet.web.tsx +60 -39
  32. package/src/fabric/TrueSheetViewNativeComponent.ts +1 -1
  33. package/src/navigation/types.ts +2 -1
@@ -45,7 +45,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
45
45
  // Immediately update state with screen width during first state update
46
46
  // This ensures we have initial width for content layout before presenting
47
47
  if (field == null && value != null) {
48
- updateState(viewController.screenWidth, 0)
48
+ updateState(viewController.screenWidth, viewController.screenHeight)
49
49
  }
50
50
  field = value
51
51
  }
@@ -272,10 +272,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
272
272
  viewController.sheetBackgroundColor = color
273
273
  }
274
274
 
275
- fun setSoftInputMode(mode: Int) {
276
- viewController.setSoftInputMode(mode)
277
- }
278
-
279
275
  fun setDismissible(dismissible: Boolean) {
280
276
  viewController.dismissible = dismissible
281
277
  }
@@ -352,7 +348,8 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
352
348
  * Uses post to ensure all layout passes complete before reconfiguring.
353
349
  */
354
350
  fun updateSheetIfNeeded() {
355
- if (!viewController.isPresented || isSheetUpdatePending) return
351
+ if (!viewController.isPresented) return
352
+ if (isSheetUpdatePending) return
356
353
 
357
354
  isSheetUpdatePending = true
358
355
  viewController.post {
@@ -11,6 +11,7 @@ import android.view.View
11
11
  import android.view.WindowManager
12
12
  import android.view.accessibility.AccessibilityNodeInfo
13
13
  import android.widget.FrameLayout
14
+ import androidx.core.view.ViewCompat
14
15
  import androidx.core.view.isNotEmpty
15
16
  import androidx.core.view.isVisible
16
17
  import com.facebook.react.R
@@ -28,6 +29,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
28
29
  import com.lodev09.truesheet.core.GrabberOptions
29
30
  import com.lodev09.truesheet.core.RNScreensFragmentObserver
30
31
  import com.lodev09.truesheet.core.TrueSheetGrabberView
32
+ import com.lodev09.truesheet.core.TrueSheetKeyboardHandler
31
33
  import com.lodev09.truesheet.utils.ScreenUtils
32
34
 
33
35
  data class DetentInfo(val index: Int, val position: Float)
@@ -116,6 +118,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
116
118
  var currentDetentIndex: Int = -1
117
119
  private set
118
120
 
121
+ private var lastStateWidth: Int = 0
122
+ private var lastStateHeight: Int = 0
119
123
  private var isDragging = false
120
124
  private var isDismissing = false
121
125
  private var isReconfiguring = false
@@ -132,8 +136,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
132
136
  // MARK: - Configuration Properties
133
137
  // ====================================================================
134
138
 
135
- var screenHeight = 0
136
- var screenWidth = 0
139
+ val screenHeight: Int
140
+ get() = ScreenUtils.getScreenHeight(reactContext)
141
+ val screenWidth: Int
142
+ get() = ScreenUtils.getScreenWidth(reactContext)
143
+
137
144
  var maxSheetHeight: Int? = null
138
145
  var detents = mutableListOf(0.5, 1.0)
139
146
 
@@ -196,8 +203,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
196
203
  // ====================================================================
197
204
 
198
205
  init {
199
- screenHeight = ScreenUtils.getScreenHeight(reactContext)
200
- screenWidth = ScreenUtils.getScreenWidth(reactContext)
201
206
  jSPointerDispatcher = JSPointerDispatcher(this)
202
207
  }
203
208
 
@@ -219,6 +224,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
219
224
 
220
225
  window?.apply {
221
226
  windowAnimation = attributes.windowAnimations
227
+ // Disable default keyboard avoidance - sheet handles it via setupKeyboardHandler
228
+ setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
222
229
  }
223
230
 
224
231
  setupModalObserver()
@@ -248,6 +255,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
248
255
  setOnDismissListener(null)
249
256
  }
250
257
 
258
+ cleanupKeyboardHandler()
251
259
  cleanupModalObserver()
252
260
  sheetContainer?.removeView(this)
253
261
 
@@ -266,6 +274,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
266
274
  resetAnimation()
267
275
  setupBackground()
268
276
  setupGrabber()
277
+ setupKeyboardHandler()
269
278
 
270
279
  sheetContainer?.post {
271
280
  bottomSheetView?.let { emitChangePositionDelegate(it, realtime = false) }
@@ -370,26 +379,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
370
379
  private fun setupModalObserver() {
371
380
  rnScreensObserver = RNScreensFragmentObserver(
372
381
  reactContext = reactContext,
373
- onModalWillPresent = {
374
- if (isPresented) {
375
- delegate?.viewControllerWillBlur()
376
- }
377
- },
378
382
  onModalPresented = {
379
383
  if (isPresented) {
380
384
  hideDialog()
381
- delegate?.viewControllerDidBlur()
382
- }
383
- },
384
- onModalWillDismiss = {
385
- if (isPresented) {
386
- delegate?.viewControllerWillFocus()
387
385
  }
388
386
  },
389
387
  onModalDismissed = {
390
388
  if (isPresented) {
391
389
  showDialog()
392
- delegate?.viewControllerDidFocus()
393
390
  }
394
391
  }
395
392
  )
@@ -520,8 +517,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
520
517
  isFitToContents = false
521
518
  maxWidth = DEFAULT_MAX_WIDTH.dpToPx().toInt()
522
519
 
523
- val oldExpandOffset = expandedOffset
524
-
525
520
  val maxAvailableHeight = realHeight - edgeToEdgeTopInset
526
521
 
527
522
  setPeekHeight(getDetentHeight(detents[0]), isPresented)
@@ -539,10 +534,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
539
534
  expandedOffset = maxOf(edgeToEdgeTopInset, realHeight - maxDetentHeight)
540
535
  isFitToContents = detents.size < 3 && expandedOffset == 0
541
536
 
542
- if (oldExpandOffset != expandedOffset || expandedOffset == 0) {
543
- val offset = if (expandedOffset == 0) topInset else 0
544
- val newHeight = realHeight - expandedOffset - offset
545
- delegate?.viewControllerDidChangeSize(width, newHeight)
537
+ val offset = if (expandedOffset == 0) topInset else 0
538
+ val newHeight = realHeight - expandedOffset - offset
539
+ val newWidth = minOf(screenWidth, maxWidth)
540
+
541
+ if (lastStateWidth != newWidth || lastStateHeight != newHeight) {
542
+ lastStateWidth = newWidth
543
+ lastStateHeight = newHeight
544
+ delegate?.viewControllerDidChangeSize(newWidth, newHeight)
546
545
  }
547
546
 
548
547
  if (isPresented) {
@@ -569,6 +568,20 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
569
568
  bottomSheet.addView(grabberView)
570
569
  }
571
570
 
571
+ private var keyboardHandler: TrueSheetKeyboardHandler? = null
572
+
573
+ /** Sets up keyboard handler for IME transitions. */
574
+ fun setupKeyboardHandler() {
575
+ val bottomSheet = bottomSheetView ?: return
576
+ keyboardHandler = TrueSheetKeyboardHandler(bottomSheet, reactContext) { topInset }
577
+ keyboardHandler?.setup()
578
+ }
579
+
580
+ fun cleanupKeyboardHandler() {
581
+ keyboardHandler?.cleanup()
582
+ keyboardHandler = null
583
+ }
584
+
572
585
  fun setupBackground() {
573
586
  val bottomSheet = bottomSheetView ?: return
574
587
 
@@ -634,10 +647,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
634
647
  behavior?.state = getStateForDetentIndex(index)
635
648
  }
636
649
 
637
- fun setSoftInputMode(mode: Int) {
638
- dialog?.window?.setSoftInputMode(mode)
639
- }
640
-
641
650
  fun getDefaultBackgroundColor(): Int {
642
651
  val typedValue = TypedValue()
643
652
  return if (reactContext.theme.resolveAttribute(
@@ -883,22 +892,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
883
892
 
884
893
  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
885
894
  super.onSizeChanged(w, h, oldw, oldh)
895
+
886
896
  if (w == oldw && h == oldh) return
897
+ if (!isPresented) return
887
898
 
888
899
  // Skip continuous size changes when fullScreen + edge-to-edge
889
- if (h + topInset > screenHeight && isExpanded && oldw == w) {
900
+ if (h + topInset >= screenHeight && isExpanded && oldw == w) {
890
901
  return
891
902
  }
892
903
 
893
- val oldScreenHeight = screenHeight
894
- screenHeight = ScreenUtils.getScreenHeight(reactContext)
895
-
896
- if (isPresented && oldScreenHeight != screenHeight && oldScreenHeight > 0) {
904
+ this.post {
897
905
  setupSheetDetents()
898
- this.post {
899
- positionFooter()
900
- bottomSheetView?.let { emitChangePositionDelegate(it, realtime = false) }
901
- }
906
+ positionFooter()
907
+ bottomSheetView?.let { emitChangePositionDelegate(it, realtime = false) }
902
908
  }
903
909
  }
904
910
 
@@ -172,15 +172,6 @@ class TrueSheetViewManager :
172
172
  }
173
173
  }
174
174
 
175
- @ReactProp(name = "keyboardMode")
176
- override fun setKeyboardMode(view: TrueSheetView, mode: String?) {
177
- val softInputMode = when (mode) {
178
- "pan" -> WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
179
- else -> WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
180
- }
181
- view.setSoftInputMode(softInputMode)
182
- }
183
-
184
175
  @ReactProp(name = "blurTint")
185
176
  override fun setBlurTint(view: TrueSheetView, tint: String?) {
186
177
  // iOS-specific prop - no-op on Android
@@ -13,9 +13,7 @@ private const val RN_SCREENS_PACKAGE = "com.swmansion.rnscreens"
13
13
  */
14
14
  class RNScreensFragmentObserver(
15
15
  private val reactContext: ReactContext,
16
- private val onModalWillPresent: () -> Unit = {},
17
16
  private val onModalPresented: () -> Unit,
18
- private val onModalWillDismiss: () -> Unit = {},
19
17
  private val onModalDismissed: () -> Unit
20
18
  ) {
21
19
  private var fragmentLifecycleCallback: FragmentManager.FragmentLifecycleCallbacks? = null
@@ -33,14 +31,8 @@ class RNScreensFragmentObserver(
33
31
  super.onFragmentStarted(fm, fragment)
34
32
 
35
33
  if (isModalFragment(fragment) && !activeModalFragments.contains(fragment)) {
36
- // Notify willPresent before adding to active set
37
- if (activeModalFragments.isEmpty()) {
38
- onModalWillPresent()
39
- }
40
-
41
34
  activeModalFragments.add(fragment)
42
35
 
43
- // Notify didPresent after modal is started
44
36
  if (activeModalFragments.size == 1) {
45
37
  onModalPresented()
46
38
  }
@@ -51,14 +43,8 @@ class RNScreensFragmentObserver(
51
43
  super.onFragmentStopped(fm, fragment)
52
44
 
53
45
  if (activeModalFragments.contains(fragment)) {
54
- // Notify willDismiss before removing from active set
55
- if (activeModalFragments.size == 1) {
56
- onModalWillDismiss()
57
- }
58
-
59
46
  activeModalFragments.remove(fragment)
60
47
 
61
- // Notify didDismiss after all modals are dismissed
62
48
  if (activeModalFragments.isEmpty()) {
63
49
  onModalDismissed()
64
50
  }
@@ -0,0 +1,120 @@
1
+ package com.lodev09.truesheet.core
2
+
3
+ import android.graphics.Rect
4
+ import android.os.Build
5
+ import android.view.View
6
+ import android.view.ViewTreeObserver
7
+ import androidx.core.view.ViewCompat
8
+ import androidx.core.view.WindowInsetsAnimationCompat
9
+ import androidx.core.view.WindowInsetsCompat
10
+ import com.facebook.react.uimanager.ThemedReactContext
11
+
12
+ /**
13
+ * Handles keyboard (IME) for sheet translation.
14
+ * Uses WindowInsetsAnimationCompat for smooth animation on API 30+,
15
+ * falls back to ViewTreeObserver on Activity's decor view for API 29 and below.
16
+ *
17
+ * @param targetView The view to translate (typically the bottom sheet)
18
+ * @param reactContext The React context to get the current activity
19
+ * @param topInset The top safe area inset to respect
20
+ */
21
+ class TrueSheetKeyboardHandler(
22
+ private val targetView: View,
23
+ private val reactContext: ThemedReactContext,
24
+ private val topInset: () -> Int
25
+ ) {
26
+
27
+ private var globalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
28
+ private var activityRootView: View? = null
29
+
30
+ fun setup() {
31
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
32
+ setupAnimationCallback()
33
+ } else {
34
+ setupLegacyListener()
35
+ }
36
+ }
37
+
38
+ fun cleanup() {
39
+ globalLayoutListener?.let { listener ->
40
+ activityRootView?.viewTreeObserver?.removeOnGlobalLayoutListener(listener)
41
+ globalLayoutListener = null
42
+ activityRootView = null
43
+ }
44
+ ViewCompat.setWindowInsetsAnimationCallback(targetView, null)
45
+ }
46
+
47
+ private fun applyTranslation(imeHeight: Int) {
48
+ // Cap translation so sheet doesn't move beyond screen bounds
49
+ val maxTranslation = maxOf(0, targetView.top - topInset())
50
+ val translation = minOf(imeHeight, maxTranslation)
51
+ targetView.translationY = -translation.toFloat()
52
+ }
53
+
54
+ /** API 30+ smooth keyboard animation */
55
+ private fun setupAnimationCallback() {
56
+ ViewCompat.setWindowInsetsAnimationCallback(
57
+ targetView,
58
+ object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
59
+ private var startImeHeight = 0
60
+ private var endImeHeight = 0
61
+
62
+ private fun getKeyboardHeight(rootInsets: WindowInsetsCompat?): Int {
63
+ if (rootInsets == null) return 0
64
+ return rootInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
65
+ }
66
+
67
+ override fun onPrepare(animation: WindowInsetsAnimationCompat) {
68
+ startImeHeight = getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView))
69
+ }
70
+
71
+ override fun onStart(
72
+ animation: WindowInsetsAnimationCompat,
73
+ bounds: WindowInsetsAnimationCompat.BoundsCompat
74
+ ): WindowInsetsAnimationCompat.BoundsCompat {
75
+ endImeHeight = getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView))
76
+ return bounds
77
+ }
78
+
79
+ override fun onProgress(insets: WindowInsetsCompat, runningAnimations: List<WindowInsetsAnimationCompat>): WindowInsetsCompat {
80
+ val imeAnimation = runningAnimations.find {
81
+ it.typeMask and WindowInsetsCompat.Type.ime() != 0
82
+ } ?: return insets
83
+
84
+ val fraction = imeAnimation.interpolatedFraction
85
+ val currentImeHeight = (startImeHeight + (endImeHeight - startImeHeight) * fraction).toInt()
86
+ applyTranslation(currentImeHeight)
87
+
88
+ return insets
89
+ }
90
+
91
+ override fun onEnd(animation: WindowInsetsAnimationCompat) {
92
+ applyTranslation(getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView)))
93
+ }
94
+ }
95
+ )
96
+ }
97
+
98
+ /** API 29 and below fallback using ViewTreeObserver on Activity's root view */
99
+ private fun setupLegacyListener() {
100
+ val rootView = reactContext.currentActivity?.window?.decorView?.rootView ?: return
101
+
102
+ activityRootView = rootView
103
+
104
+ globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
105
+ val rect = Rect()
106
+ rootView.getWindowVisibleDisplayFrame(rect)
107
+
108
+ val screenHeight = rootView.height
109
+ val keyboardHeight = screenHeight - rect.bottom
110
+
111
+ if (keyboardHeight > screenHeight * 0.15) {
112
+ applyTranslation(keyboardHeight)
113
+ } else {
114
+ applyTranslation(0)
115
+ }
116
+ }
117
+
118
+ rootView.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
119
+ }
120
+ }
@@ -67,15 +67,6 @@ object ScreenUtils {
67
67
  } ?: Insets(0, 0)
68
68
  }
69
69
 
70
- /**
71
- * Calculate the screen height using the same method as React Native's useWindowDimensions.
72
- * This returns the window height which automatically accounts for edge-to-edge mode.
73
- *
74
- * @param reactContext The ReactContext to get resources from
75
- * @return Screen height in pixels
76
- */
77
- fun getScreenHeight(reactContext: ReactContext): Int = reactContext.resources.displayMetrics.heightPixels
78
-
79
70
  /**
80
71
  * Get the real physical device screen height, including system bars.
81
72
  * This is consistent across all API levels.
@@ -103,6 +94,15 @@ object ScreenUtils {
103
94
  */
104
95
  fun getScreenWidth(reactContext: ReactContext): Int = reactContext.resources.displayMetrics.widthPixels
105
96
 
97
+ /**
98
+ * Calculate the screen height using the same method as React Native's useWindowDimensions.
99
+ * This returns the window height which automatically accounts for edge-to-edge mode.
100
+ *
101
+ * @param reactContext The ReactContext to get resources from
102
+ * @return Screen height in pixels
103
+ */
104
+ fun getScreenHeight(reactContext: ReactContext): Int = reactContext.resources.displayMetrics.heightPixels
105
+
106
106
  /**
107
107
  * Get the location of a view in screen coordinates
108
108
  *
@@ -63,6 +63,16 @@ NS_ASSUME_NONNULL_BEGIN
63
63
  */
64
64
  - (void)setupContentScrollViewPinning;
65
65
 
66
+ /**
67
+ * Setup keyboard handler for footer
68
+ */
69
+ - (void)setupKeyboardHandler;
70
+
71
+ /**
72
+ * Cleanup keyboard handler for footer
73
+ */
74
+ - (void)cleanupKeyboardHandler;
75
+
66
76
  @end
67
77
 
68
78
  NS_ASSUME_NONNULL_END
@@ -179,6 +179,16 @@ using namespace facebook::react;
179
179
  }
180
180
  }
181
181
 
182
+ #pragma mark - Keyboard Handling
183
+
184
+ - (void)setupKeyboardHandler {
185
+ [_footerView setupKeyboardHandler];
186
+ }
187
+
188
+ - (void)cleanupKeyboardHandler {
189
+ [_footerView cleanupKeyboardHandler];
190
+ }
191
+
182
192
  @end
183
193
 
184
194
  Class<RCTComponentViewProtocol> TrueSheetContainerViewCls(void) {
@@ -19,6 +19,8 @@ NS_ASSUME_NONNULL_BEGIN
19
19
  @interface TrueSheetFooterView : RCTViewComponentView
20
20
 
21
21
  - (void)setupConstraintsWithHeight:(CGFloat)height;
22
+ - (void)setupKeyboardHandler;
23
+ - (void)cleanupKeyboardHandler;
22
24
 
23
25
  @end
24
26
 
@@ -21,6 +21,7 @@ using namespace facebook::react;
21
21
  @implementation TrueSheetFooterView {
22
22
  CGFloat _lastHeight;
23
23
  BOOL _didInitialLayout;
24
+ NSLayoutConstraint *_bottomConstraint;
24
25
  }
25
26
 
26
27
  + (ComponentDescriptorProvider)componentDescriptorProvider {
@@ -37,6 +38,7 @@ using namespace facebook::react;
37
38
 
38
39
  _lastHeight = 0;
39
40
  _didInitialLayout = NO;
41
+ _bottomConstraint = nil;
40
42
  }
41
43
  return self;
42
44
  }
@@ -49,12 +51,22 @@ using namespace facebook::react;
49
51
 
50
52
  // Remove existing constraints before applying new ones
51
53
  [LayoutUtil unpinView:self fromParentView:parentView];
54
+ _bottomConstraint = nil;
52
55
 
53
- // Pin footer to bottom and sides of container with specific height
54
- [LayoutUtil pinView:self
55
- toParentView:parentView
56
- edges:UIRectEdgeLeft | UIRectEdgeRight | UIRectEdgeBottom
57
- height:height];
56
+ self.translatesAutoresizingMaskIntoConstraints = NO;
57
+
58
+ // Pin footer to sides of container
59
+ [self.leadingAnchor constraintEqualToAnchor:parentView.leadingAnchor].active = YES;
60
+ [self.trailingAnchor constraintEqualToAnchor:parentView.trailingAnchor].active = YES;
61
+
62
+ // Store bottom constraint for keyboard adjustment
63
+ _bottomConstraint = [self.bottomAnchor constraintEqualToAnchor:parentView.bottomAnchor];
64
+ _bottomConstraint.active = YES;
65
+
66
+ // Apply height constraint
67
+ if (height > 0) {
68
+ [self.heightAnchor constraintEqualToConstant:height].active = YES;
69
+ }
58
70
 
59
71
  _lastHeight = height;
60
72
  }
@@ -89,11 +101,59 @@ using namespace facebook::react;
89
101
  - (void)prepareForRecycle {
90
102
  [super prepareForRecycle];
91
103
 
104
+ [self cleanupKeyboardHandler];
105
+
92
106
  // Remove footer constraints
93
107
  [LayoutUtil unpinView:self fromParentView:self.superview];
94
108
 
95
109
  _lastHeight = 0;
96
110
  _didInitialLayout = NO;
111
+ _bottomConstraint = nil;
112
+ }
113
+
114
+ #pragma mark - Keyboard Handling
115
+
116
+ - (void)setupKeyboardHandler {
117
+ [[NSNotificationCenter defaultCenter] addObserver:self
118
+ selector:@selector(keyboardWillChangeFrame:)
119
+ name:UIKeyboardWillChangeFrameNotification
120
+ object:nil];
121
+ }
122
+
123
+ - (void)cleanupKeyboardHandler {
124
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillChangeFrameNotification object:nil];
125
+ }
126
+
127
+ - (void)keyboardWillChangeFrame:(NSNotification *)notification {
128
+ if (!_bottomConstraint) {
129
+ return;
130
+ }
131
+
132
+ NSDictionary *userInfo = notification.userInfo;
133
+ CGRect keyboardFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
134
+ NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
135
+ UIViewAnimationOptions curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue] << 16;
136
+
137
+ // Convert keyboard frame to window coordinates
138
+ UIWindow *window = self.window;
139
+ if (!window) {
140
+ return;
141
+ }
142
+
143
+ CGRect keyboardFrameInWindow = [window convertRect:keyboardFrame fromWindow:nil];
144
+ CGFloat keyboardHeight = window.bounds.size.height - keyboardFrameInWindow.origin.y;
145
+
146
+ // Cap to ensure we don't go negative
147
+ CGFloat bottomOffset = MAX(0, keyboardHeight);
148
+
149
+ [UIView animateWithDuration:duration
150
+ delay:0
151
+ options:curve | UIViewAnimationOptionBeginFromCurrentState
152
+ animations:^{
153
+ self->_bottomConstraint.constant = -bottomOffset;
154
+ [self.superview layoutIfNeeded];
155
+ }
156
+ completion:nil];
97
157
  }
98
158
 
99
159
  @end
@@ -427,6 +427,7 @@ using namespace facebook::react;
427
427
  }
428
428
 
429
429
  - (void)viewControllerDidPresentAtIndex:(NSInteger)index position:(CGFloat)position detent:(CGFloat)detent {
430
+ [_containerView setupKeyboardHandler];
430
431
  [TrueSheetLifecycleEvents emitDidPresent:_eventEmitter index:index position:position detent:detent];
431
432
  }
432
433
 
@@ -451,6 +452,7 @@ using namespace facebook::react;
451
452
  }
452
453
 
453
454
  - (void)viewControllerWillDismiss {
455
+ [_containerView cleanupKeyboardHandler];
454
456
  [TrueSheetLifecycleEvents emitWillDismiss:_eventEmitter];
455
457
  }
456
458
 
@@ -38,7 +38,6 @@
38
38
  TrueSheetGrabberView *_grabberView;
39
39
 
40
40
  NSMutableArray<NSNumber *> *_resolvedDetentPositions;
41
- BOOL _hasPresentedController;
42
41
  }
43
42
 
44
43
  #pragma mark - Initialization
@@ -67,7 +66,6 @@
67
66
  _blurInteraction = YES;
68
67
  _insetAdjustment = @"automatic";
69
68
  _resolvedDetentPositions = [NSMutableArray array];
70
- _hasPresentedController = NO;
71
69
  }
72
70
  return self;
73
71
  }
@@ -317,58 +315,6 @@
317
315
  _isTrackingPositionFromLayout = NO;
318
316
  }
319
317
 
320
- #pragma mark - Presentation Tracking (RN Screens)
321
-
322
- - (void)presentViewController:(UIViewController *)viewControllerToPresent
323
- animated:(BOOL)flag
324
- completion:(void (^)(void))completion {
325
- BOOL isExternalController = ![viewControllerToPresent isKindOfClass:[TrueSheetViewController class]];
326
-
327
- if (isExternalController && !_hasPresentedController) {
328
- _hasPresentedController = YES;
329
- if ([self.delegate respondsToSelector:@selector(viewControllerWillBlur)]) {
330
- [self.delegate viewControllerWillBlur];
331
- }
332
- }
333
-
334
- [super presentViewController:viewControllerToPresent
335
- animated:flag
336
- completion:^{
337
- if (isExternalController && self->_hasPresentedController) {
338
- if ([self.delegate respondsToSelector:@selector(viewControllerDidBlur)]) {
339
- [self.delegate viewControllerDidBlur];
340
- }
341
- }
342
- if (completion) {
343
- completion();
344
- }
345
- }];
346
- }
347
-
348
- - (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
349
- UIViewController *presented = self.presentedViewController;
350
- BOOL isExternalController = presented && ![presented isKindOfClass:[TrueSheetViewController class]];
351
-
352
- if (isExternalController && _hasPresentedController) {
353
- if ([self.delegate respondsToSelector:@selector(viewControllerWillFocus)]) {
354
- [self.delegate viewControllerWillFocus];
355
- }
356
- }
357
-
358
- [super dismissViewControllerAnimated:flag
359
- completion:^{
360
- if (isExternalController && self->_hasPresentedController) {
361
- self->_hasPresentedController = NO;
362
- if ([self.delegate respondsToSelector:@selector(viewControllerDidFocus)]) {
363
- [self.delegate viewControllerDidFocus];
364
- }
365
- }
366
- if (completion) {
367
- completion();
368
- }
369
- }];
370
- }
371
-
372
318
  #pragma mark - Position & Gesture Handling
373
319
 
374
320
  - (TrueSheetContentView *)findContentView:(UIView *)view {
@@ -278,7 +278,6 @@ export class TrueSheet extends PureComponent {
278
278
  dimmed = true,
279
279
  initialDetentIndex = -1,
280
280
  initialDetentAnimated = true,
281
- keyboardMode = 'resize',
282
281
  dimmedDetentIndex,
283
282
  blurTint,
284
283
  blurOptions,
@@ -327,7 +326,6 @@ export class TrueSheet extends PureComponent {
327
326
  },
328
327
  dimmed: dimmed,
329
328
  dimmedDetentIndex: dimmedDetentIndex,
330
- keyboardMode: keyboardMode,
331
329
  initialDetentIndex: initialDetentIndex,
332
330
  initialDetentAnimated: initialDetentAnimated,
333
331
  dismissible: dismissible,