@lodev09/react-native-true-sheet 3.11.0-beta.8 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +1 -1
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +11 -0
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +48 -15
  4. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +114 -1
  5. package/ios/TrueSheetContainerView.h +3 -0
  6. package/ios/TrueSheetContainerView.mm +29 -0
  7. package/ios/TrueSheetFooterView.mm +25 -0
  8. package/ios/TrueSheetView.mm +7 -0
  9. package/ios/TrueSheetViewController.h +2 -0
  10. package/ios/TrueSheetViewController.mm +151 -0
  11. package/lib/module/TrueSheet.web.js +18 -2
  12. package/lib/module/TrueSheet.web.js.map +1 -1
  13. package/lib/module/navigation/TrueSheetRouter.js +1 -1
  14. package/lib/module/navigation/TrueSheetRouter.js.map +1 -1
  15. package/lib/module/navigation/createTrueSheetNavigator.js +1 -1
  16. package/lib/module/navigation/createTrueSheetNavigator.js.map +1 -1
  17. package/lib/module/navigation/useTrueSheetNavigation.js +1 -1
  18. package/lib/module/navigation/useTrueSheetNavigation.js.map +1 -1
  19. package/lib/module/web/vaul/browser.js +1 -1
  20. package/lib/module/web/vaul/browser.js.map +1 -1
  21. package/lib/module/web/vaul/style.css.d.js +2 -0
  22. package/lib/module/web/vaul/style.css.d.js.map +1 -0
  23. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  24. package/lib/typescript/src/navigation/TrueSheetRouter.d.ts +1 -1
  25. package/lib/typescript/src/navigation/TrueSheetRouter.d.ts.map +1 -1
  26. package/lib/typescript/src/navigation/TrueSheetView.d.ts +1 -1
  27. package/lib/typescript/src/navigation/TrueSheetView.d.ts.map +1 -1
  28. package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts +1 -1
  29. package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts.map +1 -1
  30. package/lib/typescript/src/navigation/screen/types.d.ts +1 -1
  31. package/lib/typescript/src/navigation/screen/types.d.ts.map +1 -1
  32. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +1 -1
  33. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
  34. package/lib/typescript/src/navigation/types.d.ts +1 -1
  35. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  36. package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts +1 -1
  37. package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts.map +1 -1
  38. package/package.json +21 -19
  39. package/src/TrueSheet.web.tsx +20 -4
  40. package/src/navigation/TrueSheetRouter.ts +1 -1
  41. package/src/navigation/TrueSheetView.tsx +1 -1
  42. package/src/navigation/createTrueSheetNavigator.tsx +1 -1
  43. package/src/navigation/screen/types.ts +1 -1
  44. package/src/navigation/screen/useSheetScreenState.ts +1 -1
  45. package/src/navigation/types.ts +1 -1
  46. package/src/navigation/useTrueSheetNavigation.ts +1 -1
  47. package/src/web/vaul/style.css.d.ts +1 -0
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 lodev09
3
+ Copyright (c) 2026 lodev09
4
4
  Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  of this software and associated documentation files (the "Software"), to deal
6
6
  in the Software without restriction, including without limitation the rights
@@ -3,6 +3,7 @@ package com.lodev09.truesheet
3
3
  import android.annotation.SuppressLint
4
4
  import android.view.MotionEvent
5
5
  import android.view.View
6
+ import android.view.ViewParent
6
7
  import com.facebook.react.uimanager.JSPointerDispatcher
7
8
  import com.facebook.react.uimanager.JSTouchDispatcher
8
9
  import com.facebook.react.uimanager.PointerEvents
@@ -10,6 +11,7 @@ import com.facebook.react.uimanager.RootView
10
11
  import com.facebook.react.uimanager.ThemedReactContext
11
12
  import com.facebook.react.uimanager.events.EventDispatcher
12
13
  import com.facebook.react.views.view.ReactViewGroup
14
+ import com.lodev09.truesheet.core.TrueSheetCoordinatorLayout
13
15
 
