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

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.
@@ -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;
@@ -7,6 +7,7 @@
7
7
  //
8
8
 
9
9
  #import "TrueSheetViewController.h"
10
+ #import "TrueSheetContainerView.h"
10
11
  #import "TrueSheetContentView.h"
11
12
  #import "core/TrueSheetBlurView.h"
12
13
  #import "core/TrueSheetDetentCalculator.h"
@@ -18,6 +19,7 @@
18
19
 
19
20
  #import <React/RCTLog.h>
20
21
  #import <React/RCTScrollViewComponentView.h>
22
+ #import <objc/runtime.h>
21
23
  #import <react/renderer/components/TrueSheetSpec/Props.h>
22
24
 
23
25
  using namespace facebook::react;
@@ -32,8 +34,16 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
32
34
  return fabs(a.position - b.position) <= 0.01 && fabs(a.detent - b.detent) <= 0.01 && fabs(a.index - b.index) <= 0.01;
33
35
  }
34
36
 
37
+ static char TrueSheetAccessibilityWindowOwnerKey;
38
+ static char TrueSheetAccessibilityWindowPreviousElementsKey;
39
+
35
40
  @interface TrueSheetViewController ()
36
41
 
42
+ - (UIViewController *)accessibilityPresentingViewController;
43
+ - (void)restoreWindowAccessibilityElements;
44
+ - (void)setSheetAccessibilityElementsHidden:(BOOL)hidden;
45
+ - (void)setAccessibilityContentElement:(UIView *)contentView;
46
+
37
47
  @end
38
48
 
39
49
  @implementation TrueSheetViewController {
@@ -55,6 +65,8 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
55
65
 
56
66
  UIView *_anchorView;
57
67
 
68
+ __weak UIWindow *_accessibilityWindow;
69
+
58
70
  TrueSheetBlurView *_blurView;
59
71
  TrueSheetGrabberView *_grabberView;
60
72
  TrueSheetDetentCalculator *_detentCalculator;
@@ -95,6 +107,7 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
95
107
  }
96
108
 
