// // _ASDisplayViewAccessiblity.mm // Texture // // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // This source code is licensed under the BSD-style license found in the // LICENSE file in the /ASDK-Licenses directory of this source tree. An additional // grant of patent rights can be found in the PATENTS file in the same directory. // // Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, // Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // #ifndef ASDK_ACCESSIBILITY_DISABLE #import #import #import #import #import #import #import NS_INLINE UIAccessibilityTraits InteractiveAccessibilityTraitsMask() { return UIAccessibilityTraitLink | UIAccessibilityTraitKeyboardKey | UIAccessibilityTraitButton; } #pragma mark - UIAccessibilityElement @protocol ASAccessibilityElementPositioning @property (nonatomic, readonly) CGRect accessibilityFrame; @end typedef NSComparisonResult (^SortAccessibilityElementsComparator)(id, id); /// Sort accessiblity elements first by y and than by x origin. static void SortAccessibilityElements(NSMutableArray *elements) { ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray"); static SortAccessibilityElementsComparator comparator = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ comparator = ^NSComparisonResult(id a, id b) { CGPoint originA = a.accessibilityFrame.origin; CGPoint originB = b.accessibilityFrame.origin; if (originA.y == originB.y) { if (originA.x == originB.x) { return NSOrderedSame; } return (originA.x < originB.x) ? NSOrderedAscending : NSOrderedDescending; } return (originA.y < originB.y) ? NSOrderedAscending : NSOrderedDescending; }; }); [elements sortUsingComparator:comparator]; } @interface ASAccessibilityElement : UIAccessibilityElement @property (nonatomic, strong) ASDisplayNode *node; @property (nonatomic, strong) ASDisplayNode *containerNode; + (ASAccessibilityElement *)accessibilityElementWithContainer:(UIView *)container node:(ASDisplayNode *)node containerNode:(ASDisplayNode *)containerNode; @end @implementation ASAccessibilityElement + (ASAccessibilityElement *)accessibilityElementWithContainer:(UIView *)container node:(ASDisplayNode *)node containerNode:(ASDisplayNode *)containerNode { ASAccessibilityElement *accessibilityElement = [[ASAccessibilityElement alloc] initWithAccessibilityContainer:container]; accessibilityElement.node = node; accessibilityElement.containerNode = containerNode; accessibilityElement.accessibilityIdentifier = node.accessibilityIdentifier; accessibilityElement.accessibilityLabel = node.accessibilityLabel; accessibilityElement.accessibilityHint = node.accessibilityHint; accessibilityElement.accessibilityValue = node.accessibilityValue; accessibilityElement.accessibilityTraits = node.accessibilityTraits; #if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 if (AS_AVAILABLE_IOS(11)) { accessibilityElement.accessibilityAttributedLabel = node.accessibilityAttributedLabel; accessibilityElement.accessibilityAttributedHint = node.accessibilityAttributedHint; accessibilityElement.accessibilityAttributedValue = node.accessibilityAttributedValue; } #endif return accessibilityElement; } - (CGRect)accessibilityFrame { CGRect accessibilityFrame = [self.containerNode convertRect:self.node.bounds fromNode:self.node]; accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(accessibilityFrame, self.accessibilityContainer); return accessibilityFrame; } @end #pragma mark - _ASDisplayView / UIAccessibilityContainer @interface ASAccessibilityCustomAction : UIAccessibilityCustomAction @property (nonatomic, strong) UIView *container; @property (nonatomic, strong) ASDisplayNode *node; @property (nonatomic, strong) ASDisplayNode *containerNode; @end @implementation ASAccessibilityCustomAction - (CGRect)accessibilityFrame { CGRect accessibilityFrame = [self.containerNode convertRect:self.node.bounds fromNode:self.node]; accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(accessibilityFrame, self.container); return accessibilityFrame; } @end /// Collect all subnodes for the given node by walking down the subnode tree and calculates the screen coordinates based on the containerNode and container static void CollectUIAccessibilityElementsForNode(ASDisplayNode *node, ASDisplayNode *containerNode, id container, NSMutableArray *elements) { ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray"); ASDisplayNodePerformBlockOnEveryNodeBFS(node, ^(ASDisplayNode * _Nonnull currentNode) { // For every subnode that is layer backed or it's supernode has subtree rasterization enabled // we have to create a UIAccessibilityElement as no view for this node exists if (currentNode != containerNode && currentNode.isAccessibilityElement) { UIAccessibilityElement *accessibilityElement = [ASAccessibilityElement accessibilityElementWithContainer:container node:currentNode containerNode:containerNode]; [elements addObject:accessibilityElement]; } }); } static void CollectAccessibilityElementsForContainer(ASDisplayNode *container, _ASDisplayView *view, NSMutableArray *elements) { UIAccessibilityElement *accessiblityElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:container containerNode:container]; NSMutableArray *labeledNodes = [NSMutableArray array]; NSMutableArray *actions = [NSMutableArray array]; std::queue queue; queue.push(container); ASDisplayNode *node; while (!queue.empty()) { node = queue.front(); queue.pop(); if (node != container && node.isAccessibilityContainer) { CollectAccessibilityElementsForContainer(node, view, elements); continue; } if (node.accessibilityLabel.length > 0) { if (node.accessibilityTraits & InteractiveAccessibilityTraitsMask()) { ASAccessibilityCustomAction *action = [[ASAccessibilityCustomAction alloc] initWithName:node.accessibilityLabel target:node selector:@selector(performAccessibilityCustomAction:)]; action.node = node; action.containerNode = node.supernode; action.container = node.supernode.view; [actions addObject:action]; } else { // Even though not surfaced to UIKit, create a non-interactive element for purposes of building sorted aggregated label. ASAccessibilityElement *nonInteractiveElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:node containerNode:container]; [labeledNodes addObject:nonInteractiveElement]; } } for (ASDisplayNode *subnode in node.subnodes) { queue.push(subnode); } } SortAccessibilityElements(labeledNodes); #if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 if (AS_AVAILABLE_IOS(11)) { NSArray *attributedLabels = [labeledNodes valueForKey:@"accessibilityAttributedLabel"]; NSMutableAttributedString *attributedLabel = [NSMutableAttributedString new]; [attributedLabels enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if (idx != 0) { [attributedLabel appendAttributedString:[[NSAttributedString alloc] initWithString:@", "]]; } [attributedLabel appendAttributedString:(NSAttributedString *)obj]; }]; accessiblityElement.accessibilityAttributedLabel = attributedLabel; } else #endif { NSArray *labels = [labeledNodes valueForKey:@"accessibilityLabel"]; accessiblityElement.accessibilityLabel = [labels componentsJoinedByString:@", "]; } SortAccessibilityElements(actions); accessiblityElement.accessibilityCustomActions = actions; [elements addObject:accessiblityElement]; } /// Collect all accessibliity elements for a given view and view node static void CollectAccessibilityElementsForView(_ASDisplayView *view, NSMutableArray *elements) { ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray"); ASDisplayNode *node = view.asyncdisplaykit_node; if (node.isAccessibilityContainer) { CollectAccessibilityElementsForContainer(node, view, elements); return; } // Handle rasterize case if (node.rasterizesSubtree) { CollectUIAccessibilityElementsForNode(node, node, view, elements); return; } for (ASDisplayNode *subnode in node.subnodes) { if (subnode.isAccessibilityElement) { // An accessiblityElement can either be a UIView or a UIAccessibilityElement if (subnode.isLayerBacked) { // No view for layer backed nodes exist. It's necessary to create a UIAccessibilityElement that represents this node UIAccessibilityElement *accessiblityElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:subnode containerNode:node]; [elements addObject:accessiblityElement]; } else { // Accessiblity element is not layer backed just add the view as accessibility element [elements addObject:subnode.view]; } } else if (subnode.isLayerBacked) { // Go down the hierarchy of the layer backed subnode and collect all of the UIAccessibilityElement CollectUIAccessibilityElementsForNode(subnode, node, view, elements); } else if ([subnode accessibilityElementCount] > 0) { // UIView is itself a UIAccessibilityContainer just add it [elements addObject:subnode.view]; } } } @interface _ASDisplayView () { NSArray *_accessibleElements; } @end @implementation _ASDisplayView (UIAccessibilityContainer) #pragma mark - UIAccessibility - (void)setAccessibleElements:(NSArray *)accessibleElements { _accessibleElements = nil; } - (NSArray *)accessibleElements { ASDisplayNode *viewNode = self.asyncdisplaykit_node; if (viewNode == nil) { return @[]; } if (_accessibleElements != nil) { return _accessibleElements; } NSMutableArray *accessibleElements = [NSMutableArray array]; CollectAccessibilityElementsForView(self, accessibleElements); SortAccessibilityElements(accessibleElements); _accessibleElements = accessibleElements; return _accessibleElements; } - (NSInteger)accessibilityElementCount { return self.accessibleElements.count; } - (id)accessibilityElementAtIndex:(NSInteger)index { return self.accessibleElements[index]; } - (NSInteger)indexOfAccessibilityElement:(id)element { return [self.accessibleElements indexOfObjectIdenticalTo:element]; } @end #endif