14
16
  /**
15
17
  * Delegate interface for footer view size changes and event dispatching
@@ -90,12 +92,21 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
90
92
  }
91
93
 
92
94
  override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
95
+ findCoordinatorLayout()?.childDidClaimNativeGesture()
93
96
  eventDispatcher?.let { dispatcher ->
94
97
  jsTouchDispatcher.onChildStartedNativeGesture(ev, dispatcher)
95
98
  jsPointerDispatcher?.onChildStartedNativeGesture(childView, ev, dispatcher)
96
99
  }
97
100
  }
98
101
 
102
+ private fun findCoordinatorLayout(): TrueSheetCoordinatorLayout? {
103
+ var current: ViewParent? = parent
104
+ while (current != null && current !is TrueSheetCoordinatorLayout) {
105
+ current = current.parent
106
+ }
107
+ return current as? TrueSheetCoordinatorLayout
108
+ }
109
+
99
110
  override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
100
111
  eventDispatcher?.let { jsTouchDispatcher.onChildEndedNativeGesture(ev, it) }
101
112
  jsPointerDispatcher?.onChildEndedNativeGesture()
@@ -153,6 +153,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
153
153
  var currentDetentIndex: Int = -1
154
154
  private set
155
155
 
156
+ // Target detent index during an in-flight resize animation. -1 when idle.
157
+ // Used by setupSheetDetents so a layout-driven reconfigure during the
158
+ // animation doesn't snap the sheet back to the stale currentDetentIndex.
159
+ private var pendingDetentIndex: Int = -1
160
+
156
161
  private var interactionState: InteractionState = InteractionState.Idle
157
162
  internal var isBeingDismissed = false
158
163
  private set
@@ -187,6 +192,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
187
192
  private val jsTouchDispatcher = JSTouchDispatcher(this)
188
193
  private val jsPointerDispatcher = JSPointerDispatcher(this)
189
194
 
195
+ // True when the active touch stream began inside the footer. Latched on
196
+ // ACTION_DOWN so subsequent MOVE events keep routing to the footer even if
197
+ // the finger drifts outside the footer's rect mid-gesture.
198
+ private var footerOwnsTouchStream = false
199
+
190
200
  private val eventDispatcher
191
201
  get() = delegate?.eventDispatcher
192
202
 
@@ -376,6 +386,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
376
386
  isPresentAnimating = false
377
387
  lastEmittedPositionPx = -1
378
388
  detentIndexBeforeKeyboard = -1
389
+ pendingDetentIndex = -1
379
390
  isKeyboardDismissProgrammatic = false
380
391
  focusedViewBeforeBlur = null
381
392
  shouldAnimatePresent = true
@@ -568,6 +579,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
568
579
  private fun handleStateSettled(sheetView: View, newState: Int) {
569
580
  if (interactionState is InteractionState.Reconfiguring) return
570
581
 
582
+ // Sheet has reached a stable state — any in-flight resize target is now stale,
583
+ // whether settled at the target, interrupted by drag, or superseded.
584
+ pendingDetentIndex = -1
585
+
571
586
  val index = detentCalculator.getDetentIndexForState(newState) ?: return
572
587
  val position = getPositionDpForView(sheetView)
573
588
  val detentInfo = DetentInfo(index, position)
@@ -711,6 +726,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
711
726
  detentIndexBeforeKeyboard = detentIndex
712
727
  }
713
728
 
729
+ pendingDetentIndex = detentIndex
714
730
  setupDimmedBackground()
715
731
  setStateForDetentIndex(detentIndex)
716
732
  resizePromise?.invoke()
@@ -859,7 +875,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
859
875
  updateStateDimensions(expandedOffset)
860
876
 
861
877
  if (isPresented && applyState) {
862
- setStateForDetentIndex(currentDetentIndex)
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
- detentIndexBeforeKeyboard = currentDetentIndex
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
- val footerLocation = ScreenUtils.getScreenLocation(footer)
1279
- val touchX = event.rawX.toInt()
1280
- val touchY = event.rawY.toInt()
1281
-
1282
- if (touchX >= footerLocation[0] &&
1283
- touchX <= footerLocation[0] + footer.width &&
1284
- touchY >= footerLocation[1] &&
1285
- touchY <= footerLocation[1] + footer.height
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
- if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
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 {
@@ -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;