From e16fdd67e127bd2418e6ac3042b3e621af43962d Mon Sep 17 00:00:00 2001 From: ricky Date: Wed, 3 Jun 2020 16:16:43 -0700 Subject: [PATCH] [ASDisplayNode] Implement accessibilityElementsHidden (#1859) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Most of this code comes from an old PR that @fruitcoder put up https://github.com/TextureGroup/Texture/pull/795 2 years ago. When creating our array of accessibilityElements, we need to respect the value of `accessibilityElementsHidden`. If the value of this property changes, we need to invalidate the cached accessibility elements (unless we are in the experiment that doesn’t cache `accessibilityElements`). I created a simple test app and made sure this matched UIKit’s implementation. I also added a test case that changes the value of `accessibilityElementsHidden` and makes sure the proper accessibilityElements are returned. --- Source/Details/_ASDisplayViewAccessiblity.mm | 12 ++++++- Source/Private/ASDisplayNode+UIViewBridge.mm | 6 ++++ Tests/ASDisplayViewAccessibilityTests.mm | 37 ++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/Source/Details/_ASDisplayViewAccessiblity.mm b/Source/Details/_ASDisplayViewAccessiblity.mm index 7324c5c29..7fb79eaa2 100644 --- a/Source/Details/_ASDisplayViewAccessiblity.mm +++ b/Source/Details/_ASDisplayViewAccessiblity.mm @@ -89,6 +89,7 @@ + (ASAccessibilityElement *)accessibilityElementWithContainer:(UIView *)containe accessibilityElement.accessibilityHint = node.accessibilityHint; accessibilityElement.accessibilityValue = node.accessibilityValue; accessibilityElement.accessibilityTraits = node.accessibilityTraits; + accessibilityElement.accessibilityElementsHidden = node.accessibilityElementsHidden; if (AS_AVAILABLE_IOS_TVOS(11, 11)) { accessibilityElement.accessibilityAttributedLabel = node.accessibilityAttributedLabel; accessibilityElement.accessibilityAttributedHint = node.accessibilityAttributedHint; @@ -221,6 +222,11 @@ static BOOL recusivelyCheckSuperviewsForScrollView(UIView *view) { return recusivelyCheckSuperviewsForScrollView(view.superview); } +/// returns YES if this node should be considered "hidden" from the screen reader. +static BOOL nodeIsHiddenFromAcessibility(ASDisplayNode *node) { + return node.isHidden || node.alpha == 0.0 || node.accessibilityElementsHidden; +} + /// Collect all accessibliity elements for a given view and view node static void CollectAccessibilityElements(ASDisplayNode *node, NSMutableArray *elements) { @@ -254,6 +260,10 @@ static void CollectAccessibilityElements(ASDisplayNode *node, NSMutableArray *el return; } + if (nodeIsHiddenFromAcessibility(node)) { + return; + } + // 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. @@ -270,7 +280,7 @@ static void CollectAccessibilityElements(ASDisplayNode *node, NSMutableArray *el 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) { + if (nodeIsHiddenFromAcessibility(subnode)) { continue; } diff --git a/Source/Private/ASDisplayNode+UIViewBridge.mm b/Source/Private/ASDisplayNode+UIViewBridge.mm index 94d8ace0a..50641d80d 100644 --- a/Source/Private/ASDisplayNode+UIViewBridge.mm +++ b/Source/Private/ASDisplayNode+UIViewBridge.mm @@ -1310,7 +1310,13 @@ - (BOOL)accessibilityElementsHidden - (void)setAccessibilityElementsHidden:(BOOL)accessibilityElementsHidden { _bridge_prologue_write; + BOOL oldHiddenValue = _getFromViewOnly(accessibilityElementsHidden); _setAccessibilityToViewAndProperty(_flags.accessibilityElementsHidden, accessibilityElementsHidden, accessibilityElementsHidden, accessibilityElementsHidden); + + // if we made a change, we need to clear the view's accessibilityElements cache. + if (!ASActivateExperimentalFeature(ASExperimentalDoNotCacheAccessibilityElements) && self.isNodeLoaded && oldHiddenValue != accessibilityElementsHidden) { + [self invalidateAccessibilityElements]; + } } - (BOOL)accessibilityViewIsModal diff --git a/Tests/ASDisplayViewAccessibilityTests.mm b/Tests/ASDisplayViewAccessibilityTests.mm index 02d99d790..f1a7696cc 100644 --- a/Tests/ASDisplayViewAccessibilityTests.mm +++ b/Tests/ASDisplayViewAccessibilityTests.mm @@ -591,5 +591,42 @@ - (void)testMultipleSubnodesAreModal { XCTAssertTrue([elements containsObject:modalNode2.view]); } +- (void)testAccessibilityElementsHidden { + + 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:@"on screen"]; + label1.frame = CGRectMake(0, 0, 100, 20); + + ASTextNode *label2 = [[ASTextNode alloc] init]; + label2.attributedText = [[NSAttributedString alloc] initWithString:@"partially on screen y"]; + label2.frame = CGRectMake(0, 20, 100, 20); + + [node addSubnode:label1]; + [node addSubnode:label2]; + + NSArray *elements = [node.view accessibilityElements]; + XCTAssertTrue(elements.count == 2); + XCTAssertTrue([elements containsObject:label1.view]); + XCTAssertTrue([elements containsObject:label2.view]); + + node.accessibilityElementsHidden = YES; + elements = [node.view accessibilityElements]; + XCTAssertTrue(elements.count == 0); + + node.accessibilityElementsHidden = NO; + elements = [node.view accessibilityElements]; + XCTAssertTrue(elements.count == 2); + XCTAssertTrue([elements containsObject:label1.view]); + XCTAssertTrue([elements containsObject:label2.view]); +} @end