97
109
  - (void)dealloc {
110
+ [self restoreWindowAccessibilityElements];
98
111
  [_transitioningTimer invalidate];
99
112
  _transitioningTimer = nil;
100
113
  [[NSNotificationCenter defaultCenter] removeObserver:self];
@@ -207,6 +220,7 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
207
220
  if ([presenter isKindOfClass:[TrueSheetViewController class]]) {
208
221
  _parentSheetController = (TrueSheetViewController *)presenter;
209
222
  [_parentSheetController.delegate viewControllerWillBlur];
223
+ [_parentSheetController setAccessibilityContentElement:self.accessibilityContentView ?: self.view];
210
224
  }
211
225
 
212
226
  dispatch_async(dispatch_get_main_queue(), ^{
@@ -224,6 +238,7 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
224
238
 
225
239
  - (void)viewDidAppear:(BOOL)animated {
226
240
  [super viewDidAppear:animated];
241
+ [self setSheetAccessibilityElementsHidden:NO];
227
242
 
228
243
  if (!_isPresented) {
229
244
  [_parentSheetController.delegate viewControllerDidBlur];
@@ -243,6 +258,95 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
243
258
  [self setupGestureRecognizer];
244
259
  _isPresented = YES;
245
260
  }
261
+
262
+ [self setupAccessibilityContainer];
263
+ }
264
+
265
+ - (void)setupAccessibilityContainer {
266
+ UIView *contentView = self.accessibilityContentView;
267
+ if (!contentView) {
268
+ return;
269
+ }
270
+
271
+ [self setAccessibilityContentElement:contentView];
272
+ }
273
+
274
+ - (UIViewController *)accessibilityPresentingViewController {
275
+ UIViewController *presentingViewController = self.presentingViewController;
276
+ while ([presentingViewController isKindOfClass:[TrueSheetViewController class]] &&
277
+ presentingViewController.presentingViewController) {
278
+ presentingViewController = presentingViewController.presentingViewController;
279
+ }
280
+ return presentingViewController;
281
+ }
282
+
283
+ - (void)restoreWindowAccessibilityElements {
284
+ UIWindow *window = _accessibilityWindow;
285
+ if (window) {
286
+ // The active sheet may temporarily own window accessibility traversal. Only
287
+ // restore the previous value when this controller still owns that override.
288
+ NSValue *ownerValue = objc_getAssociatedObject(window, &TrueSheetAccessibilityWindowOwnerKey);
289
+ if (ownerValue && ownerValue.nonretainedObjectValue == self) {
290
+ id previousElements = objc_getAssociatedObject(window, &TrueSheetAccessibilityWindowPreviousElementsKey);
291
+ window.accessibilityElements = previousElements == [NSNull null] ? nil : previousElements;
292
+ objc_setAssociatedObject(window, &TrueSheetAccessibilityWindowOwnerKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
293
+ objc_setAssociatedObject(
294
+ window, &TrueSheetAccessibilityWindowPreviousElementsKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
295
+ }
296
+
297
+ _accessibilityWindow = nil;
298
+ }
299
+ }
300
+
301
+ - (void)setSheetAccessibilityElementsHidden:(BOOL)hidden {
302
+ self.view.accessibilityElementsHidden = hidden;
303
+
304
+ UIView *presentedView = self.presentationController.presentedView;
305
+ if (presentedView) {
306
+ presentedView.accessibilityElementsHidden = hidden;
307
+ }
308
+ }
309
+
310
+ - (void)setAccessibilityContentElement:(UIView *)contentView {
311
+ NSArray *contentElements = contentView.accessibilityElements;
312
+ NSArray *accessibilityElements = contentElements.count > 0 ? contentElements : @[ contentView ];
313
+ BOOL hasAccessibilityFooterElements = [contentView isKindOfClass:[TrueSheetContainerView class]] &&
314
+ [(TrueSheetContainerView *)contentView hasAccessibilityFooterElements];
315
+ // Footer controls exposed as separate accessibility elements can be skipped
316
+ // by XCTest when the presented sheet is a hard modal accessibility boundary.
317
+ BOOL isAccessibilityModal = _dimmed && !hasAccessibilityFooterElements;
318
+
319
+ self.view.isAccessibilityElement = NO;
320
+ self.view.accessibilityViewIsModal = YES;
321
+ self.view.accessibilityElements = accessibilityElements;
322
+
323
+ UIView *presentedView = self.presentationController.presentedView;
324
+ if (presentedView) {
325
+ presentedView.isAccessibilityElement = NO;
326
+ presentedView.accessibilityViewIsModal = isAccessibilityModal;
327
+ presentedView.accessibilityElements = accessibilityElements;
328
+
329
+ UIWindow *window = self.view.window;
330
+ UIViewController *presentingViewController = [self accessibilityPresentingViewController];
331
+ if (window && presentingViewController.view) {
332
+ if (!_accessibilityWindow) {
333
+ _accessibilityWindow = window;
334
+ id previousElements = objc_getAssociatedObject(window, &TrueSheetAccessibilityWindowPreviousElementsKey);
335
+ if (!previousElements) {
336
+ previousElements = window.accessibilityElements ?: [NSNull null];
337
+ objc_setAssociatedObject(window, &TrueSheetAccessibilityWindowPreviousElementsKey, previousElements,
338
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
339
+ }
340
+ }
341
+ objc_setAssociatedObject(window, &TrueSheetAccessibilityWindowOwnerKey, [NSValue valueWithNonretainedObject:self],
342
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
343
+ window.accessibilityElements =
344
+ isAccessibilityModal ? @[ presentedView ] : @[ presentingViewController.view, presentedView ];
345
+ return;
346
+ }
347
+ }
348
+
349
+ [self restoreWindowAccessibilityElements];
246
350
  }
247
351
 
248
352
  - (void)emitWillDismissEvents {
@@ -257,6 +361,7 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
257
361
 
258
362
  - (void)emitDidDismissEvents {
259
363
  if (self.isBeingDismissed) {
364
+ [self restoreWindowAccessibilityElements];
260
365
  _isPresented = NO;
261
366
  _isWillDismissEmitted = NO;
262
367
 
@@ -264,6 +369,8 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
264
369
  _anchorView = nil;
265
370
 
266
371
  [_parentSheetController.delegate viewControllerDidFocus];
372
+ [_parentSheetController setSheetAccessibilityElementsHidden:NO];
373
+ [_parentSheetController setupAccessibilityContainer];
267
374
  _parentSheetController = nil;
268
375
 
269
376
  [self.delegate viewControllerDidBlur];
@@ -273,6 +380,8 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
273
380
 
274
381
  - (void)viewWillDisappear:(BOOL)animated {
275
382
  [super viewWillDisappear:animated];
383
+ [self restoreWindowAccessibilityElements];
384
+ [self setSheetAccessibilityElementsHidden:YES];
276
385
 
277
386
  // Dispatch to allow pan gesture to set _isDragging before checking
278
387
  // handleTransitionTracker will emit when sheet is transitioning to dismiss
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodev09/react-native-true-sheet",
3
- "version": "3.11.0-beta.12",
3
+ "version": "3.11.0-beta.14",
4
4
  "description": "The true native bottom sheet experience for your React Native Apps.",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/module/index.js",