@lodev09/react-native-true-sheet 3.11.9 → 3.11.10
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 +6 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +7 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +21 -3
- 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/ios/TrueSheetView.mm +3 -0
- package/ios/TrueSheetViewController.h +8 -0
- package/ios/TrueSheetViewController.mm +43 -5
- 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 +20 -1
- 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 +22 -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
|
@@ -68,6 +68,12 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
|
|
|
68
68
|
return super.onInterceptTouchEvent(event)
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
|
|
72
|
+
// Mirrors ReactSurfaceView: keep receiving onInterceptTouchEvent so
|
|
73
|
+
// jsTouchDispatcher sees the gesture end, but forward the request up the tree.
|
|
74
|
+
parent?.requestDisallowInterceptTouchEvent(disallowIntercept)
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
72
78
|
if (pointerEvents == PointerEvents.NONE || pointerEvents == PointerEvents.BOX_NONE) {
|
|
73
79
|
return false
|
|
@@ -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
|
|
@@ -212,6 +214,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
212
214
|
var dimmedDetentIndex = 0
|
|
213
215
|
override var grabber: Boolean = true
|
|
214
216
|
override var grabberOptions: GrabberOptions? = null
|
|
217
|
+
override var accessibilityOptions: AccessibilityOptions? = null
|
|
215
218
|
override var sheetBackgroundColor: Int? = null
|
|
216
219
|
var insetAdjustment: TrueSheetInsetAdjustment = TrueSheetInsetAdjustment.AUTOMATIC
|
|
217
220
|
|
|
@@ -310,7 +313,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
310
313
|
val bottomInset: Int
|
|
311
314
|
get() = if (edgeToEdgeEnabled) ScreenUtils.getInsets(reactContext).bottom else 0
|
|
312
315
|
|
|
313
|
-
val topInset: Int
|
|
316
|
+
override val topInset: Int
|
|
314
317
|
get() = if (edgeToEdgeEnabled) ScreenUtils.getInsets(reactContext).top else 0
|
|
315
318
|
|
|
316
319
|
override val contentBottomInset: Int
|
|
@@ -634,6 +637,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
634
637
|
if (!isKeyboardTransitioning) {
|
|
635
638
|
updateDimAmount(animated = true)
|
|
636
639
|
}
|
|
640
|
+
|
|
641
|
+
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
637
642
|
}
|
|
638
643
|
|
|
639
644
|
// =============================================================================
|
|
@@ -653,6 +658,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
653
658
|
|
|
654
659
|
isSheetVisible = false
|
|
655
660
|
wasHiddenByScreen = true
|
|
661
|
+
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
656
662
|
delegate?.viewControllerDidChangeVisibility(false)
|
|
657
663
|
dimViews.forEach { it.animate().alpha(0f).setDuration(SCREEN_FADE_DURATION).start() }
|
|
658
664
|
sheet.animate()
|
|
@@ -673,6 +679,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
673
679
|
setSheetVisibility(true)
|
|
674
680
|
sheetView?.alpha = 1f
|
|
675
681
|
updateDimAmount(animated = true)
|
|
682
|
+
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
676
683
|
}
|
|
677
684
|
|
|
678
685
|
/**
|
|
@@ -709,6 +716,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
709
716
|
sheet.setupBackground()
|
|
710
717
|
sheet.setupElevation()
|
|
711
718
|
sheet.setupGrabber()
|
|
719
|
+
sheet.setupAccessibility()
|
|
712
720
|
|
|
713
721
|
if (shouldAnimatePresent) {
|
|
714
722
|
isPresentAnimating = true
|
|
@@ -821,11 +829,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
821
829
|
delegate?.viewControllerDidFocus()
|
|
822
830
|
sheetView?.updateGrabberAccessibilityValue(index, detents.size)
|
|
823
831
|
|
|
832
|
+
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
833
|
+
// Move accessibility focus into the sheet
|
|
834
|
+
sheetView?.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
|
|
835
|
+
|
|
824
836
|
presentPromise?.invoke()
|
|
825
837
|
presentPromise = null
|
|
826
838
|
}
|
|
827
839
|
|
|
828
840
|
private fun finishDismiss() {
|
|
841
|
+
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
829
842
|
restoreFocusedView()
|
|
830
843
|
emitDidDismissEvents()
|
|
831
844
|
cleanupSheet()
|
|
@@ -1145,8 +1158,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1145
1158
|
// keyboard-expanded position and a non-keyboard detent position. If so, dismiss
|
|
1146
1159
|
// keyboard and commit to that detent.
|
|
1147
1160
|
val maxAvailableHeight = realScreenHeight - topInset
|
|
1148
|
-
val
|
|
1149
|
-
val keyboardTop = realScreenHeight - keyboardHeight
|
|
1161
|
+
val keyboardTop = detentCalculator.getSheetTopForDetentIndex(detents.size - 1)
|
|
1150
1162
|
|
|
1151
1163
|
for (i in detents.indices) {
|
|
1152
1164
|
val nonKeyboardHeight = minOf(detentCalculator.getDetentHeight(detents[i], includeKeyboard = false), maxAvailableHeight)
|
|
@@ -1348,6 +1360,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1348
1360
|
return super.onInterceptTouchEvent(event)
|
|
1349
1361
|
}
|
|
1350
1362
|
|
|
1363
|
+
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
|
|
1364
|
+
// Mirrors ReactSurfaceView: keep receiving onInterceptTouchEvent so
|
|
1365
|
+
// jsTouchDispatcher sees the gesture end, but forward the request up the tree.
|
|
1366
|
+
parent?.requestDisallowInterceptTouchEvent(disallowIntercept)
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1351
1369
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
1352
1370
|
eventDispatcher?.let {
|
|
1353
1371
|
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
@@ -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.
|
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
|
|
@@ -1065,6 +1102,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1065
1102
|
self.sheet.prefersGrabberVisible = NO;
|
|
1066
1103
|
|
|
1067
1104
|
GrabberOptions *options = self.grabberOptions;
|
|
1105
|
+
_grabberView.accessibilityOptions = self.accessibilityOptions;
|
|
1068
1106
|
_grabberView.grabberWidth = options.width;
|
|
1069
1107
|
_grabberView.grabberHeight = options.height;
|
|
1070
1108
|
_grabberView.topMargin = options.topMargin;
|
|
@@ -55,6 +55,12 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
55
55
|
*/
|
|
56
56
|
- (void)learnOffsetForDetentIndex:(NSInteger)index;
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
Returns the expected sheet height for a detent index: the UIKit-resolved
|
|
60
|
+
height plus the learned offset.
|
|
61
|
+
*/
|
|
62
|
+
- (CGFloat)resolvedHeightForIndex:(NSInteger)index;
|
|
63
|
+
|
|
58
64
|
/**
|
|
59
65
|
Finds the segment between detents for a given position.
|
|
60
66
|
Returns YES if position is between two detents, NO if at edges.
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
|
|
9
9
|
#import <UIKit/UIKit.h>
|
|
10
|
+
#import <react/renderer/components/TrueSheetSpec/Props.h>
|
|
10
11
|
|
|
11
12
|
NS_ASSUME_NONNULL_BEGIN
|
|
12
13
|
|
|
@@ -45,6 +46,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
45
46
|
/// Whether the grabber color adapts to the background (default: YES)
|
|
46
47
|
@property (nonatomic, strong, nullable) NSNumber *adaptive;
|
|
47
48
|
|
|
49
|
+
/// Accessibility strings (defaults come from the codegen prop defaults)
|
|
50
|
+
@property (nonatomic, assign) facebook::react::TrueSheetViewAccessibilityOptionsStruct accessibilityOptions;
|
|
51
|
+
|
|
48
52
|
/// Called when the grabber is tapped
|
|
49
53
|
@property (nonatomic, copy, nullable) void (^onTap)(void);
|
|
50
54
|
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
#import "TrueSheetGrabberView.h"
|
|
10
10
|
|
|
11
|
+
#import <React/RCTConversions.h>
|
|
12
|
+
|
|
13
|
+
using namespace facebook::react;
|
|
14
|
+
|
|
11
15
|
@implementation GrabberOptions
|
|
12
16
|
|
|
13
17
|
- (instancetype)init {
|
|
@@ -66,9 +70,7 @@ static const CGFloat kHitPaddingVertical = 10.0;
|
|
|
66
70
|
- (void)setupView {
|
|
67
71
|
self.clipsToBounds = NO;
|
|
68
72
|
self.isAccessibilityElement = YES;
|
|
69
|
-
self.accessibilityLabel = @"Sheet Grabber";
|
|
70
73
|
self.accessibilityTraits = UIAccessibilityTraitAdjustable | UIAccessibilityTraitButton;
|
|
71
|
-
self.accessibilityHint = @"Double-tap to expand. Swipe up or down to resize the sheet";
|
|
72
74
|
|
|
73
75
|
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap)];
|
|
74
76
|
[self addGestureRecognizer:tap];
|
|
@@ -103,6 +105,12 @@ static const CGFloat kHitPaddingVertical = 10.0;
|
|
|
103
105
|
|
|
104
106
|
#pragma mark - Public
|
|
105
107
|
|
|
108
|
+
- (void)setAccessibilityOptions:(TrueSheetViewAccessibilityOptionsStruct)accessibilityOptions {
|
|
109
|
+
_accessibilityOptions = accessibilityOptions;
|
|
110
|
+
self.accessibilityLabel = RCTNSStringFromStringNilIfEmpty(accessibilityOptions.grabberLabel);
|
|
111
|
+
self.accessibilityHint = RCTNSStringFromStringNilIfEmpty(accessibilityOptions.grabberHint);
|
|
112
|
+
}
|
|
113
|
+
|
|
106
114
|
- (void)updateAccessibilityValueWithIndex:(NSInteger)index detentCount:(NSInteger)count {
|
|
107
115
|
if (index < 0 || count <= 0) {
|
|
108
116
|
self.accessibilityValue = nil;
|
|
@@ -110,11 +118,13 @@ static const CGFloat kHitPaddingVertical = 10.0;
|
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
if (index >= count - 1) {
|
|
113
|
-
self.accessibilityValue =
|
|
121
|
+
self.accessibilityValue = RCTNSStringFromStringNilIfEmpty(_accessibilityOptions.expandedValue);
|
|
114
122
|
} else if (index == 0) {
|
|
115
|
-
self.accessibilityValue =
|
|
123
|
+
self.accessibilityValue = RCTNSStringFromStringNilIfEmpty(_accessibilityOptions.collapsedValue);
|
|
116
124
|
} else {
|
|
117
|
-
|
|
125
|
+
NSString *value = RCTNSStringFromStringNilIfEmpty(_accessibilityOptions.detentValue);
|
|
126
|
+
value = [value stringByReplacingOccurrencesOfString:@"{index}" withString:@(index + 1).stringValue];
|
|
127
|
+
self.accessibilityValue = [value stringByReplacingOccurrencesOfString:@"{count}" withString:@(count).stringValue];
|
|
118
128
|
}
|
|
119
129
|
}
|
|
120
130
|
|
|
@@ -83,6 +83,21 @@
|
|
|
83
83
|
|
|
84
84
|
_currentHeight = keyboardHeight;
|
|
85
85
|
|
|
86
|
+
if (keyboardHeight > 0) {
|
|
87
|
+
_viewController.keyboardSheetGrown = YES;
|
|
88
|
+
} else {
|
|
89
|
+
// The sheet stays grown until UIKit finishes the shrink-back animation —
|
|
90
|
+
// clear after it completes, unless the keyboard came back in the meantime.
|
|
91
|
+
__weak __typeof(self) weakSelf = self;
|
|
92
|
+
dispatch_after(
|
|
93
|
+
dispatch_time(DISPATCH_TIME_NOW, (int64_t)((duration + 0.1) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
94
|
+
__strong __typeof(weakSelf) strongSelf = weakSelf;
|
|
95
|
+
if (strongSelf && strongSelf->_currentHeight == 0) {
|
|
96
|
+
strongSelf->_viewController.keyboardSheetGrown = NO;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
86
101
|
for (id<TrueSheetKeyboardObserverDelegate> delegate in _delegates) {
|
|
87
102
|
if (keyboardHeight > 0) {
|
|
88
103
|
[delegate keyboardWillShow:keyboardHeight duration:duration curve:curve];
|