[ASDisplayNode] Implement accessibilityViewIsModal (#1858)

* [ASDisplayNode] Implement accessibilityViewIsModal

A PR to add support for `accessibilityViewIsModal` in `CollectAccessibilityElements`.

If in a list of subnodes more than 1 subnode has `accessibilityViewIsModal` marked as `YES`, then the node with the highest index in `subnodes` will be the one that is considered modal. This behavior matches UIKit.

If the value of `accessibilityViewIsModal` changes, we need to clear all the cached `accessibilityElements` from that view up. I added this in ASDisplayNode’s `setAccessibilityViewIsModal` method. Note that if we ship `ASExperimentalDoNotCacheAccessibilityElements` then we can remove the invalidation step.

Finally, I changed all the tests to ask the view for accessibilityElements, not the node. This is a better representation of what will really happen when UIKit asks a node’s view for its accessibility elements. It also allowed me to test that clearing the accessibilityElements was working.

* add some experiment checks

* fix tests and address jon’s comment

* Fix tests

* remove debug code
This commit is contained in:
ricky
2020-06-01 12:58:44 -07:00
committed by GitHub
parent 3ccbc5f436
commit 08bd6f6e3b
3 changed files with 147 additions and 7 deletions
+15 -1
View File
@@ -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;
@@ -1137,6 +1137,22 @@ nodeProperty = nodeValueExpr; _setToViewOnly(viewAndPendingViewStateProperty, vi
@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;
@@ -1306,9 +1322,16 @@ nodeProperty = nodeValueExpr; _setToViewOnly(viewAndPendingViewStateProperty, vi
- (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;
+109 -6
View File
@@ -247,7 +247,7 @@ extern void SortAccessibilityElements(NSMutableArray *elements);
// 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);
@@ -273,7 +273,7 @@ extern void SortAccessibilityElements(NSMutableArray *elements);
// force load
__unused UIView *view = containerNode.view;
NSArray *elements = [containerNode accessibilityElements];
NSArray *elements = [containerNode.view accessibilityElements];
XCTAssertTrue(elements.count == 1);
XCTAssertEqual(elements.firstObject, label);
}
@@ -301,7 +301,7 @@ extern void SortAccessibilityElements(NSMutableArray *elements);
// 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);
}
@@ -328,7 +328,7 @@ extern void SortAccessibilityElements(NSMutableArray *elements);
// 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);
}
@@ -376,7 +376,7 @@ extern void SortAccessibilityElements(NSMutableArray *elements);
[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]);
@@ -427,7 +427,7 @@ extern void SortAccessibilityElements(NSMutableArray *elements);
[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]);
@@ -488,5 +488,108 @@ extern void SortAccessibilityElements(NSMutableArray *elements);
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