@lodev09/react-native-true-sheet 3.11.0-beta.9 → 3.11.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/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +11 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +6 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +52 -16
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +7 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +114 -1
- package/ios/TrueSheetContainerView.h +8 -0
- package/ios/TrueSheetContainerView.mm +33 -0
- package/ios/TrueSheetFooterView.h +6 -0
- package/ios/TrueSheetFooterView.mm +42 -2
- package/ios/TrueSheetView.mm +13 -0
- package/ios/TrueSheetViewController.h +3 -0
- package/ios/TrueSheetViewController.mm +151 -0
- package/lib/module/TrueSheet.js +2 -0
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +11 -1
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +5 -0
- package/lib/module/navigation/TrueSheetRouter.js +1 -1
- package/lib/module/navigation/TrueSheetRouter.js.map +1 -1
- package/lib/module/navigation/createTrueSheetNavigator.js +1 -1
- package/lib/module/navigation/createTrueSheetNavigator.js.map +1 -1
- package/lib/module/navigation/useTrueSheetNavigation.js +1 -1
- package/lib/module/navigation/useTrueSheetNavigation.js.map +1 -1
- package/lib/module/web/vaul/browser.js +1 -1
- package/lib/module/web/vaul/browser.js.map +1 -1
- package/lib/module/web/vaul/style.css.d.js +2 -0
- package/lib/module/web/vaul/style.css.d.js.map +1 -0
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +17 -0
- 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 +4 -0
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/navigation/TrueSheetRouter.d.ts +1 -1
- package/lib/typescript/src/navigation/TrueSheetRouter.d.ts.map +1 -1
- package/lib/typescript/src/navigation/TrueSheetView.d.ts +1 -1
- package/lib/typescript/src/navigation/TrueSheetView.d.ts.map +1 -1
- package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts +1 -1
- package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts.map +1 -1
- package/lib/typescript/src/navigation/screen/types.d.ts +1 -1
- package/lib/typescript/src/navigation/screen/types.d.ts.map +1 -1
- package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +1 -1
- package/lib/typescript/src/navigation/screen/useSheetScreenState.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/navigation/useTrueSheetNavigation.d.ts +1 -1
- package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts.map +1 -1
- package/package.json +23 -27
- package/src/TrueSheet.tsx +2 -0
- package/src/TrueSheet.types.ts +19 -0
- package/src/TrueSheet.web.tsx +13 -3
- package/src/fabric/TrueSheetViewNativeComponent.ts +5 -0
- package/src/navigation/TrueSheetRouter.ts +1 -1
- package/src/navigation/TrueSheetView.tsx +1 -1
- package/src/navigation/createTrueSheetNavigator.tsx +1 -1
- package/src/navigation/screen/types.ts +1 -1
- package/src/navigation/screen/useSheetScreenState.ts +1 -1
- package/src/navigation/types.ts +1 -1
- package/src/navigation/useTrueSheetNavigation.ts +1 -1
- package/src/web/vaul/style.css.d.ts +1 -0
|
@@ -3,6 +3,7 @@ package com.lodev09.truesheet
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.view.MotionEvent
|
|
5
5
|
import android.view.View
|
|
6
|
+
import android.view.ViewParent
|
|
6
7
|
import com.facebook.react.uimanager.JSPointerDispatcher
|
|
7
8
|
import com.facebook.react.uimanager.JSTouchDispatcher
|
|
8
9
|
import com.facebook.react.uimanager.PointerEvents
|
|
@@ -10,6 +11,7 @@ import com.facebook.react.uimanager.RootView
|
|
|
10
11
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
11
12
|
import com.facebook.react.uimanager.events.EventDispatcher
|
|
12
13
|
import com.facebook.react.views.view.ReactViewGroup
|
|
14
|
+
import com.lodev09.truesheet.core.TrueSheetCoordinatorLayout
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Delegate interface for footer view size changes and event dispatching
|
|
@@ -90,12 +92,21 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
|
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
|
|
95
|
+
findCoordinatorLayout()?.childDidClaimNativeGesture()
|
|
93
96
|
eventDispatcher?.let { dispatcher ->
|
|
94
97
|
jsTouchDispatcher.onChildStartedNativeGesture(ev, dispatcher)
|
|
95
98
|
jsPointerDispatcher?.onChildStartedNativeGesture(childView, ev, dispatcher)
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
101
|
|
|
102
|
+
private fun findCoordinatorLayout(): TrueSheetCoordinatorLayout? {
|
|
103
|
+
var current: ViewParent? = parent
|
|
104
|
+
while (current != null && current !is TrueSheetCoordinatorLayout) {
|
|
105
|
+
current = current.parent
|
|
106
|
+
}
|
|
107
|
+
return current as? TrueSheetCoordinatorLayout
|
|
108
|
+
}
|
|
109
|
+
|
|
99
110
|
override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
|
|
100
111
|
eventDispatcher?.let { jsTouchDispatcher.onChildEndedNativeGesture(ev, it) }
|
|
101
112
|
jsPointerDispatcher?.onChildEndedNativeGesture()
|
|
@@ -7,6 +7,7 @@ import android.view.accessibility.AccessibilityEvent
|
|
|
7
7
|
import androidx.annotation.UiThread
|
|
8
8
|
import com.facebook.react.bridge.LifecycleEventListener
|
|
9
9
|
import com.facebook.react.bridge.WritableNativeMap
|
|
10
|
+
import com.facebook.react.uimanager.PixelUtil.dpToPx
|
|
10
11
|
import com.facebook.react.uimanager.PixelUtil.pxToDp
|
|
11
12
|
import com.facebook.react.uimanager.StateWrapper
|
|
12
13
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
@@ -283,6 +284,11 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
283
284
|
setupScrollable()
|
|
284
285
|
}
|
|
285
286
|
|
|
287
|
+
fun setFooterKeyboardOffset(offset: Float) {
|
|
288
|
+
viewController.footerKeyboardOffset = offset.dpToPx().toInt()
|
|
289
|
+
viewController.positionFooter()
|
|
290
|
+
}
|
|
291
|
+
|
|
286
292
|
private fun setupScrollable() {
|
|
287
293
|
viewController.containerView?.let {
|
|
288
294
|
it.insetAdjustment = viewController.insetAdjustment
|
|
@@ -153,6 +153,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
153
153
|
var currentDetentIndex: Int = -1
|
|
154
154
|
private set
|
|
155
155
|
|
|
156
|
+
// Target detent index during an in-flight resize animation. -1 when idle.
|
|
157
|
+
// Used by setupSheetDetents so a layout-driven reconfigure during the
|
|
158
|
+
// animation doesn't snap the sheet back to the stale currentDetentIndex.
|
|
159
|
+
private var pendingDetentIndex: Int = -1
|
|
160
|
+
|
|
156
161
|
private var interactionState: InteractionState = InteractionState.Idle
|
|
157
162
|
internal var isBeingDismissed = false
|
|
158
163
|
private set
|
|
@@ -187,6 +192,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
187
192
|
private val jsTouchDispatcher = JSTouchDispatcher(this)
|
|
188
193
|
private val jsPointerDispatcher = JSPointerDispatcher(this)
|
|
189
194
|
|
|
195
|
+
// True when the active touch stream began inside the footer. Latched on
|
|
196
|
+
// ACTION_DOWN so subsequent MOVE events keep routing to the footer even if
|
|
197
|
+
// the finger drifts outside the footer's rect mid-gesture.
|
|
198
|
+
private var footerOwnsTouchStream = false
|
|
199
|
+
|
|
190
200
|
private val eventDispatcher
|
|
191
201
|
get() = delegate?.eventDispatcher
|
|
192
202
|
|
|
@@ -376,6 +386,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
376
386
|
isPresentAnimating = false
|
|
377
387
|
lastEmittedPositionPx = -1
|
|
378
388
|
detentIndexBeforeKeyboard = -1
|
|
389
|
+
pendingDetentIndex = -1
|
|
379
390
|
isKeyboardDismissProgrammatic = false
|
|
380
391
|
focusedViewBeforeBlur = null
|
|
381
392
|
shouldAnimatePresent = true
|
|
@@ -568,6 +579,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
568
579
|
private fun handleStateSettled(sheetView: View, newState: Int) {
|
|
569
580
|
if (interactionState is InteractionState.Reconfiguring) return
|
|
570
581
|
|
|
582
|
+
// Sheet has reached a stable state — any in-flight resize target is now stale,
|
|
583
|
+
// whether settled at the target, interrupted by drag, or superseded.
|
|
584
|
+
pendingDetentIndex = -1
|
|
585
|
+
|
|
571
586
|
val index = detentCalculator.getDetentIndexForState(newState) ?: return
|
|
572
587
|
val position = getPositionDpForView(sheetView)
|
|
573
588
|
val detentInfo = DetentInfo(index, position)
|
|
@@ -711,6 +726,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
711
726
|
detentIndexBeforeKeyboard = detentIndex
|
|
712
727
|
}
|
|
713
728
|
|
|
729
|
+
pendingDetentIndex = detentIndex
|
|
714
730
|
setupDimmedBackground()
|
|
715
731
|
setStateForDetentIndex(detentIndex)
|
|
716
732
|
resizePromise?.invoke()
|
|
@@ -859,7 +875,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
859
875
|
updateStateDimensions(expandedOffset)
|
|
860
876
|
|
|
861
877
|
if (isPresented && applyState) {
|
|
862
|
-
|
|
878
|
+
// Prefer the pending target while a resize animation is in flight so a
|
|
879
|
+
// layout-driven reconfigure doesn't revert to the stale currentDetentIndex.
|
|
880
|
+
val targetIndex = if (pendingDetentIndex >= 0) pendingDetentIndex else currentDetentIndex
|
|
881
|
+
setStateForDetentIndex(targetIndex)
|
|
863
882
|
}
|
|
864
883
|
|
|
865
884
|
interactionState = InteractionState.Idle
|
|
@@ -960,6 +979,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
960
979
|
// MARK: - Footer Positioning
|
|
961
980
|
// =============================================================================
|
|
962
981
|
|
|
982
|
+
var footerKeyboardOffset: Int = 0
|
|
983
|
+
|
|
963
984
|
fun positionFooter(slideOffset: Float? = null) {
|
|
964
985
|
if (!isPresented) return
|
|
965
986
|
val footerView = containerView?.footerView ?: return
|
|
@@ -969,7 +990,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
969
990
|
val sheetHeight = sheet.height
|
|
970
991
|
val sheetTop = sheet.top
|
|
971
992
|
|
|
972
|
-
|
|
993
|
+
val keyboardShift = if (currentKeyboardInset > 0) maxOf(0, currentKeyboardInset + footerKeyboardOffset) else 0
|
|
994
|
+
var footerY = (sheetHeight - sheetTop - footerHeight - keyboardShift).toFloat()
|
|
973
995
|
|
|
974
996
|
// Adjust during dismiss animation when slideOffset is negative
|
|
975
997
|
if (slideOffset != null && slideOffset < 0) {
|
|
@@ -1019,7 +1041,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1019
1041
|
delegate = object : TrueSheetKeyboardObserverDelegate {
|
|
1020
1042
|
override fun keyboardWillShow(height: Int) {
|
|
1021
1043
|
if (!shouldHandleKeyboard()) return
|
|
1022
|
-
|
|
1044
|
+
// If a resize is in flight, restore to its target — not the stale current
|
|
1045
|
+
detentIndexBeforeKeyboard = if (pendingDetentIndex >= 0) pendingDetentIndex else currentDetentIndex
|
|
1046
|
+
pendingDetentIndex = -1
|
|
1023
1047
|
setupSheetDetents()
|
|
1024
1048
|
currentDetentIndex = detents.size - 1
|
|
1025
1049
|
setStateForDetentIndex(currentDetentIndex)
|
|
@@ -1097,6 +1121,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1097
1121
|
|
|
1098
1122
|
private fun handleDragBegin(sheetView: View) {
|
|
1099
1123
|
detentIndexBeforeKeyboard = -1
|
|
1124
|
+
pendingDetentIndex = -1
|
|
1100
1125
|
|
|
1101
1126
|
val position = getPositionDpForView(sheetView)
|
|
1102
1127
|
val detent = detentCalculator.getDetentValueForIndex(currentDetentIndex)
|
|
@@ -1274,26 +1299,36 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1274
1299
|
|
|
1275
1300
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
1276
1301
|
val footer = containerView?.footerView
|
|
1302
|
+
val action = event.actionMasked
|
|
1303
|
+
|
|
1277
1304
|
if (footer != null && footer.isShown) {
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1305
|
+
if (action == MotionEvent.ACTION_DOWN) {
|
|
1306
|
+
val loc = ScreenUtils.getScreenLocation(footer)
|
|
1307
|
+
val x = event.rawX.toInt()
|
|
1308
|
+
val y = event.rawY.toInt()
|
|
1309
|
+
footerOwnsTouchStream = x >= loc[0] &&
|
|
1310
|
+
x <= loc[0] + footer.width &&
|
|
1311
|
+
y >= loc[1] &&
|
|
1312
|
+
y <= loc[1] + footer.height
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
if (footerOwnsTouchStream) {
|
|
1316
|
+
val loc = ScreenUtils.getScreenLocation(footer)
|
|
1287
1317
|
val localEvent = MotionEvent.obtain(event)
|
|
1288
|
-
localEvent.setLocation(
|
|
1289
|
-
(touchX - footerLocation[0]).toFloat(),
|
|
1290
|
-
(touchY - footerLocation[1]).toFloat()
|
|
1291
|
-
)
|
|
1318
|
+
localEvent.setLocation(event.rawX - loc[0], event.rawY - loc[1])
|
|
1292
1319
|
val handled = footer.dispatchTouchEvent(localEvent)
|
|
1293
1320
|
localEvent.recycle()
|
|
1321
|
+
|
|
1322
|
+
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
|
1323
|
+
footerOwnsTouchStream = false
|
|
1324
|
+
}
|
|
1294
1325
|
if (handled) return true
|
|
1295
1326
|
}
|
|
1296
1327
|
}
|
|
1328
|
+
|
|
1329
|
+
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
|
1330
|
+
footerOwnsTouchStream = false
|
|
1331
|
+
}
|
|
1297
1332
|
return super.dispatchTouchEvent(event)
|
|
1298
1333
|
}
|
|
1299
1334
|
|
|
@@ -1325,6 +1360,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1325
1360
|
}
|
|
1326
1361
|
|
|
1327
1362
|
override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
|
|
1363
|
+
coordinatorLayout?.childDidClaimNativeGesture()
|
|
1328
1364
|
eventDispatcher?.let {
|
|
1329
1365
|
jsTouchDispatcher.onChildStartedNativeGesture(ev, it)
|
|
1330
1366
|
jsPointerDispatcher.onChildStartedNativeGesture(childView, ev, it)
|
|
@@ -230,6 +230,13 @@ class TrueSheetViewManager :
|
|
|
230
230
|
view.setScrollableOptions(scrollableOptions)
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
@ReactProp(name = "footerOptions")
|
|
234
|
+
override fun setFooterOptions(view: TrueSheetView, options: ReadableMap?) {
|
|
235
|
+
val keyboardOffset =
|
|
236
|
+
if (options != null && options.hasKey("keyboardOffset")) options.getDouble("keyboardOffset").toFloat() else 0f
|
|
237
|
+
view.setFooterKeyboardOffset(keyboardOffset)
|
|
238
|
+
}
|
|
239
|
+
|
|
233
240
|
companion object {
|
|
234
241
|
const val REACT_CLASS = "TrueSheetView"
|
|
235
242
|
const val TAG_NAME = "TrueSheet"
|
|
@@ -4,8 +4,10 @@ import android.annotation.SuppressLint
|
|
|
4
4
|
import android.content.Context
|
|
5
5
|
import android.content.res.Configuration
|
|
6
6
|
import android.view.MotionEvent
|
|
7
|
+
import android.view.View
|
|
7
8
|
import android.view.ViewConfiguration
|
|
8
9
|
import android.view.ViewGroup
|
|
10
|
+
import android.widget.EditText
|
|
9
11
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
10
12
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
11
13
|
import com.facebook.react.uimanager.PointerEvents
|
|
@@ -36,6 +38,23 @@ class TrueSheetCoordinatorLayout(context: Context) :
|
|
|
36
38
|
private var initialY = 0f
|
|
37
39
|
private var activePointerId = 0
|
|
38
40
|
|
|
41
|
+
// Horizontal-dominance tracking. iOS lets horizontal child gestures win over
|
|
42
|
+
// the sheet's vertical pan; we mirror that by locking out BottomSheetBehavior
|
|
43
|
+
// interception once the first significant movement of a stream is horizontal.
|
|
44
|
+
private var streamInitialX = 0f
|
|
45
|
+
private var streamInitialY = 0f
|
|
46
|
+
private var streamHorizontalLocked = false
|
|
47
|
+
private var streamDirectionDecided = false
|
|
48
|
+
|
|
49
|
+
// Native-gesture-claim tracking. RNGH handlers activate past BottomSheetBehavior's
|
|
50
|
+
// drag slop and never call requestDisallowInterceptTouchEvent, so the sheet would
|
|
51
|
+
// steal vertical gestures (e.g. a vertical pan) before they can activate. While the
|
|
52
|
+
// touch is inside a descendant GestureHandlerRootView we hold off interception within
|
|
53
|
+
// a grace distance; once a child claims the gesture (relayed from the RootView impls
|
|
54
|
+
// via childDidClaimNativeGesture) we yield the rest of the stream, mirroring iOS.
|
|
55
|
+
private var streamHasGestureRoot = false
|
|
56
|
+
private var streamGestureClaimed = false
|
|
57
|
+
|
|
39
58
|
init {
|
|
40
59
|
layoutParams = LayoutParams(
|
|
41
60
|
LayoutParams.MATCH_PARENT,
|
|
@@ -86,6 +105,48 @@ class TrueSheetCoordinatorLayout(context: Context) :
|
|
|
86
105
|
} catch (_: Exception) {}
|
|
87
106
|
}
|
|
88
107
|
|
|
108
|
+
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
|
|
109
|
+
// RN's ReactEditText fires this(true) on ACTION_DOWN, killing sheet drag over an input.
|
|
110
|
+
// Swallow only that case — an EditText under the touch with nothing vertically scrollable
|
|
111
|
+
// (a pinned ScrollView or an overflowing multiline EditText still scrolls). Other children
|
|
112
|
+
// (maps, sliders, scrollables) keep the standard disallow contract.
|
|
113
|
+
if (disallowIntercept && touchedViewIsEditText() && !touchedViewCanScrollVertically()) return
|
|
114
|
+
super.requestDisallowInterceptTouchEvent(disallowIntercept)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private fun touchedViewIsEditText(): Boolean = findDescendantAt(this, streamInitialX.toInt(), streamInitialY.toInt()) { it is EditText }
|
|
118
|
+
|
|
119
|
+
private fun touchedViewCanScrollVertically(): Boolean {
|
|
120
|
+
delegate?.findScrollView()?.let {
|
|
121
|
+
if (it.canScrollVertically(1) || it.canScrollVertically(-1)) return true
|
|
122
|
+
}
|
|
123
|
+
return findDescendantAt(this, streamInitialX.toInt(), streamInitialY.toInt()) {
|
|
124
|
+
it.canScrollVertically(1) || it.canScrollVertically(-1)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private fun findDescendantAt(view: View, rawX: Int, rawY: Int, predicate: (View) -> Boolean): Boolean {
|
|
129
|
+
val loc = IntArray(2)
|
|
130
|
+
view.getLocationOnScreen(loc)
|
|
131
|
+
if (rawX < loc[0] || rawX > loc[0] + view.width || rawY < loc[1] || rawY > loc[1] + view.height) return false
|
|
132
|
+
if (predicate(view)) return true
|
|
133
|
+
if (view is ViewGroup) {
|
|
134
|
+
for (i in 0 until view.childCount) {
|
|
135
|
+
if (findDescendantAt(view.getChildAt(i), rawX, rawY, predicate)) return true
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Called by descendant RootView implementations (controller, footer) when a child
|
|
143
|
+
* starts a native gesture — e.g. an RNGH handler activated or a scrollable began
|
|
144
|
+
* scrolling. Yields sheet interception for the rest of the touch stream.
|
|
145
|
+
*/
|
|
146
|
+
fun childDidClaimNativeGesture() {
|
|
147
|
+
streamGestureClaimed = true
|
|
148
|
+
}
|
|
149
|
+
|
|
89
150
|
/**
|
|
90
151
|
* Intercepts touch events for ScrollViews that can't scroll (content < viewport),
|
|
91
152
|
* allowing the sheet to be dragged in these cases.
|
|
@@ -94,8 +155,55 @@ class TrueSheetCoordinatorLayout(context: Context) :
|
|
|
94
155
|
* See: https://github.com/facebook/react-native/pull/44099
|
|
95
156
|
*/
|
|
96
157
|
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
|
97
|
-
|
|
158
|
+
val action = ev.actionMasked
|
|
159
|
+
|
|
160
|
+
if (action == MotionEvent.ACTION_DOWN) {
|
|
98
161
|
clearStaleNestedScrollingChildRef()
|
|
162
|
+
|
|
163
|
+
streamInitialX = ev.rawX
|
|
164
|
+
streamInitialY = ev.rawY
|
|
165
|
+
streamHorizontalLocked = false
|
|
166
|
+
streamDirectionDecided = false
|
|
167
|
+
streamGestureClaimed = false
|
|
168
|
+
streamHasGestureRoot = findDescendantAt(this, ev.rawX.toInt(), ev.rawY.toInt()) {
|
|
169
|
+
it.javaClass.name == GESTURE_HANDLER_ROOT_VIEW_CLASS
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Decide gesture direction on first significant movement of the stream.
|
|
174
|
+
// If horizontal wins, lock out BottomSheetBehavior for the rest of the stream
|
|
175
|
+
// so child handlers (carousels, swipe buttons, etc.) can claim the touch.
|
|
176
|
+
if (!streamDirectionDecided && action == MotionEvent.ACTION_MOVE) {
|
|
177
|
+
val dx = kotlin.math.abs(ev.rawX - streamInitialX)
|
|
178
|
+
val dy = kotlin.math.abs(ev.rawY - streamInitialY)
|
|
179
|
+
if (dx > touchSlop || dy > touchSlop) {
|
|
180
|
+
streamDirectionDecided = true
|
|
181
|
+
streamHorizontalLocked = dx > dy
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (streamHorizontalLocked) {
|
|
186
|
+
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
|
187
|
+
streamHorizontalLocked = false
|
|
188
|
+
streamDirectionDecided = false
|
|
189
|
+
}
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (streamGestureClaimed) {
|
|
194
|
+
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
|
195
|
+
streamGestureClaimed = false
|
|
196
|
+
}
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Hold off BottomSheetBehavior within the grace distance so a gesture handler
|
|
201
|
+
// under the touch can activate and claim the stream first.
|
|
202
|
+
if (streamHasGestureRoot &&
|
|
203
|
+
action == MotionEvent.ACTION_MOVE &&
|
|
204
|
+
kotlin.math.abs(ev.rawY - streamInitialY) < touchSlop * GESTURE_CLAIM_SLOP_FACTOR
|
|
205
|
+
) {
|
|
206
|
+
return false
|
|
99
207
|
}
|
|
100
208
|
|
|
101
209
|
val scrollView = delegate?.findScrollView()
|
|
@@ -151,4 +259,9 @@ class TrueSheetCoordinatorLayout(context: Context) :
|
|
|
151
259
|
}
|
|
152
260
|
return super.onTouchEvent(ev)
|
|
153
261
|
}
|
|
262
|
+
|
|
263
|
+
companion object {
|
|
264
|
+
private const val GESTURE_HANDLER_ROOT_VIEW_CLASS = "com.swmansion.gesturehandler.react.RNGestureHandlerRootView"
|
|
265
|
+
private const val GESTURE_CLAIM_SLOP_FACTOR = 3
|
|
266
|
+
}
|
|
154
267
|
}
|
|
@@ -31,6 +31,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
31
31
|
@optional
|
|
32
32
|
|
|
33
33
|
- (void)containerViewHeaderDidChangeSize:(CGSize)newSize;
|
|
34
|
+
- (void)containerViewFooterDidChangeSize:(CGSize)newSize;
|
|
34
35
|
|
|
35
36
|
@end
|
|
36
37
|
|
|
@@ -71,6 +72,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
71
72
|
*/
|
|
72
73
|
- (void)layoutFooter;
|
|
73
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Re-applies the footer's keyboard slide to reflect a live `keyboardOffset` change.
|
|
77
|
+
*/
|
|
78
|
+
- (void)updateFooterKeyboardOffset;
|
|
79
|
+
|
|
80
|
+
- (BOOL)hasAccessibilityFooterElements;
|
|
81
|
+
|
|
74
82
|
/**
|
|
75
83
|
* Setup scrollable content
|
|
76
84
|
*/
|
|
@@ -72,10 +72,38 @@ using namespace facebook::react;
|
|
|
72
72
|
_headerView = nil;
|
|
73
73
|
_footerView = nil;
|
|
74
74
|
_scrollableSet = NO;
|
|
75
|
+
self.isAccessibilityElement = NO;
|
|
75
76
|
}
|
|
76
77
|
return self;
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
#pragma mark - Accessibility
|
|
81
|
+
|
|
82
|
+
- (NSArray *)accessibilityElements {
|
|
83
|
+
NSMutableArray *elements = [NSMutableArray array];
|
|
84
|
+
if (_headerView) {
|
|
85
|
+
[elements addObject:_headerView];
|
|
86
|
+
}
|
|
87
|
+
if (_contentView) {
|
|
88
|
+
[elements addObject:_contentView];
|
|
89
|
+
}
|
|
90
|
+
if (_footerView) {
|
|
91
|
+
// Footer hosts are layout containers; expose their children when present so
|
|
92
|
+
// VoiceOver and XCTest can target the actual footer controls.
|
|
93
|
+
NSArray *footerElements = _footerView.accessibilityElements;
|
|
94
|
+
if (footerElements.count > 0) {
|
|
95
|
+
[elements addObjectsFromArray:footerElements];
|
|
96
|
+
} else {
|
|
97
|
+
[elements addObject:_footerView];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return elements;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
- (BOOL)hasAccessibilityFooterElements {
|
|
104
|
+
return _footerView && _footerView.accessibilityElements.count > 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
79
107
|
#pragma mark - Layout
|
|
80
108
|
|
|
81
109
|
- (void)layoutSubviews {
|
|
@@ -100,6 +128,10 @@ using namespace facebook::react;
|
|
|
100
128
|
}
|
|
101
129
|
}
|
|
102
130
|
|
|
131
|
+
- (void)updateFooterKeyboardOffset {
|
|
132
|
+
[_footerView applyKeyboardOffset];
|
|
133
|
+
}
|
|
134
|
+
|
|
103
135
|
- (void)setScrollableEnabled:(BOOL)scrollableEnabled {
|
|
104
136
|
_scrollableEnabled = scrollableEnabled;
|
|
105
137
|
_scrollableSet = YES;
|
|
@@ -234,6 +266,7 @@ using namespace facebook::react;
|
|
|
234
266
|
#pragma mark - TrueSheetFooterViewDelegate
|
|
235
267
|
|
|
236
268
|
- (void)footerViewDidChangeSize:(CGSize)newSize {
|
|
269
|
+
[self.delegate containerViewFooterDidChangeSize:newSize];
|
|
237
270
|
if (@available(iOS 26.0, *)) {
|
|
238
271
|
[self setupEdgeInteractions];
|
|
239
272
|
}
|
|
@@ -29,6 +29,12 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
29
29
|
|
|
30
30
|
- (void)setupConstraintsWithHeight:(CGFloat)height;
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Re-applies the footer's keyboard slide using the current keyboard height.
|
|
34
|
+
* No-op when the keyboard is hidden. Used to reflect live `keyboardOffset` changes.
|
|
35
|
+
*/
|
|
36
|
+
- (void)applyKeyboardOffset;
|
|
37
|
+
|
|
32
38
|
@end
|
|
33
39
|
|
|
34
40
|
NS_ASSUME_NONNULL_END
|
|
@@ -37,6 +37,7 @@ using namespace facebook::react;
|
|
|
37
37
|
_props = defaultProps;
|
|
38
38
|
|
|
39
39
|
self.backgroundColor = [UIColor clearColor];
|
|
40
|
+
self.isAccessibilityElement = NO;
|
|
40
41
|
|
|
41
42
|
_lastHeight = 0;
|
|
42
43
|
_pendingHeight = 0;
|
|
@@ -47,6 +48,30 @@ using namespace facebook::react;
|
|
|
47
48
|
return self;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
#pragma mark - Accessibility
|
|
52
|
+
|
|
53
|
+
- (NSArray *)accessibilityElements {
|
|
54
|
+
NSMutableArray *elements = [NSMutableArray array];
|
|
55
|
+
[self collectAccessibilityElementsFromView:self into:elements];
|
|
56
|
+
if (elements.count > 0) {
|
|
57
|
+
return elements;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return [super accessibilityElements];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
- (void)collectAccessibilityElementsFromView:(UIView *)view into:(NSMutableArray *)elements {
|
|
64
|
+
for (UIView *subview in view.subviews) {
|
|
65
|
+
if (subview.isAccessibilityElement || subview.accessibilityLabel || subview.accessibilityIdentifier) {
|
|
66
|
+
[elements addObject:subview];
|
|
67
|
+
} else if (subview.accessibilityElements.count > 0) {
|
|
68
|
+
[elements addObjectsFromArray:subview.accessibilityElements];
|
|
69
|
+
} else {
|
|
70
|
+
[self collectAccessibilityElementsFromView:subview into:elements];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
50
75
|
#pragma mark - Layout
|
|
51
76
|
|
|
52
77
|
- (void)setupConstraintsWithHeight:(CGFloat)height {
|
|
@@ -126,13 +151,15 @@ using namespace facebook::react;
|
|
|
126
151
|
return;
|
|
127
152
|
}
|
|
128
153
|
|
|
129
|
-
|
|
154
|
+
CGFloat keyboardOffset = self.keyboardObserver.viewController.footerKeyboardOffset;
|
|
155
|
+
CGFloat slide = MAX(0, height + keyboardOffset);
|
|
156
|
+
_currentKeyboardOffset = slide;
|
|
130
157
|
|
|
131
158
|
[UIView animateWithDuration:duration
|
|
132
159
|
delay:0
|
|
133
160
|
options:curve | UIViewAnimationOptionBeginFromCurrentState
|
|
134
161
|
animations:^{
|
|
135
|
-
self->_bottomConstraint.constant = -
|
|
162
|
+
self->_bottomConstraint.constant = -slide;
|
|
136
163
|
[self.superview layoutIfNeeded];
|
|
137
164
|
}
|
|
138
165
|
completion:nil];
|
|
@@ -155,6 +182,19 @@ using namespace facebook::react;
|
|
|
155
182
|
completion:nil];
|
|
156
183
|
}
|
|
157
184
|
|
|
185
|
+
- (void)applyKeyboardOffset {
|
|
186
|
+
CGFloat height = self.keyboardObserver.currentHeight;
|
|
187
|
+
if (!_bottomConstraint || height <= 0) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
CGFloat keyboardOffset = self.keyboardObserver.viewController.footerKeyboardOffset;
|
|
192
|
+
CGFloat slide = MAX(0, height + keyboardOffset);
|
|
193
|
+
_currentKeyboardOffset = slide;
|
|
194
|
+
_bottomConstraint.constant = -slide;
|
|
195
|
+
[self.superview layoutIfNeeded];
|
|
196
|
+
}
|
|
197
|
+
|
|
158
198
|
@end
|
|
159
199
|
|
|
160
200
|
Class<RCTComponentViewProtocol> TrueSheetFooterViewCls(void) {
|
package/ios/TrueSheetView.mm
CHANGED
|
@@ -264,6 +264,12 @@ using namespace facebook::react;
|
|
|
264
264
|
|
|
265
265
|
_controller.scrollingExpandsSheet = scrollingExpandsSheet;
|
|
266
266
|
|
|
267
|
+
CGFloat footerKeyboardOffset = newProps.footerOptions.keyboardOffset;
|
|
268
|
+
if (_controller.footerKeyboardOffset != footerKeyboardOffset) {
|
|
269
|
+
_controller.footerKeyboardOffset = footerKeyboardOffset;
|
|
270
|
+
[_containerView updateFooterKeyboardOffset];
|
|
271
|
+
}
|
|
272
|
+
|
|
267
273
|
_insetAdjustment = newProps.insetAdjustment;
|
|
268
274
|
_controller.insetAdjustment = _insetAdjustment;
|
|
269
275
|
|
|
@@ -373,6 +379,9 @@ using namespace facebook::react;
|
|
|
373
379
|
[_controller.view addSubview:_containerView];
|
|
374
380
|
[LayoutUtil pinView:_containerView toParentView:_controller.view edges:UIRectEdgeAll];
|
|
375
381
|
[_controller.view bringSubviewToFront:_containerView];
|
|
382
|
+
_containerView.accessibilityViewIsModal = YES;
|
|
383
|
+
_controller.accessibilityContentView = _containerView;
|
|
384
|
+
[_controller setupAccessibilityContainer];
|
|
376
385
|
|
|
377
386
|
CGFloat contentHeight = [_containerView contentHeight];
|
|
378
387
|
if (contentHeight > 0) {
|
|
@@ -570,6 +579,10 @@ using namespace facebook::react;
|
|
|
570
579
|
[self setupSheetDetentsForSizeChange];
|
|
571
580
|
}
|
|
572
581
|
|
|
582
|
+
- (void)containerViewFooterDidChangeSize:(CGSize)newSize {
|
|
583
|
+
[_controller setupAccessibilityContainer];
|
|
584
|
+
}
|
|
585
|
+
|
|
573
586
|
// When the ScrollView changes (e.g. conditional remount), re-pin the new ScrollView.
|
|
574
587
|
- (void)containerViewScrollViewDidChange {
|
|
575
588
|
[self setupScrollable];
|
|
@@ -71,13 +71,16 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
71
71
|
@property (nonatomic, assign) facebook::react::TrueSheetViewAnchor anchor;
|
|
72
72
|
@property (nonatomic, assign) facebook::react::TrueSheetViewInsetAdjustment insetAdjustment;
|
|
73
73
|
@property (nonatomic, assign) BOOL scrollingExpandsSheet;
|
|
74
|
+
@property (nonatomic, assign) CGFloat footerKeyboardOffset;
|
|
74
75
|
@property (nonatomic, assign) BOOL dismissible;
|
|
75
76
|
@property (nonatomic, assign) BOOL isPresented;
|
|
76
77
|
@property (nonatomic, assign) NSInteger activeDetentIndex;
|
|
77
78
|
@property (nonatomic, readonly) BOOL isTopmostPresentedController;
|
|
79
|
+
@property (nonatomic, weak, nullable) UIView *accessibilityContentView;
|
|
78
80
|
|
|
79
81
|
@property (nonatomic, readonly) CGFloat screenHeight;
|
|
80
82
|
|
|
83
|
+
- (void)setupAccessibilityContainer;
|
|
81
84
|
- (void)applyActiveDetent;
|
|
82
85
|
- (void)setupActiveDetentWithIndex:(NSInteger)index;
|
|
83
86
|
- (void)resizeToDetentIndex:(NSInteger)index;
|