mirror of
https://github.com/TextureGroup/Texture.git
synced 2026-04-07 19:17:39 +00:00
[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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user