@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.
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +3 -6
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +41 -35
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +0 -9
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +0 -14
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetKeyboardHandler.kt +120 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +9 -9
- package/ios/TrueSheetContainerView.h +10 -0
- package/ios/TrueSheetContainerView.mm +10 -0
- package/ios/TrueSheetFooterView.h +2 -0
- package/ios/TrueSheetFooterView.mm +65 -5
- package/ios/TrueSheetView.mm +2 -0
- package/ios/TrueSheetViewController.mm +0 -54
- package/lib/module/TrueSheet.js +0 -2
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +57 -39
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/__mocks__/index.js +81 -0
- package/lib/module/__mocks__/index.js.map +1 -0
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +1 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +25 -5
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +0 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +1 -1
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- package/package.json +19 -12
- package/src/TrueSheet.tsx +1 -2
- package/src/TrueSheet.types.ts +26 -5
- package/src/TrueSheet.web.tsx +60 -39
- package/src/fabric/TrueSheetViewNativeComponent.ts +1 -1
- 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,
|
|
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
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
|
900
|
+
if (h + topInset >= screenHeight && isExpanded && oldw == w) {
|
|
890
901
|
return
|
|
891
902
|
}
|
|
892
903
|
|
|
893
|
-
|
|
894
|
-
screenHeight = ScreenUtils.getScreenHeight(reactContext)
|
|
895
|
-
|
|
896
|
-
if (isPresented && oldScreenHeight != screenHeight && oldScreenHeight > 0) {
|
|
904
|
+
this.post {
|
|
897
905
|
setupSheetDetents()
|
|
898
|
-
|
|
899
|
-
|
|
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) {
|
|
@@ -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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
package/ios/TrueSheetView.mm
CHANGED
|
@@ -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 {
|
package/lib/module/TrueSheet.js
CHANGED
|
@@ -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,
|