@lodev09/react-native-true-sheet 3.11.9 → 3.11.11
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 +14 -2
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +7 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +30 -5
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +20 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +17 -4
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +12 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +30 -7
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +52 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/TouchEventDeduper.kt +31 -0
- package/ios/TrueSheetView.mm +3 -0
- package/ios/TrueSheetViewController.h +8 -0
- package/ios/TrueSheetViewController.mm +44 -9
- package/ios/core/TrueSheetDetentCalculator.h +6 -0
- package/ios/core/TrueSheetGrabberView.h +4 -0
- package/ios/core/TrueSheetGrabberView.mm +15 -5
- package/ios/core/TrueSheetKeyboardObserver.mm +15 -0
- package/lib/module/TrueSheet.js +27 -2
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +2 -1
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +12 -0
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +69 -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 +11 -0
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +1 -1
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TrueSheet.tsx +27 -1
- package/src/TrueSheet.types.ts +71 -0
- package/src/TrueSheet.web.tsx +4 -1
- package/src/fabric/TrueSheetViewNativeComponent.ts +12 -0
- package/src/navigation/types.ts +1 -0
|
@@ -12,6 +12,7 @@ import com.facebook.react.uimanager.ThemedReactContext
|
|
|
12
12
|
import com.facebook.react.uimanager.events.EventDispatcher
|
|
13
13
|
import com.facebook.react.views.view.ReactViewGroup
|
|
14
14
|
import com.lodev09.truesheet.core.TrueSheetCoordinatorLayout
|
|
15
|
+
import com.lodev09.truesheet.utils.TouchEventDeduper
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Delegate interface for footer view size changes and event dispatching
|
|
@@ -43,6 +44,7 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
|
|
|
43
44
|
|
|
44
45
|
private val jsTouchDispatcher = JSTouchDispatcher(this)
|
|
45
46
|
private var jsPointerDispatcher: JSPointerDispatcher? = null
|
|
47
|
+
private val touchDeduper = TouchEventDeduper()
|
|
46
48
|
|
|
47
49
|
init {
|
|
48
50
|
jsPointerDispatcher = JSPointerDispatcher(this)
|
|
@@ -62,19 +64,29 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
|
|
|
62
64
|
|
|
63
65
|
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
64
66
|
eventDispatcher?.let { dispatcher ->
|
|
65
|
-
|
|
67
|
+
if (touchDeduper.shouldDispatch(event)) {
|
|
68
|
+
jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
|
|
69
|
+
}
|
|
66
70
|
jsPointerDispatcher?.handleMotionEvent(event, dispatcher, true)
|
|
67
71
|
}
|
|
68
72
|
return super.onInterceptTouchEvent(event)
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
|
|
76
|
+
// Mirrors ReactSurfaceView: keep receiving onInterceptTouchEvent so
|
|
77
|
+
// jsTouchDispatcher sees the gesture end, but forward the request up the tree.
|
|
78
|
+
parent?.requestDisallowInterceptTouchEvent(disallowIntercept)
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
72
82
|
if (pointerEvents == PointerEvents.NONE || pointerEvents == PointerEvents.BOX_NONE) {
|
|
73
83
|
return false
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
eventDispatcher?.let { dispatcher ->
|
|
77
|
-
|
|
87
|
+
if (touchDeduper.shouldDispatch(event)) {
|
|
88
|
+
jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
|
|
89
|
+
}
|
|
78
90
|
jsPointerDispatcher?.handleMotionEvent(event, dispatcher, false)
|
|
79
91
|
}
|
|
80
92
|
super.onTouchEvent(event)
|
|
@@ -15,6 +15,7 @@ import com.facebook.react.uimanager.UIManagerHelper
|
|
|
15
15
|
import com.facebook.react.uimanager.events.EventDispatcher
|
|
16
16
|
import com.facebook.react.util.RNLog
|
|
17
17
|
import com.facebook.react.views.view.ReactViewGroup
|
|
18
|
+
import com.lodev09.truesheet.core.AccessibilityOptions
|
|
18
19
|
import com.lodev09.truesheet.core.GrabberOptions
|
|
19
20
|
import com.lodev09.truesheet.core.RNScreensEventObserver
|
|
20
21
|
import com.lodev09.truesheet.core.RNScreensEventObserverDelegate
|
|
@@ -223,6 +224,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
223
224
|
if (viewController.isPresented) {
|
|
224
225
|
viewController.setupDimmedBackground()
|
|
225
226
|
viewController.updateDimAmount()
|
|
227
|
+
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
226
228
|
}
|
|
227
229
|
}
|
|
228
230
|
|
|
@@ -232,6 +234,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
232
234
|
if (viewController.isPresented) {
|
|
233
235
|
viewController.setupDimmedBackground()
|
|
234
236
|
viewController.updateDimAmount()
|
|
237
|
+
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
235
238
|
}
|
|
236
239
|
}
|
|
237
240
|
|
|
@@ -261,6 +264,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
261
264
|
viewController.grabberOptions = options
|
|
262
265
|
}
|
|
263
266
|
|
|
267
|
+
fun setAccessibilityOptions(options: AccessibilityOptions?) {
|
|
268
|
+
viewController.accessibilityOptions = options
|
|
269
|
+
}
|
|
270
|
+
|
|
264
271
|
fun setSheetElevation(elevation: Float) {
|
|
265
272
|
viewController.sheetElevation = elevation
|
|
266
273
|
}
|
|
@@ -6,6 +6,7 @@ import android.os.Build
|
|
|
6
6
|
import android.view.MotionEvent
|
|
7
7
|
import android.view.View
|
|
8
8
|
import android.view.ViewGroup
|
|
9
|
+
import android.view.accessibility.AccessibilityEvent
|
|
9
10
|
import android.view.accessibility.AccessibilityNodeInfo
|
|
10
11
|
import android.widget.ImageView
|
|
11
12
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
@@ -22,6 +23,7 @@ import com.facebook.react.uimanager.events.EventDispatcher
|
|
|
22
23
|
import com.facebook.react.util.RNLog
|
|
23
24
|
import com.facebook.react.views.view.ReactViewGroup
|
|
24
25
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
26
|
+
import com.lodev09.truesheet.core.AccessibilityOptions
|
|
25
27
|
import com.lodev09.truesheet.core.GrabberOptions
|
|
26
28
|
import com.lodev09.truesheet.core.TrueSheetBottomSheetBehavior
|
|
27
29
|
import com.lodev09.truesheet.core.TrueSheetBottomSheetView
|
|
@@ -37,6 +39,7 @@ import com.lodev09.truesheet.core.TrueSheetKeyboardObserverDelegate
|
|
|
37
39
|
import com.lodev09.truesheet.core.TrueSheetStackManager
|
|
38
40
|
import com.lodev09.truesheet.utils.KeyboardUtils
|
|
39
41
|
import com.lodev09.truesheet.utils.ScreenUtils
|
|
42
|
+
import com.lodev09.truesheet.utils.TouchEventDeduper
|
|
40
43
|
|
|
41
44
|
// =============================================================================
|
|
42
45
|
// MARK: - Data Types & Delegate Protocol
|
|
@@ -197,6 +200,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
197
200
|
// the finger drifts outside the footer's rect mid-gesture.
|
|
198
201
|
private var footerOwnsTouchStream = false
|
|
199
202
|
|
|
203
|
+
private val touchDeduper = TouchEventDeduper()
|
|
204
|
+
|
|
200
205
|
private val eventDispatcher
|
|
201
206
|
get() = delegate?.eventDispatcher
|
|
202
207
|
|
|
@@ -212,6 +217,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
212
217
|
var dimmedDetentIndex = 0
|
|
213
218
|
override var grabber: Boolean = true
|
|
214
219
|
override var grabberOptions: GrabberOptions? = null
|
|
220
|
+
override var accessibilityOptions: AccessibilityOptions? = null
|
|
215
221
|
override var sheetBackgroundColor: Int? = null
|
|
216
222
|
var insetAdjustment: TrueSheetInsetAdjustment = TrueSheetInsetAdjustment.AUTOMATIC
|
|
217
223
|
|
|
@@ -310,7 +316,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
310
316
|
val bottomInset: Int
|
|
311
317
|
get() = if (edgeToEdgeEnabled) ScreenUtils.getInsets(reactContext).bottom else 0
|
|
312
318
|
|
|
313
|
-
val topInset: Int
|
|
319
|
+
override val topInset: Int
|
|
314
320
|
get() = if (edgeToEdgeEnabled) ScreenUtils.getInsets(reactContext).top else 0
|
|
315
321
|
|
|
316
322
|
override val contentBottomInset: Int
|
|
@@ -634,6 +640,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
634
640
|
if (!isKeyboardTransitioning) {
|
|
635
641
|
updateDimAmount(animated = true)
|
|
636
642
|
}
|
|
643
|
+
|
|
644
|
+
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
637
645
|
}
|
|
638
646
|
|
|
639
647
|
// =============================================================================
|
|
@@ -653,6 +661,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
653
661
|
|
|
654
662
|
isSheetVisible = false
|
|
655
663
|
wasHiddenByScreen = true
|
|
664
|
+
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
656
665
|
delegate?.viewControllerDidChangeVisibility(false)
|
|
657
666
|
dimViews.forEach { it.animate().alpha(0f).setDuration(SCREEN_FADE_DURATION).start() }
|
|
658
667
|
sheet.animate()
|
|
@@ -673,6 +682,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
673
682
|
setSheetVisibility(true)
|
|
674
683
|
sheetView?.alpha = 1f
|
|
675
684
|
updateDimAmount(animated = true)
|
|
685
|
+
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
676
686
|
}
|
|
677
687
|
|
|
678
688
|
/**
|
|
@@ -709,6 +719,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
709
719
|
sheet.setupBackground()
|
|
710
720
|
sheet.setupElevation()
|
|
711
721
|
sheet.setupGrabber()
|
|
722
|
+
sheet.setupAccessibility()
|
|
712
723
|
|
|
713
724
|
if (shouldAnimatePresent) {
|
|
714
725
|
isPresentAnimating = true
|
|
@@ -821,11 +832,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
821
832
|
delegate?.viewControllerDidFocus()
|
|
822
833
|
sheetView?.updateGrabberAccessibilityValue(index, detents.size)
|
|
823
834
|
|
|
835
|
+
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
836
|
+
// Move accessibility focus into the sheet
|
|
837
|
+
sheetView?.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
|
|
838
|
+
|
|
824
839
|
presentPromise?.invoke()
|
|
825
840
|
presentPromise = null
|
|
826
841
|
}
|
|
827
842
|
|
|
828
843
|
private fun finishDismiss() {
|
|
844
|
+
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
829
845
|
restoreFocusedView()
|
|
830
846
|
emitDidDismissEvents()
|
|
831
847
|
cleanupSheet()
|
|
@@ -1145,8 +1161,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1145
1161
|
// keyboard-expanded position and a non-keyboard detent position. If so, dismiss
|
|
1146
1162
|
// keyboard and commit to that detent.
|
|
1147
1163
|
val maxAvailableHeight = realScreenHeight - topInset
|
|
1148
|
-
val
|
|
1149
|
-
val keyboardTop = realScreenHeight - keyboardHeight
|
|
1164
|
+
val keyboardTop = detentCalculator.getSheetTopForDetentIndex(detents.size - 1)
|
|
1150
1165
|
|
|
1151
1166
|
for (i in detents.indices) {
|
|
1152
1167
|
val nonKeyboardHeight = minOf(detentCalculator.getDetentHeight(detents[i], includeKeyboard = false), maxAvailableHeight)
|
|
@@ -1342,15 +1357,25 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1342
1357
|
|
|
1343
1358
|
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
1344
1359
|
eventDispatcher?.let {
|
|
1345
|
-
|
|
1360
|
+
if (touchDeduper.shouldDispatch(event)) {
|
|
1361
|
+
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
1362
|
+
}
|
|
1346
1363
|
jsPointerDispatcher.handleMotionEvent(event, it, true)
|
|
1347
1364
|
}
|
|
1348
1365
|
return super.onInterceptTouchEvent(event)
|
|
1349
1366
|
}
|
|
1350
1367
|
|
|
1368
|
+
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
|
|
1369
|
+
// Mirrors ReactSurfaceView: keep receiving onInterceptTouchEvent so
|
|
1370
|
+
// jsTouchDispatcher sees the gesture end, but forward the request up the tree.
|
|
1371
|
+
parent?.requestDisallowInterceptTouchEvent(disallowIntercept)
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1351
1374
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
1352
1375
|
eventDispatcher?.let {
|
|
1353
|
-
|
|
1376
|
+
if (touchDeduper.shouldDispatch(event)) {
|
|
1377
|
+
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
1378
|
+
}
|
|
1354
1379
|
jsPointerDispatcher.handleMotionEvent(event, it, false)
|
|
1355
1380
|
}
|
|
1356
1381
|
super.onTouchEvent(event)
|
|
@@ -14,6 +14,7 @@ import com.facebook.react.uimanager.ViewManagerDelegate
|
|
|
14
14
|
import com.facebook.react.uimanager.annotations.ReactProp
|
|
15
15
|
import com.facebook.react.viewmanagers.TrueSheetViewManagerDelegate
|
|
16
16
|
import com.facebook.react.viewmanagers.TrueSheetViewManagerInterface
|
|
17
|
+
import com.lodev09.truesheet.core.AccessibilityOptions
|
|
17
18
|
import com.lodev09.truesheet.core.GrabberOptions
|
|
18
19
|
import com.lodev09.truesheet.events.*
|
|
19
20
|
|
|
@@ -136,6 +137,25 @@ class TrueSheetViewManager :
|
|
|
136
137
|
view.setGrabberOptions(grabberOptions)
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
@ReactProp(name = "accessibilityOptions")
|
|
141
|
+
override fun setAccessibilityOptions(view: TrueSheetView, options: ReadableMap?) {
|
|
142
|
+
if (options == null) {
|
|
143
|
+
view.setAccessibilityOptions(null)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
val accessibilityOptions = AccessibilityOptions(
|
|
148
|
+
grabberLabel = if (options.hasKey("grabberLabel")) options.getString("grabberLabel") else null,
|
|
149
|
+
expandedValue = if (options.hasKey("expandedValue")) options.getString("expandedValue") else null,
|
|
150
|
+
collapsedValue = if (options.hasKey("collapsedValue")) options.getString("collapsedValue") else null,
|
|
151
|
+
detentValue = if (options.hasKey("detentValue")) options.getString("detentValue") else null,
|
|
152
|
+
expandActionLabel = if (options.hasKey("expandActionLabel")) options.getString("expandActionLabel") else null,
|
|
153
|
+
collapseActionLabel = if (options.hasKey("collapseActionLabel")) options.getString("collapseActionLabel") else null,
|
|
154
|
+
paneTitle = if (options.hasKey("paneTitle")) options.getString("paneTitle") else null
|
|
155
|
+
)
|
|
156
|
+
view.setAccessibilityOptions(accessibilityOptions)
|
|
157
|
+
}
|
|
158
|
+
|
|
139
159
|
@ReactProp(name = "dismissible", defaultBoolean = true)
|
|
140
160
|
override fun setDismissible(view: TrueSheetView, dismissible: Boolean) {
|
|
141
161
|
view.setDismissible(dismissible)
|
|
@@ -31,6 +31,7 @@ interface TrueSheetBottomSheetViewDelegate {
|
|
|
31
31
|
val anchorOffset: Int
|
|
32
32
|
val grabber: Boolean
|
|
33
33
|
val grabberOptions: GrabberOptions?
|
|
34
|
+
val accessibilityOptions: AccessibilityOptions?
|
|
34
35
|
val draggable: Boolean
|
|
35
36
|
fun bottomSheetViewDidTapGrabber()
|
|
36
37
|
fun bottomSheetViewDidAccessibilityIncrement()
|
|
@@ -78,8 +79,6 @@ class TrueSheetBottomSheetView(private val reactContext: ThemedReactContext) : F
|
|
|
78
79
|
// Allow content to extend beyond bounds (for footer positioning)
|
|
79
80
|
clipChildren = false
|
|
80
81
|
clipToPadding = false
|
|
81
|
-
|
|
82
|
-
ViewCompat.setAccessibilityPaneTitle(this, "Bottom sheet")
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
override fun setTranslationY(translationY: Float) {
|
|
@@ -195,23 +194,37 @@ class TrueSheetBottomSheetView(private val reactContext: ThemedReactContext) : F
|
|
|
195
194
|
fun setupGrabber() {
|
|
196
195
|
findViewWithTag<View>(GRABBER_TAG)?.let { removeView(it) }
|
|
197
196
|
|
|
197
|
+
// Content view is the controller, added before the grabber (delegate is the controller)
|
|
198
|
+
val contentView = delegate as? View
|
|
199
|
+
|
|
198
200
|
val isEnabled = delegate?.grabber ?: true
|
|
199
201
|
val isDraggable = delegate?.draggable ?: true
|
|
200
|
-
if (!isEnabled || !isDraggable)
|
|
202
|
+
if (!isEnabled || !isDraggable) {
|
|
203
|
+
contentView?.accessibilityTraversalAfter = View.NO_ID
|
|
204
|
+
return
|
|
205
|
+
}
|
|
201
206
|
|
|
202
|
-
val grabberView = TrueSheetGrabberView(reactContext, delegate?.grabberOptions).apply {
|
|
207
|
+
val grabberView = TrueSheetGrabberView(reactContext, delegate?.grabberOptions, delegate?.accessibilityOptions).apply {
|
|
203
208
|
tag = GRABBER_TAG
|
|
209
|
+
id = View.generateViewId()
|
|
204
210
|
onAccessibilityIncrement = { delegate?.bottomSheetViewDidAccessibilityIncrement() }
|
|
205
211
|
onAccessibilityDecrement = { delegate?.bottomSheetViewDidAccessibilityDecrement() }
|
|
206
212
|
}
|
|
207
213
|
|
|
208
214
|
addView(grabberView)
|
|
215
|
+
|
|
216
|
+
// Grabber is added after content for z-ordering, but should be announced first
|
|
217
|
+
contentView?.accessibilityTraversalAfter = grabberView.id
|
|
209
218
|
}
|
|
210
219
|
|
|
211
220
|
fun updateGrabberAccessibilityValue(index: Int, detentCount: Int) {
|
|
212
221
|
findViewWithTag<TrueSheetGrabberView>(GRABBER_TAG)?.updateAccessibilityValue(index, detentCount)
|
|
213
222
|
}
|
|
214
223
|
|
|
224
|
+
fun setupAccessibility() {
|
|
225
|
+
ViewCompat.setAccessibilityPaneTitle(this, delegate?.accessibilityOptions?.paneTitle ?: "Bottom sheet")
|
|
226
|
+
}
|
|
227
|
+
|
|
215
228
|
// =============================================================================
|
|
216
229
|
// MARK: - Grabber Tap Detection
|
|
217
230
|
// =============================================================================
|
|
@@ -20,6 +20,7 @@ interface TrueSheetDetentCalculatorDelegate {
|
|
|
20
20
|
val contentBottomInset: Int
|
|
21
21
|
val maxContentHeight: Int?
|
|
22
22
|
val keyboardInset: Int
|
|
23
|
+
val topInset: Int
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -39,6 +40,7 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
39
40
|
private val contentBottomInset: Int get() = delegate?.contentBottomInset ?: 0
|
|
40
41
|
private val maxContentHeight: Int? get() = delegate?.maxContentHeight
|
|
41
42
|
private val keyboardInset: Int get() = delegate?.keyboardInset ?: 0
|
|
43
|
+
private val topInset: Int get() = delegate?.topInset ?: 0
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
46
|
* Height for peek (-2.0) detents: header + footer + peek content height.
|
|
@@ -79,7 +81,12 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
79
81
|
RNLog.w(reactContext, "TrueSheet: Detent index ($index) is out of bounds (0..${detents.size - 1})")
|
|
80
82
|
return realScreenHeight
|
|
81
83
|
}
|
|
82
|
-
|
|
84
|
+
// Clamp to the space the sheet can actually occupy — matching
|
|
85
|
+
// setupSheetDetents. A keyboard-inflated detent height can exceed it,
|
|
86
|
+
// placing the expected top above the screen while the real sheet stops
|
|
87
|
+
// at the top inset.
|
|
88
|
+
val maxAvailableHeight = realScreenHeight - topInset
|
|
89
|
+
return realScreenHeight - minOf(getDetentHeight(detents[index]), maxAvailableHeight)
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
/**
|
|
@@ -187,6 +194,10 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
187
194
|
val pos = getSheetTopForDetentIndex(i)
|
|
188
195
|
val nextPos = getSheetTopForDetentIndex(i + 1)
|
|
189
196
|
|
|
197
|
+
// Skip degenerate segments — keyboard growth can clamp adjacent detents
|
|
198
|
+
// to the same top; falling through reports the topmost detent
|
|
199
|
+
if (pos == nextPos) continue
|
|
200
|
+
|
|
190
201
|
if (positionPx in nextPos..pos) {
|
|
191
202
|
val range = pos - nextPos
|
|
192
203
|
val progress = if (range > 0) (pos - positionPx).toFloat() / range else 0f
|
|
@@ -26,12 +26,29 @@ data class GrabberOptions(
|
|
|
26
26
|
val adaptive: Boolean = true
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Options for customizing (e.g. localizing) accessibility strings.
|
|
31
|
+
*/
|
|
32
|
+
data class AccessibilityOptions(
|
|
33
|
+
val grabberLabel: String? = null,
|
|
34
|
+
val expandedValue: String? = null,
|
|
35
|
+
val collapsedValue: String? = null,
|
|
36
|
+
val detentValue: String? = null,
|
|
37
|
+
val expandActionLabel: String? = null,
|
|
38
|
+
val collapseActionLabel: String? = null,
|
|
39
|
+
val paneTitle: String? = null
|
|
40
|
+
)
|
|
41
|
+
|
|
29
42
|
/**
|
|
30
43
|
* Native grabber (drag handle) view for the bottom sheet.
|
|
31
44
|
* Displays a small pill-shaped indicator at the top of the sheet with a tappable hitbox.
|
|
32
45
|
*/
|
|
33
46
|
@SuppressLint("ViewConstructor")
|
|
34
|
-
class TrueSheetGrabberView(
|
|
47
|
+
class TrueSheetGrabberView(
|
|
48
|
+
context: Context,
|
|
49
|
+
private val options: GrabberOptions? = null,
|
|
50
|
+
private val accessibilityOptions: AccessibilityOptions? = null
|
|
51
|
+
) : FrameLayout(context) {
|
|
35
52
|
|
|
36
53
|
companion object {
|
|
37
54
|
private const val DEFAULT_WIDTH = 32f // dp
|
|
@@ -94,7 +111,7 @@ class TrueSheetGrabberView(context: Context, private val options: GrabberOptions
|
|
|
94
111
|
addView(pillView)
|
|
95
112
|
|
|
96
113
|
isFocusable = true
|
|
97
|
-
contentDescription = "
|
|
114
|
+
contentDescription = accessibilityOptions?.grabberLabel ?: "Drag handle"
|
|
98
115
|
|
|
99
116
|
accessibilityDelegate = object : View.AccessibilityDelegate() {
|
|
100
117
|
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {
|
|
@@ -102,13 +119,13 @@ class TrueSheetGrabberView(context: Context, private val options: GrabberOptions
|
|
|
102
119
|
info.addAction(
|
|
103
120
|
AccessibilityNodeInfo.AccessibilityAction(
|
|
104
121
|
AccessibilityNodeInfo.ACTION_SCROLL_FORWARD,
|
|
105
|
-
"Expand"
|
|
122
|
+
accessibilityOptions?.expandActionLabel ?: "Expand"
|
|
106
123
|
)
|
|
107
124
|
)
|
|
108
125
|
info.addAction(
|
|
109
126
|
AccessibilityNodeInfo.AccessibilityAction(
|
|
110
127
|
AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD,
|
|
111
|
-
"Collapse"
|
|
128
|
+
accessibilityOptions?.collapseActionLabel ?: "Collapse"
|
|
112
129
|
)
|
|
113
130
|
)
|
|
114
131
|
info.className = "android.widget.SeekBar"
|
|
@@ -134,9 +151,15 @@ class TrueSheetGrabberView(context: Context, private val options: GrabberOptions
|
|
|
134
151
|
fun updateAccessibilityValue(index: Int, detentCount: Int) {
|
|
135
152
|
val description: CharSequence? = when {
|
|
136
153
|
index < 0 || detentCount <= 0 -> null
|
|
137
|
-
|
|
138
|
-
index
|
|
139
|
-
|
|
154
|
+
|
|
155
|
+
index >= detentCount - 1 -> accessibilityOptions?.expandedValue ?: "Expanded"
|
|
156
|
+
|
|
157
|
+
index == 0 -> accessibilityOptions?.collapsedValue ?: "Collapsed"
|
|
158
|
+
|
|
159
|
+
else ->
|
|
160
|
+
(accessibilityOptions?.detentValue ?: "Detent {index} of {count}")
|
|
161
|
+
.replace("{index}", "${index + 1}")
|
|
162
|
+
.replace("{count}", "$detentCount")
|
|
140
163
|
}
|
|
141
164
|
ViewCompat.setStateDescription(this, description)
|
|
142
165
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.lodev09.truesheet.core
|
|
2
2
|
|
|
3
|
+
import android.view.View
|
|
3
4
|
import android.view.ViewGroup
|
|
4
5
|
import com.lodev09.truesheet.TrueSheetView
|
|
5
6
|
|
|
@@ -143,6 +144,57 @@ object TrueSheetStackManager {
|
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
private val savedBackgroundImportance = HashMap<View, Int>()
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Hides views behind the topmost dimmed (modal) sheet from accessibility,
|
|
151
|
+
* mirroring BottomSheetDialog behavior. Sheets live in the same window as
|
|
152
|
+
* the app content, so without this TalkBack can traverse behind the sheet.
|
|
153
|
+
*
|
|
154
|
+
* Derived from the current stack state and idempotent — call whenever a
|
|
155
|
+
* sheet presents, dismisses, settles, or changes its dim state.
|
|
156
|
+
*/
|
|
157
|
+
@JvmStatic
|
|
158
|
+
fun updateBackgroundAccessibility() {
|
|
159
|
+
synchronized(presentedSheetStack) {
|
|
160
|
+
val topDimmedIndex = presentedSheetStack.indexOfLast {
|
|
161
|
+
it.viewController.run { isPresented && isSheetVisible && !isBeingDismissed && isDimmedAtCurrentDetent }
|
|
162
|
+
}
|
|
163
|
+
val topDimmed = presentedSheetStack.getOrNull(topDimmedIndex)
|
|
164
|
+
val root = topDimmed?.viewController?.coordinatorLayout?.parent as? ViewGroup
|
|
165
|
+
|
|
166
|
+
if (root == null) {
|
|
167
|
+
restoreBackgroundAccessibility()
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// The dimmed sheet and any sheets above it stay accessible
|
|
172
|
+
val accessibleCoordinators = presentedSheetStack
|
|
173
|
+
.subList(topDimmedIndex, presentedSheetStack.size)
|
|
174
|
+
.mapNotNull { it.viewController.coordinatorLayout }
|
|
175
|
+
.toSet()
|
|
176
|
+
|
|
177
|
+
for (i in 0 until root.childCount) {
|
|
178
|
+
val child = root.getChildAt(i)
|
|
179
|
+
if (child in accessibleCoordinators) {
|
|
180
|
+
savedBackgroundImportance.remove(child)?.let { child.importantForAccessibility = it }
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
if (!savedBackgroundImportance.containsKey(child)) {
|
|
184
|
+
savedBackgroundImportance[child] = child.importantForAccessibility
|
|
185
|
+
}
|
|
186
|
+
child.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private fun restoreBackgroundAccessibility() {
|
|
192
|
+
savedBackgroundImportance.forEach { (view, importance) ->
|
|
193
|
+
view.importantForAccessibility = importance
|
|
194
|
+
}
|
|
195
|
+
savedBackgroundImportance.clear()
|
|
196
|
+
}
|
|
197
|
+
|
|
146
198
|
/**
|
|
147
199
|
* Returns the root presented sheet for dismissAll.
|
|
148
200
|
* Starts from the topmost sheet and walks up to find the root of its stack.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.lodev09.truesheet.utils
|
|
2
|
+
|
|
3
|
+
import android.view.MotionEvent
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Guards a nested RootView against dispatching the same [MotionEvent] to JS twice.
|
|
7
|
+
*
|
|
8
|
+
* An event that no child consumes reaches both `onInterceptTouchEvent` and `onTouchEvent`,
|
|
9
|
+
* so dispatching from both starts a second JS touch stream for the same gesture.
|
|
10
|
+
*
|
|
11
|
+
* A plain root can live with that, but the sheet's roots are nested: touches that hit no
|
|
12
|
+
* React child resolve to the root's own tag, and the second stream then finds that view
|
|
13
|
+
* already holding the responder, so React only asks its ancestors - handing the touch to
|
|
14
|
+
* touchables outside the sheet.
|
|
15
|
+
*/
|
|
16
|
+
internal class TouchEventDeduper {
|
|
17
|
+
private var downTime = Long.MIN_VALUE
|
|
18
|
+
private var eventTime = Long.MIN_VALUE
|
|
19
|
+
private var action = -1
|
|
20
|
+
|
|
21
|
+
fun shouldDispatch(event: MotionEvent): Boolean {
|
|
22
|
+
if (event.downTime == downTime && event.eventTime == eventTime && event.action == action) {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
downTime = event.downTime
|
|
27
|
+
eventTime = event.eventTime
|
|
28
|
+
action = event.action
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
}
|
package/ios/TrueSheetView.mm
CHANGED
|
@@ -230,6 +230,9 @@ using namespace facebook::react;
|
|
|
230
230
|
_controller.grabberOptions = nil;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
// Accessibility options - applied to the custom grabber
|
|
234
|
+
_controller.accessibilityOptions = newProps.accessibilityOptions;
|
|
235
|
+
|
|
233
236
|
_controller.presentation = newProps.presentation;
|
|
234
237
|
_controller.dismissible = newProps.dismissible;
|
|
235
238
|
_controller.draggable = newProps.draggable;
|
|
@@ -63,6 +63,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
63
63
|
@property (nonatomic, strong, nullable) NSNumber *cornerRadius;
|
|
64
64
|
@property (nonatomic, assign) BOOL grabber;
|
|
65
65
|
@property (nonatomic, strong, nullable) GrabberOptions *grabberOptions;
|
|
66
|
+
@property (nonatomic, assign) facebook::react::TrueSheetViewAccessibilityOptionsStruct accessibilityOptions;
|
|
66
67
|
@property (nonatomic, assign) BOOL draggable;
|
|
67
68
|
@property (nonatomic, assign) BOOL dimmed;
|
|
68
69
|
@property (nonatomic, strong, nullable) NSNumber *dimmedDetentIndex;
|
|
@@ -77,6 +78,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
77
78
|
@property (nonatomic, assign) BOOL dismissible;
|
|
78
79
|
@property (nonatomic, assign) BOOL isPresented;
|
|
79
80
|
@property (nonatomic, assign) NSInteger activeDetentIndex;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* YES while the keyboard has the sheet grown beyond its detent height.
|
|
84
|
+
* Offset learning is skipped in this state — the measured frame would bake
|
|
85
|
+
* ~keyboardHeight into the learned offset.
|
|
86
|
+
*/
|
|
87
|
+
@property (nonatomic, assign) BOOL keyboardSheetGrown;
|
|
80
88
|
@property (nonatomic, readonly) BOOL isTopmostPresentedController;
|
|
81
89
|
@property (nonatomic, weak, nullable) UIView *accessibilityContentView;
|
|
82
90
|
|
|
@@ -460,6 +460,10 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
460
460
|
[self endInteractiveDismissState];
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
+
// Dismissing with the keyboard up skips the hide notification — don't let
|
|
464
|
+
// the stale flag block learning on the next present.
|
|
465
|
+
_keyboardSheetGrown = NO;
|
|
466
|
+
|
|
463
467
|
[self emitDidDismissEvents];
|
|
464
468
|
}
|
|
465
469
|
|
|
@@ -478,6 +482,21 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
478
482
|
} else {
|
|
479
483
|
UIViewController *presented = self.presentedViewController;
|
|
480
484
|
BOOL hasPresentedController = presented != nil && !presented.isBeingPresented && !presented.isBeingDismissed;
|
|
485
|
+
|
|
486
|
+
// A layout pass at rest can nudge the frame by a sub-pixel amount
|
|
487
|
+
// (pixel-grid snapping after present), leaving the interpolated index
|
|
488
|
+
// slightly off the whole number. Re-learn when the frame is effectively
|
|
489
|
+
// at the current detent so the drift is absorbed instead of emitted.
|
|
490
|
+
// The tight threshold keeps real movement from being absorbed.
|
|
491
|
+
if (presented == nil && !_isDragging) {
|
|
492
|
+
NSInteger index = self.currentDetentIndex;
|
|
493
|
+
CGFloat expectedHeight = [_detentCalculator resolvedHeightForIndex:index];
|
|
494
|
+
CGFloat actualHeight = self.screenHeight - self.currentPosition;
|
|
495
|
+
if (expectedHeight > 0 && fabs(actualHeight - expectedHeight) < 2) {
|
|
496
|
+
[self learnOffsetForDetentIndex:index];
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
481
500
|
[self emitChangePositionDelegateWithPosition:self.currentPosition
|
|
482
501
|
realtime:!hasPresentedController
|
|
483
502
|
debug:@"layout"];
|
|
@@ -636,13 +655,13 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
636
655
|
strongSelf->_isTransitioning = NO;
|
|
637
656
|
strongSelf->_isTransitionSnapping = NO;
|
|
638
657
|
|
|
639
|
-
//
|
|
658
|
+
// Settle after a present or detent-snap transition.
|
|
640
659
|
// Uses dispatch_after because presentedView frame isn't final until UIKit
|
|
641
|
-
// completes its layout pass after the transition animation
|
|
660
|
+
// completes its layout pass after the transition animation — learning
|
|
661
|
+
// here absorbs any sub-pixel drift since the earlier learns.
|
|
642
662
|
if (strongSelf->_isPresented) {
|
|
643
663
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
644
|
-
|
|
645
|
-
[strongSelf emitChangePositionDelegateWithPosition:position realtime:NO debug:@"transition end"];
|
|
664
|
+
[strongSelf settleAtDetentIndex:strongSelf.currentDetentIndex debug:@"transition end"];
|
|
646
665
|
});
|
|
647
666
|
}
|
|
648
667
|
}];
|
|
@@ -798,7 +817,10 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
798
817
|
.index = [self interpolatedIndexForPosition:position],
|
|
799
818
|
};
|
|
800
819
|
|
|
801
|
-
|
|
820
|
+
// Settle (non-realtime) emits are authoritative and bypass the dedupe — a
|
|
821
|
+
// learn right before them can correct the interpolated index by less than
|
|
822
|
+
// the tolerance, and the corrected value must still reach JS.
|
|
823
|
+
if (!realtime || !TrueSheetPositionStateEquals(_lastEmittedPositionState, state)) {
|
|
802
824
|
_lastEmittedPositionState = state;
|
|
803
825
|
|
|
804
826
|
[self.delegate viewControllerDidChangePosition:state.index
|
|
@@ -809,9 +831,24 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
809
831
|
}
|
|
810
832
|
|
|
811
833
|
- (void)learnOffsetForDetentIndex:(NSInteger)index {
|
|
834
|
+
if (_keyboardSheetGrown) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
812
837
|
[_detentCalculator learnOffsetForDetentIndex:index];
|
|
813
838
|
}
|
|
814
839
|
|
|
840
|
+
- (void)setKeyboardSheetGrown:(BOOL)keyboardSheetGrown {
|
|
841
|
+
BOOL wasGrown = _keyboardSheetGrown;
|
|
842
|
+
_keyboardSheetGrown = keyboardSheetGrown;
|
|
843
|
+
|
|
844
|
+
// Settles are skipped or measure a mid-animation frame while the keyboard
|
|
845
|
+
// has the sheet grown (e.g. a drag-end during the shrink-back) — re-settle
|
|
846
|
+
// at the resting detent once the keyboard is fully away.
|
|
847
|
+
if (wasGrown && !keyboardSheetGrown && self.isPresented && !_isDragging) {
|
|
848
|
+
[self settleAtDetentIndex:self.currentDetentIndex debug:@"keyboard settled"];
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
815
852
|
/**
|
|
816
853
|
* Learn the resolver-vs-actual offset at a settled detent, then emit the corrected
|
|
817
854
|
* position. Only valid when presentedView's frame is final — never mid-drag or
|
|
@@ -1043,14 +1080,11 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1043
1080
|
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_1) && !TARGET_OS_MACCATALYST
|
|
1044
1081
|
if (@available(iOS 26.1, *)) {
|
|
1045
1082
|
if (!self.isDesignCompatibilityMode) {
|
|
1046
|
-
if (self.backgroundColor) {
|
|
1047
|
-
self.sheet.backgroundEffect = [UIColorEffect effectWithColor:self.backgroundColor];
|
|
1048
|
-
} else if (hasBlur) {
|
|
1083
|
+
if (self.backgroundColor || hasBlur) {
|
|
1049
1084
|
self.sheet.backgroundEffect = [UIColorEffect effectWithColor:[UIColor clearColor]];
|
|
1050
1085
|
} else {
|
|
1051
1086
|
self.sheet.backgroundEffect = nil;
|
|
1052
1087
|
}
|
|
1053
|
-
return;
|
|
1054
1088
|
}
|
|
1055
1089
|
}
|
|
1056
1090
|
#endif
|
|
@@ -1065,6 +1099,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1065
1099
|
self.sheet.prefersGrabberVisible = NO;
|
|
1066
1100
|
|
|
1067
1101
|
GrabberOptions *options = self.grabberOptions;
|
|
1102
|
+
_grabberView.accessibilityOptions = self.accessibilityOptions;
|
|
1068
1103
|
_grabberView.grabberWidth = options.width;
|
|
1069
1104
|
_grabberView.grabberHeight = options.height;
|
|
1070
1105
|
_grabberView.topMargin = options.topMargin;
|