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

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
  */
@@ -88,11 +88,22 @@ using namespace facebook::react;
88
88
  [elements addObject:_contentView];
89
89
  }
90
90
  if (_footerView) {
91
- [elements addObject:_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
+ }
92
99
  }
93
100
  return elements;
94
101
  }
95
102
 
103
+ - (BOOL)hasAccessibilityFooterElements {
104
+ return _footerView && _footerView.accessibilityElements.count > 0;
105
+ }
106
+
96
107
  #pragma mark - Layout
97
108
 
98
109
  - (void)layoutSubviews {
@@ -251,6 +262,7 @@ using namespace facebook::react;
251
262
  #pragma mark - TrueSheetFooterViewDelegate
252
263
 
253
264
  - (void)footerViewDidChangeSize:(CGSize)newSize {
265
+ [self.delegate containerViewFooterDidChangeSize:newSize];
254
266
  if (@available(iOS 26.0, *)) {
255
267
  [self setupEdgeInteractions];
256
268
  }
@@ -53,13 +53,19 @@ using namespace facebook::react;
53
53
  - (NSArray *)accessibilityElements {
54
54
  NSMutableArray *elements = [NSMutableArray array];
55
55
  [self collectAccessibilityElementsFromView:self into:elements];
56
- return elements;
56
+ if (elements.count > 0) {
57
+ return elements;
58
+ }
59
+
60
+ return [super accessibilityElements];
57
61
  }
58
62
 
59
63
  - (void)collectAccessibilityElementsFromView:(UIView *)view into:(NSMutableArray *)elements {
60
64
  for (UIView *subview in view.subviews) {
61
65
  if (subview.isAccessibilityElement || subview.accessibilityLabel || subview.accessibilityIdentifier) {
62
66
  [elements addObject:subview];
67
+ } else if (subview.accessibilityElements.count > 0) {
68
+ [elements addObjectsFromArray:subview.accessibilityElements];
63
69
  } else {
64
70
  [self collectAccessibilityElementsFromView:subview into:elements];
65
71
  }
@@ -573,6 +573,10 @@ using namespace facebook::react;
573
573
  [self setupSheetDetentsForSizeChange];
574
574
  }
575
575
 
576
+ - (void)containerViewFooterDidChangeSize:(CGSize)newSize {
577
+ [_controller setupAccessibilityContainer];
578
+ }
579
+
576
580
  // When the ScrollView changes (e.g. conditional remount), re-pin the new ScrollView.
577
581
  - (void)containerViewScrollViewDidChange {
578
582
  [self setupScrollable];
@@ -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,14 @@ 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;
37
45
  - (void)setAccessibilityContentElement:(UIView *)contentView;
38
46
 
39
47
  @end
@@ -57,6 +65,8 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
57
65
 
58
66
  UIView *_anchorView;
59
67
 
68
+ __weak UIWindow *_accessibilityWindow;
69
+
60
70
  TrueSheetBlurView *_blurView;
61
71
  TrueSheetGrabberView *_grabberView;
62
72
  TrueSheetDetentCalculator *_detentCalculator;
@@ -97,6 +107,7 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
97
107
  }
98
108
 
99
109
  - (void)dealloc {
110
+ [self restoreWindowAccessibilityElements];
100
111
  [_transitioningTimer invalidate];
101
112
  _transitioningTimer = nil;
102
113
  [[NSNotificationCenter defaultCenter] removeObserver:self];
@@ -227,6 +238,7 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
227
238
 
228
239
  - (void)viewDidAppear:(BOOL)animated {
229
240
  [super viewDidAppear:animated];
241
+ [self setSheetAccessibilityElementsHidden:NO];
230
242
 
231
243
  if (!_isPresented) {
232
244
  [_parentSheetController.delegate viewControllerDidBlur];
@@ -244,9 +256,10 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
244
256
  });
245
257
 
246
258
  [self setupGestureRecognizer];
247
- [self setupAccessibilityContainer];
248
259
  _isPresented = YES;
249
260
  }
261
+
262
+ [self setupAccessibilityContainer];
250
263
  }
251
264
 
252
265
  - (void)setupAccessibilityContainer {
@@ -258,17 +271,124 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
258
271
  [self setAccessibilityContentElement:contentView];
259
272
  }
260
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
+
261
310
  - (void)setAccessibilityContentElement:(UIView *)contentView {
311
+ BOOL hasAccessibilityFooterElements = [contentView isKindOfClass:[TrueSheetContainerView class]] &&
312
+ [(TrueSheetContainerView *)contentView hasAccessibilityFooterElements];
313
+ // Footer controls exposed as separate accessibility elements can be skipped
314
+ // by XCTest when the presented sheet is a hard modal accessibility boundary.
315
+ BOOL isAccessibilityModal = _dimmed && !hasAccessibilityFooterElements;
316
+
317
+ // At a hard modal boundary XCTest skips nested elements, so flatten the
318
+ // container's children into the array. Otherwise expose the container itself:
319
+ // its accessibilityElements getter recomputes live, so a footer/content subtree
320
+ // that remounts (e.g. swapping a footer button on a state change) stays
321
+ // discoverable instead of leaving this snapshot pointing at destroyed views.
322
+ NSArray *accessibilityElements;
323
+ if (isAccessibilityModal) {
324
+ NSArray *contentElements = contentView.accessibilityElements;
325
+ accessibilityElements = contentElements.count > 0 ? contentElements : @[ contentView ];
326
+ } else {
327
+ accessibilityElements = @[ contentView ];
328
+ }
329
+
262
330
  self.view.isAccessibilityElement = NO;
263
331
  self.view.accessibilityViewIsModal = YES;
264
- self.view.accessibilityElements = @[ contentView ];
332
+ self.view.accessibilityElements = accessibilityElements;
265
333
 
266
334
  UIView *presentedView = self.presentationController.presentedView;
267
335
  if (presentedView) {
268
336
  presentedView.isAccessibilityElement = NO;
269
- presentedView.accessibilityViewIsModal = YES;
270
- presentedView.accessibilityElements = @[ contentView ];
337
+ presentedView.accessibilityViewIsModal = isAccessibilityModal;
338
+ presentedView.accessibilityElements = accessibilityElements;
339
+
340
+ UIWindow *window = self.view.window;
341
+ UIViewController *presentingViewController = [self accessibilityPresentingViewController];
342
+ if (window && presentingViewController.view) {
343
+ if (!_accessibilityWindow) {
344
+ _accessibilityWindow = window;
345
+ id previousElements = objc_getAssociatedObject(window, &TrueSheetAccessibilityWindowPreviousElementsKey);
346
+ if (!previousElements) {
347
+ previousElements = window.accessibilityElements ?: [NSNull null];
348
+ objc_setAssociatedObject(window, &TrueSheetAccessibilityWindowPreviousElementsKey, previousElements,
349
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
350
+ }
351
+ }
352
+ objc_setAssociatedObject(window, &TrueSheetAccessibilityWindowOwnerKey, [NSValue valueWithNonretainedObject:self],
353
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
354
+ window.accessibilityElements =
355
+ isAccessibilityModal ? @[ presentedView ] : @[ presentingViewController.view, presentedView ];
356
+ return;
357
+ }
271
358
  }
359
+
360
+ [self restoreWindowAccessibilityElements];
361
+ }
362
+
363
+ #pragma mark - Presentation
364
+
365
+ - (void)presentViewController:(UIViewController *)viewControllerToPresent
366
+ animated:(BOOL)flag
367
+ completion:(void (^)(void))completion {
368
+ // A non-sheet controller (e.g. an action sheet, alert, or image picker) is about
369
+ // to present over us. Clear our window accessibility override so UIKit's default
370
+ // traversal surfaces it for XCTest and assistive technologies; we reclaim the
371
+ // override when it dismisses. Nested sheets claim their own override.
372
+ if (![viewControllerToPresent isKindOfClass:[TrueSheetViewController class]]) {
373
+ self.view.window.accessibilityElements = nil;
374
+ }
375
+
376
+ [super presentViewController:viewControllerToPresent animated:flag completion:completion];
377
+ }
378
+
379
+ - (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
380
+ [super dismissViewControllerAnimated:flag
381
+ completion:^{
382
+ if (completion) {
383
+ completion();
384
+ }
385
+
386
+ // Reclaim the window accessibility override once nothing
387
+ // else is presented over us.
388
+ if (self.isPresented && self.presentedViewController == nil) {
389
+ [self setupAccessibilityContainer];
390
+ }
391
+ }];
272
392
  }
273
393
 
274
394
  - (void)emitWillDismissEvents {
@@ -283,6 +403,7 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
283
403
 
284
404
  - (void)emitDidDismissEvents {
285
405
  if (self.isBeingDismissed) {
406
+ [self restoreWindowAccessibilityElements];
286
407
  _isPresented = NO;
287
408
  _isWillDismissEmitted = NO;
288
409
 
@@ -290,6 +411,7 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
290
411
  _anchorView = nil;
291
412
 
292
413
  [_parentSheetController.delegate viewControllerDidFocus];
414
+ [_parentSheetController setSheetAccessibilityElementsHidden:NO];
293
415
  [_parentSheetController setupAccessibilityContainer];
294
416
  _parentSheetController = nil;
295
417
 
@@ -300,6 +422,8 @@ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPosi
300
422
 
301
423
  - (void)viewWillDisappear:(BOOL)animated {
302
424
  [super viewWillDisappear:animated];
425
+ [self restoreWindowAccessibilityElements];
426
+ [self setSheetAccessibilityElementsHidden:YES];
303
427
 
304
428
  // Dispatch to allow pan gesture to set _isDragging before checking
305
429
  // 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.13",
3
+ "version": "3.11.0-beta.15",
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",