diff --git a/Source/Details/_ASDisplayViewAccessiblity.mm b/Source/Details/_ASDisplayViewAccessiblity.mm index 8115eea53..367f53687 100644 --- a/Source/Details/_ASDisplayViewAccessiblity.mm +++ b/Source/Details/_ASDisplayViewAccessiblity.mm @@ -210,6 +210,17 @@ static void CollectAccessibilityElementsForContainer(ASDisplayNode *container, U [elements addObject:accessiblityElement]; } +/// Check if a view is a subviews of an UIScrollView. This is used to determine whether to enforce that +/// accessibility elements must be on screen +static BOOL recusivelyCheckSuperviewsForScrollView(UIView *view) { + if (!view) { + return NO; + } else if ([view isKindOfClass:[UIScrollView class]]) { + return YES; + } + return recusivelyCheckSuperviewsForScrollView(view.superview); +} + /// Collect all accessibliity elements for a given view and view node static void CollectAccessibilityElements(ASDisplayNode *node, NSMutableArray *elements) { @@ -226,7 +237,7 @@ static void CollectAccessibilityElements(ASDisplayNode *node, NSMutableArray *el })); UIView *view = node.view; - + // If we don't have a window, let's just bail out if (!view.window) { return; @@ -246,11 +257,13 @@ static void CollectAccessibilityElements(ASDisplayNode *node, NSMutableArray *el for (ASDisplayNode *subnode in node.subnodes) { // If a node is hidden or has an alpha of 0.0 we should not include it if (subnode.hidden || subnode.alpha == 0.0) { - continue; + continue; } - // If a subnode is outside of the view's window, exclude it + + // If a subnode is outside of the view's window, exclude it UNLESS it is a subview of an UIScrollView. + // In this case UIKit will return the element even if it is outside of the window or the scrollView's visible rect (contentOffset + contentSize) CGRect nodeInWindowCoords = [node convertRect:subnode.frame toNode:nil]; - if (!CGRectIntersectsRect(view.window.frame, nodeInWindowCoords)) { + if (!CGRectIntersectsRect(view.window.frame, nodeInWindowCoords) && !recusivelyCheckSuperviewsForScrollView(view)) { continue; } diff --git a/Tests/ASDisplayViewAccessibilityTests.mm b/Tests/ASDisplayViewAccessibilityTests.mm index d52151cd6..4dc68e37d 100644 --- a/Tests/ASDisplayViewAccessibilityTests.mm +++ b/Tests/ASDisplayViewAccessibilityTests.mm @@ -20,6 +20,7 @@ #import #import #import +#import #import #import #import "ASDisplayNodeTestsHelper.h" @@ -382,6 +383,60 @@ - (void)testAccessibilityElementsNotInAppWindow { XCTAssertTrue([elements containsObject:partiallyOnScreenNodeY.view]); } +- (void)testAccessibilityElementsNotInAppWindowButInScrollView { + + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 568)]; + ASScrollNode *node = [[ASScrollNode alloc] init]; + node.automaticallyManagesSubnodes = YES; + + ASViewController *vc = [[ASViewController alloc] initWithNode:node]; + window.rootViewController = vc; + [window makeKeyAndVisible]; + [window layoutIfNeeded]; + + CGSize windowSize = window.frame.size; + node.view.contentSize = CGSizeMake(window.frame.size.width, window.frame.size.height * 2.0); + ASTextNode *label = [[ASTextNode alloc] init]; + label.attributedText = [[NSAttributedString alloc] initWithString:@"on screen"]; + label.frame = CGRectMake(0, 0, 100, 20); + + ASTextNode *partiallyOnScreenNodeY = [[ASTextNode alloc] init]; + partiallyOnScreenNodeY.attributedText = [[NSAttributedString alloc] initWithString:@"partially on screen y"]; + partiallyOnScreenNodeY.frame = CGRectMake(0, windowSize.height - 10, 100, 20); + + ASTextNode *partiallyOnScreenNodeX = [[ASTextNode alloc] init]; + partiallyOnScreenNodeX.attributedText = [[NSAttributedString alloc] initWithString:@"partially on screen x"]; + partiallyOnScreenNodeX.frame = CGRectMake(windowSize.width - 10, 100, 100, 20); + + ASTextNode *offScreenNodeY = [[ASTextNode alloc] init]; + offScreenNodeY.attributedText = [[NSAttributedString alloc] initWithString:@"off screen y"]; + offScreenNodeY.frame = CGRectMake(0, windowSize.height + 10, 100, 20); + + ASTextNode *offScreenNodeX = [[ASTextNode alloc] init]; + offScreenNodeX.attributedText = [[NSAttributedString alloc] initWithString:@"off screen x"]; + offScreenNodeX.frame = CGRectMake(windowSize.width + 1, 200, 100, 20); + + ASTextNode *offScreenNode = [[ASTextNode alloc] init]; + offScreenNode.attributedText = [[NSAttributedString alloc] initWithString:@"off screen"]; + offScreenNode.frame = CGRectMake(windowSize.width + 1, windowSize.height + 1, 100, 20); + + [node addSubnode:label]; + [node addSubnode:partiallyOnScreenNodeY]; + [node addSubnode:partiallyOnScreenNodeX]; + [node addSubnode:offScreenNodeY]; + [node addSubnode:offScreenNodeX]; + [node addSubnode:offScreenNode]; + + NSArray *elements = [node accessibilityElements]; + XCTAssertTrue(elements.count == 6); + XCTAssertTrue([elements containsObject:label.view]); + XCTAssertTrue([elements containsObject:partiallyOnScreenNodeX.view]); + XCTAssertTrue([elements containsObject:partiallyOnScreenNodeY.view]); + XCTAssertTrue([elements containsObject:offScreenNodeY.view]); + XCTAssertTrue([elements containsObject:offScreenNodeX.view]); + XCTAssertTrue([elements containsObject:offScreenNode.view]); +} + - (void)testAccessibilitySort { ASDisplayNode *node1 = [[ASDisplayNode alloc] init]; node1.accessibilityFrame = CGRectMake(0, 0, 50, 200);