Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Accessibility] Do not exclude elements outside the window’s rect that are subviews of UIScrollView #1821

Merged
merged 9 commits into from
May 22, 2020
20 changes: 16 additions & 4 deletions Source/Details/_ASDisplayViewAccessiblity.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -226,7 +237,6 @@ 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;
Expand All @@ -246,11 +256,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;
}

Expand Down
55 changes: 55 additions & 0 deletions Tests/ASDisplayViewAccessibilityTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#import <AsyncDisplayKit/ASTextNode.h>
#import <AsyncDisplayKit/ASConfiguration.h>
#import <AsyncDisplayKit/ASConfigurationInternal.h>
#import <AsyncDisplayKit/ASScrollNode.h>
#import <AsyncDisplayKit/ASViewController.h>
#import <OCMock/OCMock.h>
#import "ASDisplayNodeTestsHelper.h"
Expand Down Expand Up @@ -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);
Expand Down