@lodev09/react-native-true-sheet 3.11.0-beta.8 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +11 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +48 -15
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +114 -1
- package/ios/TrueSheetContainerView.h +3 -0
- package/ios/TrueSheetContainerView.mm +29 -0
- package/ios/TrueSheetFooterView.mm +25 -0
- package/ios/TrueSheetView.mm +7 -0
- package/ios/TrueSheetViewController.h +2 -0
- package/ios/TrueSheetViewController.mm +151 -0
- package/lib/module/TrueSheet.web.js +18 -2
- package/lib/module/TrueSheet.web.js.map +1 -1
- 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.web.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 +21 -19
- package/src/TrueSheet.web.tsx +20 -4
- 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
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 lodev09
|
|
4
4
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
5
|
of this software and associated documentation files (the "Software"), to deal
|
|
6
6
|
in the Software without restriction, including without limitation the rights
|
|
@@ -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()
|
|
@@ -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
|
|
@@ -1019,7 +1038,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1019
1038
|
delegate = object : TrueSheetKeyboardObserverDelegate {
|
|
1020
1039
|
override fun keyboardWillShow(height: Int) {
|
|
1021
1040
|
if (!shouldHandleKeyboard()) return
|
|
1022
|
-
|
|
1041
|
+
// If a resize is in flight, restore to its target — not the stale current
|
|
1042
|
+
detentIndexBeforeKeyboard = if (pendingDetentIndex >= 0) pendingDetentIndex else currentDetentIndex
|
|
1043
|
+
pendingDetentIndex = -1
|
|
1023
1044
|
setupSheetDetents()
|
|
1024
1045
|
currentDetentIndex = detents.size - 1
|
|
1025
1046
|
setStateForDetentIndex(currentDetentIndex)
|
|
@@ -1097,6 +1118,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1097
1118
|
|
|
1098
1119
|
private fun handleDragBegin(sheetView: View) {
|
|
1099
1120
|
detentIndexBeforeKeyboard = -1
|
|
1121
|
+
pendingDetentIndex = -1
|
|
1100
1122
|
|
|
1101
1123
|
val position = getPositionDpForView(sheetView)
|
|
1102
1124
|
val detent = detentCalculator.getDetentValueForIndex(currentDetentIndex)
|
|
@@ -1274,26 +1296,36 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1274
1296
|
|
|
1275
1297
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
1276
1298
|
val footer = containerView?.footerView
|
|
1299
|
+
val action = event.actionMasked
|
|
1300
|
+
|
|
1277
1301
|
if (footer != null && footer.isShown) {
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1302
|
+
if (action == MotionEvent.ACTION_DOWN) {
|
|
1303
|
+
val loc = ScreenUtils.getScreenLocation(footer)
|
|
1304
|
+
val x = event.rawX.toInt()
|
|
1305
|
+
val y = event.rawY.toInt()
|
|
1306
|
+
footerOwnsTouchStream = x >= loc[0] &&
|
|
1307
|
+
x <= loc[0] + footer.width &&
|
|
1308
|
+
y >= loc[1] &&
|
|
1309
|
+
y <= loc[1] + footer.height
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (footerOwnsTouchStream) {
|
|
1313
|
+
val loc = ScreenUtils.getScreenLocation(footer)
|
|
1287
1314
|
val localEvent = MotionEvent.obtain(event)
|
|
1288
|
-
localEvent.setLocation(
|
|
1289
|
-
(touchX - footerLocation[0]).toFloat(),
|
|
1290
|
-
(touchY - footerLocation[1]).toFloat()
|
|
1291
|
-
)
|
|
1315
|
+
localEvent.setLocation(event.rawX - loc[0], event.rawY - loc[1])
|
|
1292
1316
|
val handled = footer.dispatchTouchEvent(localEvent)
|
|
1293
1317
|
localEvent.recycle()
|
|
1318
|
+
|
|
1319
|
+
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
|
1320
|
+
footerOwnsTouchStream = false
|
|
1321
|
+
}
|
|
1294
1322
|
if (handled) return true
|
|
1295
1323
|
}
|
|
1296
1324
|
}
|
|
1325
|
+
|
|
1326
|
+
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
|
1327
|
+
footerOwnsTouchStream = false
|
|
1328
|
+
}
|
|
1297
1329
|
return super.dispatchTouchEvent(event)
|
|
1298
1330
|
}
|
|
1299
1331
|
|
|
@@ -1325,6 +1357,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1325
1357
|
}
|
|
1326
1358
|
|
|
1327
1359
|
override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
|
|
1360
|
+
coordinatorLayout?.childDidClaimNativeGesture()
|
|
1328
1361
|
eventDispatcher?.let {
|
|
1329
1362
|
jsTouchDispatcher.onChildStartedNativeGesture(ev, it)
|
|
1330
1363
|
jsPointerDispatcher.onChildStartedNativeGesture(childView, ev, it)
|
|
@@ -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,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
71
72
|
*/
|
|
72
73
|
- (void)layoutFooter;
|
|
73
74
|
|
|
75
|
+
- (BOOL)hasAccessibilityFooterElements;
|
|
76
|
+
|
|
74
77
|
/**
|
|
75
78
|
* Setup scrollable content
|
|
76
79
|
*/
|
|
@@ -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 {
|
|
@@ -234,6 +262,7 @@ using namespace facebook::react;
|
|
|
234
262
|
#pragma mark - TrueSheetFooterViewDelegate
|
|
235
263
|
|
|
236
264
|
- (void)footerViewDidChangeSize:(CGSize)newSize {
|
|
265
|
+
[self.delegate containerViewFooterDidChangeSize:newSize];
|
|
237
266
|
if (@available(iOS 26.0, *)) {
|
|
238
267
|
[self setupEdgeInteractions];
|
|
239
268
|
}
|
|
@@ -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 {
|
package/ios/TrueSheetView.mm
CHANGED
|
@@ -373,6 +373,9 @@ using namespace facebook::react;
|
|
|
373
373
|
[_controller.view addSubview:_containerView];
|
|
374
374
|
[LayoutUtil pinView:_containerView toParentView:_controller.view edges:UIRectEdgeAll];
|
|
375
375
|
[_controller.view bringSubviewToFront:_containerView];
|
|
376
|
+
_containerView.accessibilityViewIsModal = YES;
|
|
377
|
+
_controller.accessibilityContentView = _containerView;
|
|
378
|
+
[_controller setupAccessibilityContainer];
|
|
376
379
|
|
|
377
380
|
CGFloat contentHeight = [_containerView contentHeight];
|
|
378
381
|
if (contentHeight > 0) {
|
|
@@ -570,6 +573,10 @@ using namespace facebook::react;
|
|
|
570
573
|
[self setupSheetDetentsForSizeChange];
|
|
571
574
|
}
|
|
572
575
|
|
|
576
|
+
- (void)containerViewFooterDidChangeSize:(CGSize)newSize {
|
|
577
|
+
[_controller setupAccessibilityContainer];
|
|
578
|
+
}
|
|
579
|
+
|
|
573
580
|
// When the ScrollView changes (e.g. conditional remount), re-pin the new ScrollView.
|
|
574
581
|
- (void)containerViewScrollViewDidChange {
|
|
575
582
|
[self setupScrollable];
|
|
@@ -75,9 +75,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
75
75
|
@property (nonatomic, assign) BOOL isPresented;
|
|
76
76
|
@property (nonatomic, assign) NSInteger activeDetentIndex;
|
|
77
77
|
@property (nonatomic, readonly) BOOL isTopmostPresentedController;
|
|
78
|
+
@property (nonatomic, weak, nullable) UIView *accessibilityContentView;
|
|
78
79
|
|
|
79
80
|
@property (nonatomic, readonly) CGFloat screenHeight;
|
|
80
81
|
|
|
82
|
+
- (void)setupAccessibilityContainer;
|
|
81
83
|
- (void)applyActiveDetent;
|
|
82
84
|
- (void)setupActiveDetentWithIndex:(NSInteger)index;
|
|
83
85
|
- (void)resizeToDetentIndex:(NSInteger)index;
|