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

[ASDisplayNode] Implement accessibilityViewIsModal #1858

Merged
merged 5 commits into from
Jun 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion Source/Details/_ASDisplayViewAccessiblity.mm
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,21 @@ static void CollectAccessibilityElements(ASDisplayNode *node, NSMutableArray *el
return;
}

for (ASDisplayNode *subnode in node.subnodes) {
// see if one of the subnodes is modal. If it is, then we only need to collect accessibilityElements from that
// node. If more than one subnode is modal, UIKit uses the last view in subviews as the modal view (it appears to
// be based on the index in the subviews array, not the location on screen). Let's do the same.
ASDisplayNode *modalSubnode = nil;
for (ASDisplayNode *subnode in node.subnodes.reverseObjectEnumerator) {
if (subnode.accessibilityViewIsModal) {
modalSubnode = subnode;
break;
}
}

// If we have a modal subnode, just use that. Otherwise, use all subnodes
NSArray *subnodes = modalSubnode ? @[ modalSubnode ] : node.subnodes;

for (ASDisplayNode *subnode in 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;
Expand Down
23 changes: 23 additions & 0 deletions Source/Private/ASDisplayNode+UIViewBridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1137,6 +1137,22 @@ - (BOOL)_locked_insetsLayoutMarginsFromSafeArea

@implementation ASDisplayNode (UIViewBridgeAccessibility)

// Walks up the view tree to nil out all the cached accsesibilityElements. This is required when changing
// accessibility properties like accessibilityViewIsModal.
- (void)invalidateAccessibilityElements
{
// If we are not caching accessibilityElements we don't need to do anything here.
if (ASActivateExperimentalFeature(ASExperimentalDoNotCacheAccessibilityElements)) {
return;
}

// we want to check if we are on the main thread first, since _loaded checks the layer and can only be done on main
if (ASDisplayNodeThreadIsMain() && _loaded(self)) {
self.view.accessibilityElements = nil;
[self.supernode invalidateAccessibilityElements];
}
}

- (BOOL)isAccessibilityElement
{
_bridge_prologue_read;
Expand Down Expand Up @@ -1306,9 +1322,16 @@ - (BOOL)accessibilityViewIsModal
- (void)setAccessibilityViewIsModal:(BOOL)accessibilityViewIsModal
{
_bridge_prologue_write;
BOOL oldAccessibilityViewIsModal = _getFromViewOnly(accessibilityViewIsModal);
_setAccessibilityToViewAndProperty(_flags.accessibilityViewIsModal, accessibilityViewIsModal, accessibilityViewIsModal, accessibilityViewIsModal);

// if we made a change, we need to clear the view's accessibilityElements cache.
if (!ASActivateExperimentalFeature(ASExperimentalDoNotCacheAccessibilityElements) && self.isNodeLoaded && oldAccessibilityViewIsModal != accessibilityViewIsModal) {
[self invalidateAccessibilityElements];
}
}


- (BOOL)shouldGroupAccessibilityChildren
{
_bridge_prologue_read;
Expand Down
115 changes: 109 additions & 6 deletions Tests/ASDisplayViewAccessibilityTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ - (void)testThatAccessibilityElementsWorks {
// force load
__unused UIView *view = containerNode.view;

NSArray *elements = [containerNode accessibilityElements];
NSArray *elements = [containerNode.view accessibilityElements];
XCTAssertTrue(elements.count == 2);
XCTAssertEqual([elements.firstObject asyncdisplaykit_node], label);
XCTAssertEqual([elements.lastObject asyncdisplaykit_node], button);
Expand All @@ -273,7 +273,7 @@ - (void)testThatAccessibilityElementsOverrideWorks {
// force load
__unused UIView *view = containerNode.view;

NSArray *elements = [containerNode accessibilityElements];
NSArray *elements = [containerNode.view accessibilityElements];
XCTAssertTrue(elements.count == 1);
XCTAssertEqual(elements.firstObject, label);
}
Expand Down Expand Up @@ -301,7 +301,7 @@ - (void)testHiddenAccessibilityElements {
// force load
__unused UIView *view = containerNode.view;

NSArray *elements = [containerNode accessibilityElements];
NSArray *elements = [containerNode.view accessibilityElements];
XCTAssertTrue(elements.count == 1);
XCTAssertEqual(elements.firstObject, label.view);
}
Expand All @@ -328,7 +328,7 @@ - (void)testTransparentAccessibilityElements {
// force load
__unused UIView *view = containerNode.view;

NSArray *elements = [containerNode accessibilityElements];
NSArray *elements = [containerNode.view accessibilityElements];
XCTAssertTrue(elements.count == 1);
XCTAssertEqual(elements.firstObject, label.view);
}
Expand Down Expand Up @@ -376,7 +376,7 @@ - (void)testAccessibilityElementsNotInAppWindow {
[node addSubnode:offScreenNodeX];
[node addSubnode:offScreenNode];

NSArray *elements = [node accessibilityElements];
NSArray *elements = [node.view accessibilityElements];
XCTAssertTrue(elements.count == 3);
XCTAssertTrue([elements containsObject:label.view]);
XCTAssertTrue([elements containsObject:partiallyOnScreenNodeX.view]);
Expand Down Expand Up @@ -427,7 +427,7 @@ - (void)testAccessibilityElementsNotInAppWindowButInScrollView {
[node addSubnode:offScreenNodeX];
[node addSubnode:offScreenNode];

NSArray *elements = [node accessibilityElements];
NSArray *elements = [node.view accessibilityElements];
XCTAssertTrue(elements.count == 6);
XCTAssertTrue([elements containsObject:label.view]);
XCTAssertTrue([elements containsObject:partiallyOnScreenNodeX.view]);
Expand Down Expand Up @@ -488,5 +488,108 @@ - (void)testCustomAccessibilitySort {
XCTAssertEqual(elements[3], node4);
}

- (void)testSubnodeIsModal {

UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 568)];
ASDisplayNode *node = [[ASDisplayNode alloc] init];
node.automaticallyManagesSubnodes = YES;

ASViewController *vc = [[ASViewController alloc] initWithNode:node];
window.rootViewController = vc;
[window makeKeyAndVisible];
[window layoutIfNeeded];

ASTextNode *label1 = [[ASTextNode alloc] init];
label1.attributedText = [[NSAttributedString alloc] initWithString:@"label1"];
label1.frame = CGRectMake(10, 80, 300, 20);
[node addSubnode:label1];

ASTextNode *label2 = [[ASTextNode alloc] init];
label2.attributedText = [[NSAttributedString alloc] initWithString:@"label2"];
label2.frame = CGRectMake(10, CGRectGetMaxY(label1.frame) + 8, 300, 20);
[node addSubnode:label2];

ASDisplayNode *modalNode = [[ASDisplayNode alloc] init];
modalNode.frame = CGRectInset(CGRectUnion(label1.frame, label2.frame), -8, -8);

// This is kind of cheating. When voice over is activated, the modal node will end up reporting that it
// has 1 accessibilityElement. But getting that to happen in a unit test doesn't seem possible.
id modalMock = OCMPartialMock(modalNode);
OCMStub([modalMock accessibilityElementCount]).andReturn(1);
[node addSubnode:modalMock];

ASTextNode *label3 = [[ASTextNode alloc] init];
label3.attributedText = [[NSAttributedString alloc] initWithString:@"label6"];
label3.frame = CGRectMake(8, 4, 200, 20);

[modalNode addSubnode:label3];
modalNode.accessibilityViewIsModal = YES;
NSArray *elements = [node.view accessibilityElements];
XCTAssertTrue(elements.count == 1);
XCTAssertTrue([elements containsObject:modalNode.view]);
}

- (void)testMultipleSubnodesAreModal {

UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 568)];
ASDisplayNode *node = [[ASDisplayNode alloc] init];
node.automaticallyManagesSubnodes = YES;

ASViewController *vc = [[ASViewController alloc] initWithNode:node];
window.rootViewController = vc;
[window makeKeyAndVisible];
[window layoutIfNeeded];

ASTextNode *label1 = [[ASTextNode alloc] init];
label1.attributedText = [[NSAttributedString alloc] initWithString:@"label1"];
label1.frame = CGRectMake(10, 80, 300, 20);
[node addSubnode:label1];

ASTextNode *label2 = [[ASTextNode alloc] init];
label2.attributedText = [[NSAttributedString alloc] initWithString:@"label2"];
label2.frame = CGRectMake(10, CGRectGetMaxY(label1.frame) + 8, 300, 20);
[node addSubnode:label2];

ASDisplayNode *modalNode1 = [[ASDisplayNode alloc] init];
modalNode1.frame = CGRectInset(CGRectUnion(label1.frame, label2.frame), -8, -8);

// This is kind of cheating. When voice over is activated, the modal node will end up reporting that it
// has 1 accessibilityElement. But getting that to happen in a unit test doesn't seem possible.
id modalMock1 = OCMPartialMock(modalNode1);
OCMStub([modalMock1 accessibilityElementCount]).andReturn(1);

ASTextNode *label3 = [[ASTextNode alloc] init];
label3.attributedText = [[NSAttributedString alloc] initWithString:@"label6"];
label3.frame = CGRectMake(8, 4, 200, 20);
[modalNode1 addSubnode:label3];
modalNode1.accessibilityViewIsModal = YES;

ASDisplayNode *modalNode2 = [[ASDisplayNode alloc] init];
modalNode2.frame = CGRectOffset(modalNode1.frame, 0, modalNode1.frame.size.height + 10);
id modalMock2 = OCMPartialMock(modalNode2);
OCMStub([modalMock2 accessibilityElementCount]).andReturn(1);

ASTextNode *label4 = [[ASTextNode alloc] init];
label4.attributedText = [[NSAttributedString alloc] initWithString:@"label6"];
label4.frame = CGRectMake(8, 4, 200, 20);
[modalNode2 addSubnode:label4];
modalNode2.accessibilityViewIsModal = YES;

// add modalNode1 last, and assert that it is the one that appears in accessibilityElements
// (UIKit uses the last modal subview in subviews as the modal element).
[node addSubnode:modalMock2];
[node addSubnode:modalMock1];

NSArray *elements = [node.view accessibilityElements];
XCTAssertTrue(elements.count == 1);
XCTAssertTrue([elements containsObject:modalNode1.view]);

// let's change which node is modal and make sure the elements get updated.
modalNode1.accessibilityViewIsModal = NO;
elements = [node.view accessibilityElements];
XCTAssertTrue(elements.count == 1);
XCTAssertTrue([elements containsObject:modalNode2.view]);
}


@end