@lodev09/react-native-true-sheet 3.11.10 → 4.0.0-beta.1
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/README.md +6 -5
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +23 -2
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +116 -26
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +7 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +48 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterViewManager.kt +7 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +45 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +28 -12
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +99 -34
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +9 -5
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +12 -3
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +33 -4
- package/android/src/main/jni/TrueSheetSpec.h +2 -0
- package/android/src/main/jni/TrueSheetStateUpdater.cpp +96 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +24 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +43 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +28 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +11 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +37 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +24 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +56 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +28 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +11 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +41 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +18 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +16 -0
- package/ios/TrueSheetContainerView.h +12 -10
- package/ios/TrueSheetContainerView.mm +11 -29
- package/ios/TrueSheetContentView.h +22 -7
- package/ios/TrueSheetContentView.mm +143 -41
- package/ios/TrueSheetFooterView.h +12 -2
- package/ios/TrueSheetFooterView.mm +74 -71
- package/ios/TrueSheetHeaderView.mm +0 -1
- package/ios/TrueSheetView.mm +60 -27
- package/ios/TrueSheetViewController.h +9 -0
- package/ios/TrueSheetViewController.mm +288 -122
- package/ios/core/TrueSheetDetentCalculator.h +9 -0
- package/ios/core/TrueSheetDetentCalculator.mm +13 -4
- package/ios/tvos/TrueSheetStubs.mm +2 -0
- package/lib/module/TrueSheet.js +32 -13
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +262 -38
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
- package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +8 -1
- package/lib/module/web/vaul/index.js +17 -6
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +35 -11
- 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/TrueSheetContentViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +2 -0
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +5 -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/lib/typescript/src/web/vaul/index.d.ts +8 -1
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/package.json +10 -10
- package/react-native.config.js +5 -1
- package/src/TrueSheet.tsx +45 -14
- package/src/TrueSheet.types.ts +38 -12
- package/src/TrueSheet.web.tsx +314 -44
- package/src/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
- package/src/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
- package/src/fabric/TrueSheetViewNativeComponent.ts +8 -1
- package/src/navigation/types.ts +3 -1
- package/src/web/vaul/index.tsx +23 -5
- package/ios/utils/LayoutUtil.h +0 -77
- package/ios/utils/LayoutUtil.mm +0 -106
|
@@ -218,13 +218,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
218
218
|
override var sheetBackgroundColor: Int? = null
|
|
219
219
|
var insetAdjustment: TrueSheetInsetAdjustment = TrueSheetInsetAdjustment.AUTOMATIC
|
|
220
220
|
|
|
221
|
-
var scrollable: Boolean = false
|
|
222
|
-
|
|
223
221
|
var scrollableOptions: ScrollableOptions? = null
|
|
224
222
|
set(value) {
|
|
225
223
|
field = value
|
|
226
224
|
behavior?.scrollingExpandsSheet = value?.scrollingExpandsSheet ?: true
|
|
227
|
-
if (isPresented) sheetView?.let { updateScrollExpansionPadding(it.top) }
|
|
228
225
|
}
|
|
229
226
|
|
|
230
227
|
override var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
|
|
@@ -287,9 +284,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
287
284
|
override val headerHeight: Int
|
|
288
285
|
get() = containerView?.headerHeight ?: cachedHeaderHeight
|
|
289
286
|
|
|
287
|
+
override var absoluteHeader: Boolean = false
|
|
288
|
+
|
|
290
289
|
override val footerHeight: Int
|
|
291
290
|
get() = containerView?.footerHeight ?: cachedFooterHeight
|
|
292
291
|
|
|
292
|
+
override var absoluteFooter: Boolean = false
|
|
293
|
+
|
|
293
294
|
override val peekContentHeight: Int
|
|
294
295
|
get() = containerView?.peekContentHeight ?: cachedPeekContentHeight
|
|
295
296
|
|
|
@@ -564,29 +565,20 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
564
565
|
else -> { }
|
|
565
566
|
}
|
|
566
567
|
|
|
567
|
-
updateScrollExpansionPadding(sheetView.top)
|
|
568
568
|
emitChangePositionDelegate(sheetView.top)
|
|
569
569
|
|
|
570
|
-
//
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
570
|
+
// Position on every slide frame — during keyboard transitions the IME
|
|
571
|
+
// animation callbacks are sparser than the sheet's settle frames, and the
|
|
572
|
+
// footer rides the sheet between them, jittering against the keyboard.
|
|
573
|
+
// positionFooter derives from current state, so double-driving it with
|
|
574
|
+
// keyboardDidChangeHeight is harmless.
|
|
575
|
+
positionFooter(slideOffset)
|
|
575
576
|
|
|
576
577
|
if (!isKeyboardTransitioning) {
|
|
577
578
|
updateDimAmount(sheetView.top)
|
|
578
579
|
}
|
|
579
580
|
}
|
|
580
581
|
|
|
581
|
-
private fun updateScrollExpansionPadding(sheetTop: Int) {
|
|
582
|
-
if (!scrollable) {
|
|
583
|
-
containerView?.contentView?.updateScrollExpansionPadding(0)
|
|
584
|
-
return
|
|
585
|
-
}
|
|
586
|
-
val expandedOffset = behavior?.expandedOffset ?: return
|
|
587
|
-
containerView?.contentView?.updateScrollExpansionPadding(maxOf(0, sheetTop - expandedOffset))
|
|
588
|
-
}
|
|
589
|
-
|
|
590
582
|
private fun handleStateSettled(sheetView: View, newState: Int) {
|
|
591
583
|
if (interactionState is InteractionState.Reconfiguring) return
|
|
592
584
|
|
|
@@ -634,6 +626,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
634
626
|
}
|
|
635
627
|
}
|
|
636
628
|
|
|
629
|
+
// Settled — resize the container to the settled detent (applies deferred shrinks)
|
|
630
|
+
updateStateDimensions()
|
|
631
|
+
|
|
637
632
|
if (!isKeyboardTransitioning) {
|
|
638
633
|
updateDimAmount(animated = true)
|
|
639
634
|
}
|
|
@@ -743,6 +738,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
743
738
|
}
|
|
744
739
|
|
|
745
740
|
pendingDetentIndex = detentIndex
|
|
741
|
+
updateStateDimensions(deferShrink = true)
|
|
746
742
|
setupDimmedBackground()
|
|
747
743
|
setStateForDetentIndex(detentIndex)
|
|
748
744
|
resizePromise?.invoke()
|
|
@@ -893,7 +889,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
893
889
|
animate = isPresented
|
|
894
890
|
)
|
|
895
891
|
|
|
896
|
-
updateStateDimensions(
|
|
892
|
+
updateStateDimensions()
|
|
897
893
|
|
|
898
894
|
if (isPresented && applyState) {
|
|
899
895
|
// Prefer the pending target while a resize animation is in flight so a
|
|
@@ -923,6 +919,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
923
919
|
}
|
|
924
920
|
|
|
925
921
|
fun setupSheetDetentsForSizeChange() {
|
|
922
|
+
// Skip while dragging — the size change is driven by the drag itself (the
|
|
923
|
+
// container tracks the sheet's visible height), and reconfiguring resets
|
|
924
|
+
// behavior state, killing the gesture.
|
|
925
|
+
if (interactionState is InteractionState.Dragging) return
|
|
926
|
+
|
|
926
927
|
setupSheetDetents()
|
|
927
928
|
positionFooter()
|
|
928
929
|
}
|
|
@@ -1007,15 +1008,28 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1007
1008
|
val footerView = containerView?.footerView ?: return
|
|
1008
1009
|
val sheet = sheetView ?: return
|
|
1009
1010
|
|
|
1011
|
+
val keyboardShift = if (currentKeyboardInset > 0) maxOf(0, currentKeyboardInset + footerKeyboardOffset) else 0
|
|
1012
|
+
|
|
1013
|
+
// A relative footer is laid out by Yoga (pinned to the container's bottom
|
|
1014
|
+
// edge) and stays in the layout flow behind the keyboard — only an
|
|
1015
|
+
// absolute footer floats above it
|
|
1016
|
+
if (!absoluteFooter) {
|
|
1017
|
+
footerView.translationY = 0f
|
|
1018
|
+
return
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1010
1021
|
val footerHeight = footerView.height
|
|
1011
1022
|
val sheetHeight = sheet.height
|
|
1012
1023
|
val sheetTop = sheet.top
|
|
1013
1024
|
|
|
1014
|
-
val keyboardShift = if (currentKeyboardInset > 0) maxOf(0, currentKeyboardInset + footerKeyboardOffset) else 0
|
|
1015
1025
|
var footerY = (sheetHeight - sheetTop - footerHeight - keyboardShift).toFloat()
|
|
1016
1026
|
|
|
1017
|
-
// Adjust during dismiss animation when slideOffset is negative
|
|
1018
|
-
|
|
1027
|
+
// Adjust during dismiss animation when slideOffset is negative. Skipped
|
|
1028
|
+
// while the keyboard is open — the detents collapse to the expanded
|
|
1029
|
+
// position, so any downward drag reads as a negative slideOffset and the
|
|
1030
|
+
// adjustment would sink the footer behind the keyboard, snapping back up
|
|
1031
|
+
// when the keyboard detents reset on release.
|
|
1032
|
+
if (slideOffset != null && slideOffset < 0 && keyboardShift == 0) {
|
|
1019
1033
|
footerY -= (footerHeight * slideOffset)
|
|
1020
1034
|
}
|
|
1021
1035
|
|
|
@@ -1062,17 +1076,22 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1062
1076
|
delegate = object : TrueSheetKeyboardObserverDelegate {
|
|
1063
1077
|
override fun keyboardWillShow(height: Int) {
|
|
1064
1078
|
if (!shouldHandleKeyboard()) return
|
|
1079
|
+
// An absolute footer rises above the keyboard, so it drops the bottom
|
|
1080
|
+
// inset padding — before detents are configured against its height
|
|
1081
|
+
if (absoluteFooter) containerView?.footerView?.keyboardVisible = true
|
|
1065
1082
|
// If a resize is in flight, restore to its target — not the stale current
|
|
1066
1083
|
detentIndexBeforeKeyboard = if (pendingDetentIndex >= 0) pendingDetentIndex else currentDetentIndex
|
|
1067
1084
|
pendingDetentIndex = -1
|
|
1068
|
-
|
|
1085
|
+
// Commit the target index first so the container grows before the sheet expands
|
|
1069
1086
|
currentDetentIndex = detents.size - 1
|
|
1070
|
-
|
|
1087
|
+
setupSheetDetents()
|
|
1088
|
+
positionFooter()
|
|
1071
1089
|
updateDimAmount(animated = true)
|
|
1072
1090
|
}
|
|
1073
1091
|
|
|
1074
1092
|
override fun keyboardWillHide() {
|
|
1075
1093
|
if (!shouldHandleKeyboard(checkFocus = false)) return
|
|
1094
|
+
containerView?.footerView?.keyboardVisible = false
|
|
1076
1095
|
val restoring = !isBeingDismissed && detentIndexBeforeKeyboard >= 0
|
|
1077
1096
|
|
|
1078
1097
|
// Skip reconfigure during interactive keyboard dismiss (e.g. keyboardDismissMode="on-drag")
|
|
@@ -1085,6 +1104,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1085
1104
|
}
|
|
1086
1105
|
}
|
|
1087
1106
|
|
|
1107
|
+
// Same-frame reposition — the reconfigure above shrinks the container
|
|
1108
|
+
// to the target detent, which yanks the bottom-pinned footer up until
|
|
1109
|
+
// the next keyboard animation frame repositions it.
|
|
1110
|
+
positionFooter()
|
|
1088
1111
|
updateDimAmount(
|
|
1089
1112
|
sheetTop = detentCalculator.getSheetTopForDetentIndex(currentDetentIndex),
|
|
1090
1113
|
animated = true
|
|
@@ -1093,6 +1116,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1093
1116
|
|
|
1094
1117
|
override fun keyboardDidHide() {
|
|
1095
1118
|
if (!shouldHandleKeyboard(checkFocus = false)) return
|
|
1119
|
+
containerView?.footerView?.keyboardVisible = false
|
|
1096
1120
|
detentIndexBeforeKeyboard = -1
|
|
1097
1121
|
isKeyboardDismissProgrammatic = false
|
|
1098
1122
|
setupSheetDetents(applyState = false)
|
|
@@ -1114,9 +1138,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1114
1138
|
// Handle case where keyboard is already visible and focus moves into the sheet
|
|
1115
1139
|
if (!shouldHandleKeyboard()) return
|
|
1116
1140
|
if (detentIndexBeforeKeyboard < 0 && (keyboardObserver?.currentHeight ?: 0) > 0) {
|
|
1141
|
+
if (absoluteFooter) containerView?.footerView?.keyboardVisible = true
|
|
1117
1142
|
detentIndexBeforeKeyboard = currentDetentIndex
|
|
1118
1143
|
currentDetentIndex = detents.size - 1
|
|
1119
1144
|
setupSheetDetents()
|
|
1145
|
+
positionFooter()
|
|
1120
1146
|
}
|
|
1121
1147
|
}
|
|
1122
1148
|
}
|
|
@@ -1185,6 +1211,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1185
1211
|
private fun handleDragChange(sheetView: View) {
|
|
1186
1212
|
if (interactionState !is InteractionState.Dragging) return
|
|
1187
1213
|
|
|
1214
|
+
updateStateDimensionsForDrag(sheetView.top)
|
|
1215
|
+
|
|
1188
1216
|
val position = getPositionDpForView(sheetView)
|
|
1189
1217
|
val detent = detentCalculator.getDetentValueForIndex(currentDetentIndex)
|
|
1190
1218
|
delegate?.viewControllerDidDragChange(currentDetentIndex, position, detent)
|
|
@@ -1253,19 +1281,56 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1253
1281
|
behavior.maxWidth = newMaxWidth
|
|
1254
1282
|
}
|
|
1255
1283
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1284
|
+
/**
|
|
1285
|
+
* Updates the Fabric state with the current/target detent size so Yoga sizes
|
|
1286
|
+
* the container to the sheet's visible height.
|
|
1287
|
+
*
|
|
1288
|
+
* Unlike iOS, resize animations run over multiple frames (ViewDragHelper), so
|
|
1289
|
+
* shrinking is deferred to settle ([deferShrink]) when a resize animation is
|
|
1290
|
+
* about to run — growing applies immediately so content is laid out before
|
|
1291
|
+
* the sheet reveals it.
|
|
1292
|
+
*/
|
|
1293
|
+
private fun updateStateDimensions(deferShrink: Boolean = false) {
|
|
1294
|
+
if (detents.isEmpty()) return
|
|
1295
|
+
|
|
1296
|
+
val targetIndex = (if (pendingDetentIndex >= 0) pendingDetentIndex else currentDetentIndex)
|
|
1297
|
+
.coerceIn(0, detents.size - 1)
|
|
1298
|
+
val maxAvailableHeight = realScreenHeight - topInset
|
|
1299
|
+
val newHeight = minOf(detentCalculator.getDetentHeight(detents[targetIndex]), maxAvailableHeight)
|
|
1300
|
+
val newWidth = getStateWidth()
|
|
1301
|
+
|
|
1302
|
+
if (deferShrink && newWidth == lastStateWidth && newHeight < lastStateHeight) return
|
|
1303
|
+
|
|
1304
|
+
setStateDimensions(newWidth, newHeight)
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Tracks the sheet's visible height during drag (and the post-release settle
|
|
1309
|
+
* animation) so the container resizes with the finger, like iOS. Clamped to the
|
|
1310
|
+
* smallest detent — dragging below it slides the sheet out without resizing.
|
|
1311
|
+
*/
|
|
1312
|
+
private fun updateStateDimensionsForDrag(sheetTop: Int) {
|
|
1313
|
+
if (detents.isEmpty()) return
|
|
1314
|
+
|
|
1315
|
+
val maxAvailableHeight = realScreenHeight - topInset
|
|
1316
|
+
val minHeight = minOf(detentCalculator.getDetentHeight(detents.first()), maxAvailableHeight)
|
|
1317
|
+
val newHeight = detentCalculator.getVisibleSheetHeight(sheetTop).coerceIn(minHeight, maxAvailableHeight)
|
|
1318
|
+
|
|
1319
|
+
setStateDimensions(getStateWidth(), newHeight)
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
private fun getStateWidth(): Int {
|
|
1260
1323
|
val applyMaxWidth = maxContentWidth != null && !ScreenUtils.isPortraitPhone(reactContext)
|
|
1261
1324
|
val effectiveMaxWidth = if (applyMaxWidth) maxContentWidth!! else DEFAULT_MAX_WIDTH.dpToPx().toInt()
|
|
1262
|
-
|
|
1325
|
+
return minOf(screenWidth, effectiveMaxWidth)
|
|
1326
|
+
}
|
|
1263
1327
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1328
|
+
private fun setStateDimensions(width: Int, height: Int) {
|
|
1329
|
+
if (lastStateWidth == width && lastStateHeight == height) return
|
|
1330
|
+
|
|
1331
|
+
lastStateWidth = width
|
|
1332
|
+
lastStateHeight = height
|
|
1333
|
+
delegate?.viewControllerDidChangeSize(width, height)
|
|
1269
1334
|
}
|
|
1270
1335
|
|
|
1271
1336
|
fun translateSheet(translationY: Int, onEnd: (() -> Unit)? = null) {
|
|
@@ -221,11 +221,6 @@ class TrueSheetViewManager :
|
|
|
221
221
|
view.setInsetAdjustment(insetAdjustment ?: "automatic")
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
@ReactProp(name = "scrollable", defaultBoolean = false)
|
|
225
|
-
override fun setScrollable(view: TrueSheetView, value: Boolean) {
|
|
226
|
-
view.setScrollable(value)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
224
|
@ReactProp(name = "presentation")
|
|
230
225
|
override fun setPresentation(view: TrueSheetView, value: String?) {
|
|
231
226
|
// iOS/web-specific prop - no-op on Android
|
|
@@ -250,8 +245,17 @@ class TrueSheetViewManager :
|
|
|
250
245
|
view.setScrollableOptions(scrollableOptions)
|
|
251
246
|
}
|
|
252
247
|
|
|
248
|
+
@ReactProp(name = "headerOptions")
|
|
249
|
+
override fun setHeaderOptions(view: TrueSheetView, options: ReadableMap?) {
|
|
250
|
+
val position = if (options != null && options.hasKey("position")) options.getString("position") else null
|
|
251
|
+
view.setAbsoluteHeader(position == "absolute")
|
|
252
|
+
}
|
|
253
|
+
|
|
253
254
|
@ReactProp(name = "footerOptions")
|
|
254
255
|
override fun setFooterOptions(view: TrueSheetView, options: ReadableMap?) {
|
|
256
|
+
val position = if (options != null && options.hasKey("footerPosition")) options.getString("footerPosition") else null
|
|
257
|
+
view.setAbsoluteFooter(position == "absolute")
|
|
258
|
+
|
|
255
259
|
val keyboardOffset =
|
|
256
260
|
if (options != null && options.hasKey("keyboardOffset")) options.getDouble("keyboardOffset").toFloat() else 0f
|
|
257
261
|
view.setFooterKeyboardOffset(keyboardOffset)
|
|
@@ -118,8 +118,9 @@ class TrueSheetCoordinatorLayout(context: Context) :
|
|
|
118
118
|
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
|
|
119
119
|
// RN's ReactEditText fires this(true) on ACTION_DOWN, killing sheet drag over an input.
|
|
120
120
|
// Swallow only that case — nothing between the touched EditText and the sheet scrolls
|
|
121
|
-
// (an overflowing multiline EditText or an input inside a scrollable
|
|
122
|
-
// Other children (maps, sliders, scrollables) keep the standard
|
|
121
|
+
// (an overflowing multiline EditText or an input inside a scrollable that can actually
|
|
122
|
+
// scroll still scrolls). Other children (maps, sliders, scrollables) keep the standard
|
|
123
|
+
// disallow contract.
|
|
123
124
|
if (disallowIntercept && streamDraggableEditText) return
|
|
124
125
|
super.requestDisallowInterceptTouchEvent(disallowIntercept)
|
|
125
126
|
}
|
|
@@ -138,8 +139,16 @@ class TrueSheetCoordinatorLayout(context: Context) :
|
|
|
138
139
|
.firstNotNullOfOrNull { it.getView() } ?: return
|
|
139
140
|
if (target !== sheet && !target.isDescendantOf(sheet)) return
|
|
140
141
|
|
|
142
|
+
// A pinned scrollable that can scroll (or hosts pull-to-refresh) owns its
|
|
143
|
+
// touches — nested scrolling drags the sheet. When it can't scroll (e.g.
|
|
144
|
+
// the sheet is fully expanded and the content fits) it eats the gesture
|
|
145
|
+
// instead, so fall through and let the sheet drag from it — an EditText
|
|
146
|
+
// target needs the disallow-intercept swallow or its stream dies.
|
|
141
147
|
val scrollView = delegate?.findScrollView()
|
|
142
|
-
if (scrollView != null && (target === scrollView || target.isDescendantOf(scrollView)))
|
|
148
|
+
if (scrollView != null && (target === scrollView || target.isDescendantOf(scrollView))) {
|
|
149
|
+
val hasRefreshControl = scrollView.parent is SwipeRefreshLayout
|
|
150
|
+
if (hasRefreshControl || scrollView.scrollY > 0 || scrollView.canScrollVertically(1)) return
|
|
151
|
+
}
|
|
143
152
|
|
|
144
153
|
var view: View? = target
|
|
145
154
|
while (view != null && view !== this) {
|
|
@@ -15,7 +15,9 @@ interface TrueSheetDetentCalculatorDelegate {
|
|
|
15
15
|
val detents: MutableList<Double>
|
|
16
16
|
val contentHeight: Int
|
|
17
17
|
val headerHeight: Int
|
|
18
|
+
val absoluteHeader: Boolean
|
|
18
19
|
val footerHeight: Int
|
|
20
|
+
val absoluteFooter: Boolean
|
|
19
21
|
val peekContentHeight: Int
|
|
20
22
|
val contentBottomInset: Int
|
|
21
23
|
val maxContentHeight: Int?
|
|
@@ -34,6 +36,7 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
34
36
|
private val realScreenHeight: Int get() = delegate?.realScreenHeight ?: 0
|
|
35
37
|
private val detents: List<Double> get() = delegate?.detents ?: emptyList()
|
|
36
38
|
private val contentHeight: Int get() = delegate?.contentHeight ?: 0
|
|
39
|
+
|
|
37
40
|
private val headerHeight: Int get() = delegate?.headerHeight ?: 0
|
|
38
41
|
private val footerHeight: Int get() = delegate?.footerHeight ?: 0
|
|
39
42
|
private val peekContentHeight: Int get() = delegate?.peekContentHeight ?: 0
|
|
@@ -42,13 +45,39 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
42
45
|
private val keyboardInset: Int get() = delegate?.keyboardInset ?: 0
|
|
43
46
|
private val topInset: Int get() = delegate?.topInset ?: 0
|
|
44
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Height for auto (-1.0) detents: content + header + footer height.
|
|
50
|
+
* An absolute (floating) header or footer overlaps the content, so it contributes no height.
|
|
51
|
+
*/
|
|
52
|
+
private val autoDetentHeight: Int
|
|
53
|
+
get() = contentHeight +
|
|
54
|
+
(if (delegate?.absoluteHeader == true) 0 else headerHeight) +
|
|
55
|
+
(if (delegate?.absoluteFooter == true) 0 else footerHeight)
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Bottom inset for auto detents. A relative footer owns the sheet's bottom
|
|
59
|
+
* edge, so it absorbs the inset instead of adding it to the auto height.
|
|
60
|
+
*/
|
|
61
|
+
private val autoDetentBottomInset: Int
|
|
62
|
+
get() = if (footerHeight > 0 && delegate?.absoluteFooter == false) 0 else contentBottomInset
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Bottom inset for peek detents. An absolute footer counts toward the peek
|
|
66
|
+
* height and absorbs the inset, so it isn't added again.
|
|
67
|
+
*/
|
|
68
|
+
private val peekDetentBottomInset: Int
|
|
69
|
+
get() = if (footerHeight > 0 && delegate?.absoluteFooter == true) 0 else contentBottomInset
|
|
70
|
+
|
|
45
71
|
/**
|
|
46
72
|
* Height for peek (-2.0) detents: header + footer + peek content height.
|
|
73
|
+
* A relative footer is pushed off-screen at peek, so it contributes no height.
|
|
47
74
|
* Falls back to 150dp when none is present.
|
|
48
75
|
*/
|
|
49
76
|
private val peekDetentHeight: Int
|
|
50
77
|
get() {
|
|
51
|
-
val height = headerHeight +
|
|
78
|
+
val height = headerHeight +
|
|
79
|
+
(if (delegate?.absoluteFooter == true) footerHeight else 0) +
|
|
80
|
+
peekContentHeight
|
|
52
81
|
return if (height > 0) height else DEFAULT_PEEK_HEIGHT.dpToPx().toInt()
|
|
53
82
|
}
|
|
54
83
|
|
|
@@ -58,9 +87,9 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
58
87
|
*/
|
|
59
88
|
fun getDetentHeight(detent: Double, includeKeyboard: Boolean = true): Int {
|
|
60
89
|
val baseHeight = if (detent == -1.0) {
|
|
61
|
-
|
|
90
|
+
autoDetentHeight + autoDetentBottomInset
|
|
62
91
|
} else if (detent == -2.0) {
|
|
63
|
-
peekDetentHeight +
|
|
92
|
+
peekDetentHeight + peekDetentBottomInset
|
|
64
93
|
} else {
|
|
65
94
|
if (detent <= 0.0 || detent > 1.0) {
|
|
66
95
|
throw IllegalArgumentException("TrueSheet: detent fraction ($detent) must be between 0 and 1")
|
|
@@ -106,7 +135,7 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
106
135
|
if (index < 0 || index >= detents.size) return 0f
|
|
107
136
|
val value = detents[index]
|
|
108
137
|
return if (value == -1.0) {
|
|
109
|
-
|
|
138
|
+
autoDetentHeight.toFloat() / screenHeight.toFloat()
|
|
110
139
|
} else if (value == -2.0) {
|
|
111
140
|
peekDetentHeight.toFloat() / screenHeight.toFloat()
|
|
112
141
|
} else {
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
#include <ReactCommon/JavaTurboModule.h>
|
|
4
4
|
#include <ReactCommon/TurboModule.h>
|
|
5
5
|
#include <jsi/jsi.h>
|
|
6
|
+
#include <react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h>
|
|
7
|
+
#include <react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h>
|
|
6
8
|
#include <react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h>
|
|
7
9
|
|
|
8
10
|
namespace facebook {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Created by Jovanni Lo (@lodev09)
|
|
3
|
+
// Copyright (c) 2024-present. All rights reserved.
|
|
4
|
+
//
|
|
5
|
+
// This source code is licensed under the MIT license found in the
|
|
6
|
+
// LICENSE file in the root directory of this source tree.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
#include <fbjni/fbjni.h>
|
|
10
|
+
#include <react/fabric/StateWrapperImpl.h>
|
|
11
|
+
#include <react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h>
|
|
12
|
+
#include <react/renderer/components/TrueSheetSpec/TrueSheetInsets.h>
|
|
13
|
+
#include <react/renderer/components/TrueSheetSpec/TrueSheetViewState.h>
|
|
14
|
+
#include <react/renderer/core/ConcreteState.h>
|
|
15
|
+
|
|
16
|
+
namespace facebook::react {
|
|
17
|
+
|
|
18
|
+
static std::shared_ptr<const State> getStateFromWrapper(jni::alias_ref<jobject> stateWrapper) {
|
|
19
|
+
static const auto stateWrapperImplClass =
|
|
20
|
+
jni::findClassStatic(StateWrapperImpl::StateWrapperImplJavaDescriptor);
|
|
21
|
+
if (!stateWrapper->isInstanceOf(stateWrapperImplClass)) {
|
|
22
|
+
return nullptr;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
auto impl = jni::static_ref_cast<StateWrapperImpl::jhybridobject>(stateWrapper);
|
|
26
|
+
return impl->cthis()->getState();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Kotlin's StateWrapper.updateState is hardcoded async. This bridges to
|
|
30
|
+
// ConcreteState::updateState with unstable_Immediate so the commit — and, when
|
|
31
|
+
// called from the UI thread, the mount — run synchronously, letting Yoga resize
|
|
32
|
+
// the container in the same frame as the sheet (parity with iOS).
|
|
33
|
+
static jboolean updateStateImmediate(
|
|
34
|
+
jni::alias_ref<jclass> /*clazz*/,
|
|
35
|
+
jni::alias_ref<jobject> stateWrapper,
|
|
36
|
+
jfloat containerWidth,
|
|
37
|
+
jfloat containerHeight) {
|
|
38
|
+
auto state = getStateFromWrapper(stateWrapper);
|
|
39
|
+
if (!state) {
|
|
40
|
+
return JNI_FALSE;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
auto concreteState = std::static_pointer_cast<const ConcreteState<TrueSheetViewState>>(state);
|
|
44
|
+
|
|
45
|
+
// Copy the latest data so fields this bridge doesn't set survive the update
|
|
46
|
+
TrueSheetViewState newState = concreteState->getData();
|
|
47
|
+
newState.containerWidth = containerWidth;
|
|
48
|
+
newState.containerHeight = containerHeight;
|
|
49
|
+
concreteState->updateState(std::move(newState), EventQueue::UpdateMode::unstable_Immediate);
|
|
50
|
+
|
|
51
|
+
return JNI_TRUE;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Same synchronous bridge for the footer's bottom inset — the footer must be
|
|
55
|
+
// padded (and resized) before detents are configured, otherwise the behavior
|
|
56
|
+
// is set up against the unpadded footer height and the auto detent lands an
|
|
57
|
+
// inset short.
|
|
58
|
+
static jboolean updateFooterStateImmediate(
|
|
59
|
+
jni::alias_ref<jclass> /*clazz*/,
|
|
60
|
+
jni::alias_ref<jobject> stateWrapper,
|
|
61
|
+
jfloat bottomInset) {
|
|
62
|
+
auto state = getStateFromWrapper(stateWrapper);
|
|
63
|
+
if (!state) {
|
|
64
|
+
return JNI_FALSE;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
auto concreteState =
|
|
68
|
+
std::static_pointer_cast<const ConcreteState<TrueSheetFooterViewState>>(state);
|
|
69
|
+
|
|
70
|
+
// Copy the latest data so fields this bridge doesn't set survive the update
|
|
71
|
+
TrueSheetFooterViewState newState = concreteState->getData();
|
|
72
|
+
newState.bottomInset = bottomInset;
|
|
73
|
+
newState.initialized = true;
|
|
74
|
+
concreteState->updateState(std::move(newState), EventQueue::UpdateMode::unstable_Immediate);
|
|
75
|
+
|
|
76
|
+
return JNI_TRUE;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Publishes the precalculated bottom safe-area inset so a late-mounted
|
|
80
|
+
// footer's first layout is already padded (see TrueSheetFooterViewShadowNode).
|
|
81
|
+
static void setBottomSafeArea(jni::alias_ref<jclass> /*clazz*/, jfloat insetDp) {
|
|
82
|
+
TrueSheetInsets::setBottomSafeArea(insetDp);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
} // namespace facebook::react
|
|
86
|
+
|
|
87
|
+
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void * /*reserved*/) {
|
|
88
|
+
return facebook::jni::initialize(vm, [] {
|
|
89
|
+
facebook::jni::findClassStatic("com/lodev09/truesheet/TrueSheetStateUpdater")
|
|
90
|
+
->registerNatives({
|
|
91
|
+
makeNativeMethod("nativeUpdateState", facebook::react::updateStateImmediate),
|
|
92
|
+
makeNativeMethod("nativeUpdateFooterState", facebook::react::updateFooterStateImmediate),
|
|
93
|
+
makeNativeMethod("nativeSetBottomSafeArea", facebook::react::setBottomSafeArea),
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h>
|
|
4
|
+
#include <react/renderer/core/ConcreteComponentDescriptor.h>
|
|
5
|
+
|
|
6
|
+
namespace facebook::react {
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Descriptor for <TrueSheetContentView> component.
|
|
10
|
+
*/
|
|
11
|
+
class TrueSheetContentViewComponentDescriptor final
|
|
12
|
+
: public ConcreteComponentDescriptor<TrueSheetContentViewShadowNode> {
|
|
13
|
+
using ConcreteComponentDescriptor::ConcreteComponentDescriptor;
|
|
14
|
+
|
|
15
|
+
void adopt(ShadowNode &shadowNode) const override {
|
|
16
|
+
auto &concreteShadowNode =
|
|
17
|
+
static_cast<TrueSheetContentViewShadowNode &>(shadowNode);
|
|
18
|
+
concreteShadowNode.adjustLayoutWithState();
|
|
19
|
+
|
|
20
|
+
ConcreteComponentDescriptor::adopt(shadowNode);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
} // namespace facebook::react
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#include "TrueSheetContentViewShadowNode.h"
|
|
2
|
+
|
|
3
|
+
#include <react/renderer/components/view/conversions.h>
|
|
4
|
+
|
|
5
|
+
namespace facebook::react {
|
|
6
|
+
|
|
7
|
+
using namespace yoga;
|
|
8
|
+
|
|
9
|
+
extern const char TrueSheetContentViewComponentName[] = "TrueSheetContentView";
|
|
10
|
+
|
|
11
|
+
void TrueSheetContentViewShadowNode::adjustLayoutWithState() {
|
|
12
|
+
ensureUnsealed();
|
|
13
|
+
|
|
14
|
+
auto state = std::static_pointer_cast<
|
|
15
|
+
const TrueSheetContentViewShadowNode::ConcreteState>(getState());
|
|
16
|
+
auto stateData = state->getData();
|
|
17
|
+
|
|
18
|
+
auto &props = getConcreteProps();
|
|
19
|
+
|
|
20
|
+
// When a ScrollView is pinned under an auto detent, fill the container so
|
|
21
|
+
// descendant flex layouts (flex:1 wrappers, the ScrollView itself) resolve
|
|
22
|
+
// against a definite height and the viewport is bounded — natural layout is
|
|
23
|
+
// circular there since the sheet height derives from the ScrollView's content
|
|
24
|
+
// size instead (see ContentView's naturalHeight on each platform). For fixed
|
|
25
|
+
// detents, content lays out naturally like a regular view.
|
|
26
|
+
auto flexGrow = stateData.scrollableBounded
|
|
27
|
+
? FloatOptional{1.0f}
|
|
28
|
+
: props.yogaStyle.flexGrow();
|
|
29
|
+
auto flexShrink = stateData.scrollableBounded
|
|
30
|
+
? FloatOptional{1.0f}
|
|
31
|
+
: props.yogaStyle.flexShrink();
|
|
32
|
+
|
|
33
|
+
if (yogaNode_.style().flexGrow() != flexGrow ||
|
|
34
|
+
yogaNode_.style().flexShrink() != flexShrink) {
|
|
35
|
+
yoga::Style adjustedStyle = props.yogaStyle;
|
|
36
|
+
adjustedStyle.setFlexGrow(flexGrow);
|
|
37
|
+
adjustedStyle.setFlexShrink(flexShrink);
|
|
38
|
+
yogaNode_.setStyle(adjustedStyle);
|
|
39
|
+
yogaNode_.setDirty(true);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
} // namespace facebook::react
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <jsi/jsi.h>
|
|
4
|
+
#include <react/renderer/components/TrueSheetSpec/EventEmitters.h>
|
|
5
|
+
#include <react/renderer/components/TrueSheetSpec/Props.h>
|
|
6
|
+
#include <react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h>
|
|
7
|
+
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
|
|
8
|
+
|
|
9
|
+
namespace facebook::react {
|
|
10
|
+
|
|
11
|
+
JSI_EXPORT extern const char TrueSheetContentViewComponentName[];
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* `ShadowNode` for <TrueSheetContentView> component.
|
|
15
|
+
*/
|
|
16
|
+
class JSI_EXPORT TrueSheetContentViewShadowNode final
|
|
17
|
+
: public ConcreteViewShadowNode<
|
|
18
|
+
TrueSheetContentViewComponentName,
|
|
19
|
+
TrueSheetContentViewProps,
|
|
20
|
+
TrueSheetContentViewEventEmitter,
|
|
21
|
+
TrueSheetContentViewState> {
|
|
22
|
+
using ConcreteViewShadowNode::ConcreteViewShadowNode;
|
|
23
|
+
|
|
24
|
+
public:
|
|
25
|
+
void adjustLayoutWithState();
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
} // namespace facebook::react
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#include "TrueSheetContentViewState.h"
|
|
2
|
+
|
|
3
|
+
namespace facebook::react {
|
|
4
|
+
|
|
5
|
+
#ifdef ANDROID
|
|
6
|
+
folly::dynamic TrueSheetContentViewState::getDynamic() const {
|
|
7
|
+
return folly::dynamic::object("scrollableBounded", scrollableBounded);
|
|
8
|
+
}
|
|
9
|
+
#endif
|
|
10
|
+
|
|
11
|
+
} // namespace facebook::react
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#ifdef ANDROID
|
|
4
|
+
#include <folly/dynamic.h>
|
|
5
|
+
#include <react/renderer/mapbuffer/MapBuffer.h>
|
|
6
|
+
#include <react/renderer/mapbuffer/MapBufferBuilder.h>
|
|
7
|
+
#endif
|
|
8
|
+
|
|
9
|
+
namespace facebook::react {
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* State for <TrueSheetContentView> component.
|
|
13
|
+
* Set from native when a ScrollView is pinned so the shadow node bounds the
|
|
14
|
+
* content to the container via flexShrink (see TrueSheetContentViewShadowNode).
|
|
15
|
+
*/
|
|
16
|
+
class TrueSheetContentViewState final {
|
|
17
|
+
public:
|
|
18
|
+
TrueSheetContentViewState() = default;
|
|
19
|
+
|
|
20
|
+
#ifdef ANDROID
|
|
21
|
+
TrueSheetContentViewState(
|
|
22
|
+
TrueSheetContentViewState const &previousState,
|
|
23
|
+
folly::dynamic data)
|
|
24
|
+
: scrollableBounded(data["scrollableBounded"].getBool()) {}
|
|
25
|
+
#endif
|
|
26
|
+
|
|
27
|
+
bool scrollableBounded{false};
|
|
28
|
+
|
|
29
|
+
#ifdef ANDROID
|
|
30
|
+
folly::dynamic getDynamic() const;
|
|
31
|
+
MapBuffer getMapBuffer() const {
|
|
32
|
+
return MapBufferBuilder::EMPTY();
|
|
33
|
+
}
|
|
34
|
+
#endif
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
} // namespace facebook::react
|