@lodev09/react-native-true-sheet 3.11.0-beta.9 → 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/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 +11 -1
- 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 +13 -3
- 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()
|
|
@@ -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;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
|
|
9
9
|
#import "TrueSheetViewController.h"
|
|
10
|
+
#import "TrueSheetContainerView.h"
|
|
10
11
|
#import "TrueSheetContentView.h"
|
|
11
12
|
#import "core/TrueSheetBlurView.h"
|
|
12
13
|
#import "core/TrueSheetDetentCalculator.h"
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
|
|
19
20
|
#import <React/RCTLog.h>
|
|
20
21
|
#import <React/RCTScrollViewComponentView.h>
|
|
22
|
+
#import <objc/runtime.h>
|
|
21
23
|
#import <react/renderer/components/TrueSheetSpec/Props.h>
|
|
22
24
|
|
|
23
25
|
using namespace facebook::react;
|
|
@@ -32,8 +34,16 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
|
|
|
32
34
|
return fabs(a.position - b.position) <= 0.01 && fabs(a.detent - b.detent) <= 0.01 && fabs(a.index - b.index) <= 0.01;
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
static char TrueSheetAccessibilityWindowOwnerKey;
|
|
38
|
+
static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
39
|
+
|
|
35
40
|
@interface TrueSheetViewController ()
|
|
36
41
|
|
|
42
|
+
- (UIViewController *)accessibilityPresentingViewController;
|
|
43
|
+
- (void)restoreWindowAccessibilityElements;
|
|
44
|
+
- (void)setSheetAccessibilityElementsHidden:(BOOL)hidden;
|
|
45
|
+
- (void)setAccessibilityContentElement:(UIView *)contentView;
|
|
46
|
+
|
|
37
47
|
@end
|
|
38
48
|
|
|
39
49
|
@implementation TrueSheetViewController {
|
|
@@ -55,6 +65,8 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
|
|
|
55
65
|
|
|
56
66
|
UIView *_anchorView;
|
|
57
67
|
|
|
68
|
+
__weak UIWindow *_accessibilityWindow;
|
|
69
|
+
|
|
58
70
|
TrueSheetBlurView *_blurView;
|
|
59
71
|
TrueSheetGrabberView *_grabberView;
|
|
60
72
|
TrueSheetDetentCalculator *_detentCalculator;
|
|
@@ -95,6 +107,7 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
|
|
|
95
107
|
}
|
|
96
108
|
|
|
97
109
|
- (void)dealloc {
|
|
110
|
+
[self restoreWindowAccessibilityElements];
|
|
98
111
|
[_transitioningTimer invalidate];
|
|
99
112
|
_transitioningTimer = nil;
|
|
100
113
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
@@ -207,6 +220,7 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
|
|
|
207
220
|
if ([presenter isKindOfClass:[TrueSheetViewController class]]) {
|
|
208
221
|
_parentSheetController = (TrueSheetViewController *)presenter;
|
|
209
222
|
[_parentSheetController.delegate viewControllerWillBlur];
|
|
223
|
+
[_parentSheetController setAccessibilityContentElement:self.accessibilityContentView ?: self.view];
|
|
210
224
|
}
|
|
211
225
|
|
|
212
226
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
@@ -224,6 +238,7 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
|
|
|
224
238
|
|
|
225
239
|
- (void)viewDidAppear:(BOOL)animated {
|
|
226
240
|
[super viewDidAppear:animated];
|
|
241
|
+
[self setSheetAccessibilityElementsHidden:NO];
|
|
227
242
|
|
|
228
243
|
if (!_isPresented) {
|
|
229
244
|
[_parentSheetController.delegate viewControllerDidBlur];
|
|
@@ -243,6 +258,137 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
|
|
|
243
258
|
[self setupGestureRecognizer];
|
|
244
259
|
_isPresented = YES;
|
|
245
260
|
}
|
|
261
|
+
|
|
262
|
+
[self setupAccessibilityContainer];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
- (void)setupAccessibilityContainer {
|
|
266
|
+
UIView *contentView = self.accessibilityContentView;
|
|
267
|
+
if (!contentView) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
[self setAccessibilityContentElement:contentView];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
- (UIViewController *)accessibilityPresentingViewController {
|
|
275
|
+
UIViewController *presentingViewController = self.presentingViewController;
|
|
276
|
+
while ([presentingViewController isKindOfClass:[TrueSheetViewController class]] &&
|
|
277
|
+
presentingViewController.presentingViewController) {
|
|
278
|
+
presentingViewController = presentingViewController.presentingViewController;
|
|
279
|
+
}
|
|
280
|
+
return presentingViewController;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
- (void)restoreWindowAccessibilityElements {
|
|
284
|
+
UIWindow *window = _accessibilityWindow;
|
|
285
|
+
if (window) {
|
|
286
|
+
// The active sheet may temporarily own window accessibility traversal. Only
|
|
287
|
+
// restore the previous value when this controller still owns that override.
|
|
288
|
+
NSValue *ownerValue = objc_getAssociatedObject(window, &TrueSheetAccessibilityWindowOwnerKey);
|
|
289
|
+
if (ownerValue && ownerValue.nonretainedObjectValue == self) {
|
|
290
|
+
id previousElements = objc_getAssociatedObject(window, &TrueSheetAccessibilityWindowPreviousElementsKey);
|
|
291
|
+
window.accessibilityElements = previousElements == [NSNull null] ? nil : previousElements;
|
|
292
|
+
objc_setAssociatedObject(window, &TrueSheetAccessibilityWindowOwnerKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
293
|
+
objc_setAssociatedObject(
|
|
294
|
+
window, &TrueSheetAccessibilityWindowPreviousElementsKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_accessibilityWindow = nil;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
- (void)setSheetAccessibilityElementsHidden:(BOOL)hidden {
|
|
302
|
+
self.view.accessibilityElementsHidden = hidden;
|
|
303
|
+
|
|
304
|
+
UIView *presentedView = self.presentationController.presentedView;
|
|
305
|
+
if (presentedView) {
|
|
306
|
+
presentedView.accessibilityElementsHidden = hidden;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
- (void)setAccessibilityContentElement:(UIView *)contentView {
|
|
311
|
+
BOOL hasAccessibilityFooterElements = [contentView isKindOfClass:[TrueSheetContainerView class]] &&
|
|
312
|
+
[(TrueSheetContainerView *)contentView hasAccessibilityFooterElements];
|
|
313
|
+
// Footer controls exposed as separate accessibility elements can be skipped
|
|
314
|
+
// by XCTest when the presented sheet is a hard modal accessibility boundary.
|
|
315
|
+
BOOL isAccessibilityModal = _dimmed && !hasAccessibilityFooterElements;
|
|
316
|
+
|
|
317
|
+
// At a hard modal boundary XCTest skips nested elements, so flatten the
|
|
318
|
+
// container's children into the array. Otherwise expose the container itself:
|
|
319
|
+
// its accessibilityElements getter recomputes live, so a footer/content subtree
|
|
320
|
+
// that remounts (e.g. swapping a footer button on a state change) stays
|
|
321
|
+
// discoverable instead of leaving this snapshot pointing at destroyed views.
|
|
322
|
+
NSArray *accessibilityElements;
|
|
323
|
+
if (isAccessibilityModal) {
|
|
324
|
+
NSArray *contentElements = contentView.accessibilityElements;
|
|
325
|
+
accessibilityElements = contentElements.count > 0 ? contentElements : @[ contentView ];
|
|
326
|
+
} else {
|
|
327
|
+
accessibilityElements = @[ contentView ];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
self.view.isAccessibilityElement = NO;
|
|
331
|
+
self.view.accessibilityViewIsModal = YES;
|
|
332
|
+
self.view.accessibilityElements = accessibilityElements;
|
|
333
|
+
|
|
334
|
+
UIView *presentedView = self.presentationController.presentedView;
|
|
335
|
+
if (presentedView) {
|
|
336
|
+
presentedView.isAccessibilityElement = NO;
|
|
337
|
+
presentedView.accessibilityViewIsModal = isAccessibilityModal;
|
|
338
|
+
presentedView.accessibilityElements = accessibilityElements;
|
|
339
|
+
|
|
340
|
+
UIWindow *window = self.view.window;
|
|
341
|
+
UIViewController *presentingViewController = [self accessibilityPresentingViewController];
|
|
342
|
+
if (window && presentingViewController.view) {
|
|
343
|
+
if (!_accessibilityWindow) {
|
|
344
|
+
_accessibilityWindow = window;
|
|
345
|
+
id previousElements = objc_getAssociatedObject(window, &TrueSheetAccessibilityWindowPreviousElementsKey);
|
|
346
|
+
if (!previousElements) {
|
|
347
|
+
previousElements = window.accessibilityElements ?: [NSNull null];
|
|
348
|
+
objc_setAssociatedObject(window, &TrueSheetAccessibilityWindowPreviousElementsKey, previousElements,
|
|
349
|
+
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
objc_setAssociatedObject(window, &TrueSheetAccessibilityWindowOwnerKey, [NSValue valueWithNonretainedObject:self],
|
|
353
|
+
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
354
|
+
window.accessibilityElements =
|
|
355
|
+
isAccessibilityModal ? @[ presentedView ] : @[ presentingViewController.view, presentedView ];
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
[self restoreWindowAccessibilityElements];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
#pragma mark - Presentation
|
|
364
|
+
|
|
365
|
+
- (void)presentViewController:(UIViewController *)viewControllerToPresent
|
|
366
|
+
animated:(BOOL)flag
|
|
367
|
+
completion:(void (^)(void))completion {
|
|
368
|
+
// A non-sheet controller (e.g. an action sheet, alert, or image picker) is about
|
|
369
|
+
// to present over us. Clear our window accessibility override so UIKit's default
|
|
370
|
+
// traversal surfaces it for XCTest and assistive technologies; we reclaim the
|
|
371
|
+
// override when it dismisses. Nested sheets claim their own override.
|
|
372
|
+
if (![viewControllerToPresent isKindOfClass:[TrueSheetViewController class]]) {
|
|
373
|
+
self.view.window.accessibilityElements = nil;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
[super presentViewController:viewControllerToPresent animated:flag completion:completion];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
|
|
380
|
+
[super dismissViewControllerAnimated:flag
|
|
381
|
+
completion:^{
|
|
382
|
+
if (completion) {
|
|
383
|
+
completion();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Reclaim the window accessibility override once nothing
|
|
387
|
+
// else is presented over us.
|
|
388
|
+
if (self.isPresented && self.presentedViewController == nil) {
|
|
389
|
+
[self setupAccessibilityContainer];
|
|
390
|
+
}
|
|
391
|
+
}];
|
|
246
392
|
}
|
|
247
393
|
|
|
248
394
|
- (void)emitWillDismissEvents {
|
|
@@ -257,6 +403,7 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
|
|
|
257
403
|
|
|
258
404
|
- (void)emitDidDismissEvents {
|
|
259
405
|
if (self.isBeingDismissed) {
|
|
406
|
+
[self restoreWindowAccessibilityElements];
|
|
260
407
|
_isPresented = NO;
|
|
261
408
|
_isWillDismissEmitted = NO;
|
|
262
409
|
|
|
@@ -264,6 +411,8 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
|
|
|
264
411
|
_anchorView = nil;
|
|
265
412
|
|
|
266
413
|
[_parentSheetController.delegate viewControllerDidFocus];
|
|
414
|
+
[_parentSheetController setSheetAccessibilityElementsHidden:NO];
|
|
415
|
+
[_parentSheetController setupAccessibilityContainer];
|
|
267
416
|
_parentSheetController = nil;
|
|
268
417
|
|
|
269
418
|
[self.delegate viewControllerDidBlur];
|
|
@@ -273,6 +422,8 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
|
|
|
273
422
|
|
|
274
423
|
- (void)viewWillDisappear:(BOOL)animated {
|
|
275
424
|
[super viewWillDisappear:animated];
|
|
425
|
+
[self restoreWindowAccessibilityElements];
|
|
426
|
+
[self setSheetAccessibilityElementsHidden:YES];
|
|
276
427
|
|
|
277
428
|
// Dispatch to allow pan gesture to set _isDragging before checking
|
|
278
429
|
// handleTransitionTracker will emit when sheet is transitioning to dismiss
|