mirror of
https://github.com/TextureGroup/Texture.git
synced 2026-04-07 19:17:39 +00:00
414 lines
15 KiB
Plaintext
414 lines
15 KiB
Plaintext
//
|
|
// _ASDisplayViewAccessiblity.mm
|
|
// Texture
|
|
//
|
|
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
|
|
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
|
|
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
|
|
#ifndef ASDK_ACCESSIBILITY_DISABLE
|
|
|
|
#import <AsyncDisplayKit/_ASDisplayViewAccessiblity.h>
|
|
#import <AsyncDisplayKit/_ASDisplayView.h>
|
|
#import <AsyncDisplayKit/ASAvailability.h>
|
|
#import <AsyncDisplayKit/ASCollectionNode.h>
|
|
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
|
|
#import <AsyncDisplayKit/ASDisplayNodeInternal.h>
|
|
#import <AsyncDisplayKit/ASTableNode.h>
|
|
|
|
#import <queue>
|
|
|
|
NS_INLINE UIAccessibilityTraits InteractiveAccessibilityTraitsMask() {
|
|
return UIAccessibilityTraitLink | UIAccessibilityTraitKeyboardKey | UIAccessibilityTraitButton;
|
|
}
|
|
|
|
#pragma mark - UIAccessibilityElement
|
|
|
|
@protocol ASAccessibilityElementPositioning
|
|
|
|
@property (nonatomic, readonly) CGRect accessibilityFrame;
|
|
|
|
@end
|
|
|
|
typedef NSComparisonResult (^SortAccessibilityElementsComparator)(id<ASAccessibilityElementPositioning>, id<ASAccessibilityElementPositioning>);
|
|
|
|
/// 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<ASAccessibilityElementPositioning> a, id<ASAccessibilityElementPositioning> 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<ASAccessibilityElementPositioning>
|
|
|
|
@property (nonatomic) ASDisplayNode *containerNode;
|
|
@property (nonatomic) ASDisplayNode *node;
|
|
|
|
+ (ASAccessibilityElement *)accessibilityElementWithContainerNode:(ASDisplayNode *)containerNode node:(ASDisplayNode *)node;
|
|
|
|
@end
|
|
|
|
@implementation ASAccessibilityElement
|
|
|
|
+ (ASAccessibilityElement *)accessibilityElementWithContainerNode:(ASDisplayNode *)containerNode node:(ASDisplayNode *)node
|
|
{
|
|
ASAccessibilityElement *accessibilityElement = [[ASAccessibilityElement alloc] initWithAccessibilityContainer:containerNode.view];
|
|
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 (AS_AVAILABLE_IOS_TVOS(11, 11)) {
|
|
accessibilityElement.accessibilityAttributedLabel = node.accessibilityAttributedLabel;
|
|
accessibilityElement.accessibilityAttributedHint = node.accessibilityAttributedHint;
|
|
accessibilityElement.accessibilityAttributedValue = node.accessibilityAttributedValue;
|
|
}
|
|
return accessibilityElement;
|
|
}
|
|
|
|
- (CGRect)accessibilityFrame
|
|
{
|
|
CGRect accessibilityFrame = [self.containerNode convertRect:self.node.bounds fromNode:self.node];
|
|
return UIAccessibilityConvertFrameToScreenCoordinates(accessibilityFrame, self.accessibilityContainer);
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark - _ASDisplayView / UIAccessibilityContainer
|
|
|
|
@interface ASAccessibilityCustomAction : UIAccessibilityCustomAction<ASAccessibilityElementPositioning>
|
|
|
|
@property (nonatomic) ASDisplayNode *containerNode;
|
|
@property (nonatomic) ASDisplayNode *node;
|
|
|
|
@end
|
|
|
|
@implementation ASAccessibilityCustomAction
|
|
|
|
- (CGRect)accessibilityFrame
|
|
{
|
|
ASDisplayNode *containerNode = self.containerNode;
|
|
ASDisplayNode *node = self.node;
|
|
ASDisplayNodeCAssertNotNil(containerNode, @"ASAccessibilityCustomAction needs a container node.");
|
|
ASDisplayNodeCAssertNotNil(node, @"ASAccessibilityCustomAction needs a node.");
|
|
CGRect accessibilityFrame = [containerNode convertRect:node.bounds fromNode:node];
|
|
return UIAccessibilityConvertFrameToScreenCoordinates(accessibilityFrame, containerNode.view);
|
|
}
|
|
|
|
@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. This is necessary for layer backed nodes or rasterrized subtrees as no UIView instance for this node exists.
|
|
static void CollectAccessibilityElementsForLayerBackedOrRasterizedNode(ASDisplayNode *containerNode, ASDisplayNode *node, NSMutableArray *elements)
|
|
{
|
|
ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray");
|
|
|
|
// Iterate any node in the tree and either collect nodes that are accessibility elements
|
|
// or leaf nodes that are accessibility containers
|
|
ASDisplayNodePerformBlockOnEveryNodeBFS(node, ^(ASDisplayNode * _Nonnull currentNode) {
|
|
if (currentNode != containerNode) {
|
|
if (currentNode.isAccessibilityElement) {
|
|
// 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
|
|
UIAccessibilityElement *accessibilityElement = [ASAccessibilityElement accessibilityElementWithContainerNode:containerNode node:currentNode];
|
|
[elements addObject:accessibilityElement];
|
|
} else if (currentNode.subnodes.count == 0) {
|
|
// In leaf nodes that are layer backed and acting as accessibility container we call
|
|
// through to the accessibilityElements method.
|
|
[elements addObjectsFromArray:currentNode.accessibilityElements];
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Called from the usual accessibility elements collection function for a container to collect all subnodes accessibilityLabels
|
|
static void AggregateSublabelsOrCustomActionsForContainerNode(ASDisplayNode *container, NSMutableArray *elements) {
|
|
UIAccessibilityElement *accessiblityElement = [ASAccessibilityElement accessibilityElementWithContainerNode:container node:container];
|
|
|
|
NSMutableArray<ASAccessibilityElement *> *labeledNodes = [[NSMutableArray alloc] init];
|
|
NSMutableArray<ASAccessibilityCustomAction *> *actions = [[NSMutableArray alloc] init];
|
|
|
|
// If the container does not have an accessibility label set, or if the label is meant for custom
|
|
// actions only, then aggregate its subnodes' labels. Otherwise, treat the label as an overriden
|
|
// value and do not perform the aggregation.
|
|
BOOL shouldAggregateSubnodeLabels =
|
|
(container.accessibilityLabel.length == 0) ||
|
|
(container.accessibilityTraits & InteractiveAccessibilityTraitsMask());
|
|
|
|
std::queue<ASDisplayNode *> queue;
|
|
queue.push(container);
|
|
ASDisplayNode *node = nil;
|
|
while (!queue.empty()) {
|
|
node = queue.front();
|
|
queue.pop();
|
|
|
|
// Only handle accessibility containers
|
|
if (node != container && node.isAccessibilityContainer) {
|
|
AggregateSublabelsOrCustomActionsForContainerNode(node, elements);
|
|
continue;
|
|
}
|
|
|
|
// Aggregate either custom actions for specific accessibility traits or the accessibility labels
|
|
// of the node
|
|
if (node.accessibilityLabel.length > 0) {
|
|
if (node.accessibilityTraits & InteractiveAccessibilityTraitsMask()) {
|
|
ASAccessibilityCustomAction *action = [[ASAccessibilityCustomAction alloc] initWithName:node.accessibilityLabel target:node selector:@selector(performAccessibilityCustomAction:)];
|
|
action.containerNode = node.supernode;
|
|
action.node = node;
|
|
[actions addObject:action];
|
|
} else if (node == container || shouldAggregateSubnodeLabels) {
|
|
// Even though not surfaced to UIKit, create a non-interactive element for purposes
|
|
// of building sorted aggregated label.
|
|
ASAccessibilityElement *nonInteractiveElement = [ASAccessibilityElement accessibilityElementWithContainerNode:container node:node];
|
|
[labeledNodes addObject:nonInteractiveElement];
|
|
}
|
|
}
|
|
|
|
for (ASDisplayNode *subnode in node.subnodes) {
|
|
queue.push(subnode);
|
|
}
|
|
}
|
|
|
|
SortAccessibilityElements(labeledNodes);
|
|
|
|
if (AS_AVAILABLE_IOS_TVOS(11, 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 {
|
|
NSArray *labels = [labeledNodes valueForKey:@"accessibilityLabel"];
|
|
accessiblityElement.accessibilityLabel = [labels componentsJoinedByString:@", "];
|
|
}
|
|
|
|
SortAccessibilityElements(actions);
|
|
accessiblityElement.accessibilityCustomActions = actions;
|
|
|
|
[elements addObject:accessiblityElement];
|
|
}
|
|
|
|
/// Collect all accessibliity elements for a given node
|
|
static void CollectAccessibilityElements(ASDisplayNode *node, NSMutableArray *elements)
|
|
{
|
|
ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray");
|
|
|
|
BOOL anySubNodeIsCollection = (nil != ASDisplayNodeFindFirstNode(node,
|
|
^BOOL(ASDisplayNode *nodeToCheck) {
|
|
return ASDynamicCast(nodeToCheck, ASCollectionNode) != nil ||
|
|
ASDynamicCast(nodeToCheck, ASTableNode) != nil;
|
|
}));
|
|
|
|
// Handle an accessibility container (collects accessibility labels or custom actions)
|
|
if (node.isAccessibilityContainer && !anySubNodeIsCollection) {
|
|
AggregateSublabelsOrCustomActionsForContainerNode(node, elements);
|
|
return;
|
|
}
|
|
|
|
// Handle a rasterize node
|
|
if (node.rasterizesSubtree) {
|
|
CollectAccessibilityElementsForLayerBackedOrRasterizedNode(node, node, elements);
|
|
return;
|
|
}
|
|
|
|
// Go down each subnodes and collect all accessibility elements
|
|
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 accessibilityElementWithContainerNode:node node:subnode];
|
|
[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 for layer backed subnodes which are also accessibility container
|
|
// and collect all of the UIAccessibilityElement
|
|
CollectAccessibilityElementsForLayerBackedOrRasterizedNode(node, subnode, elements);
|
|
} else if (subnode.accessibilityElementCount > 0) {
|
|
// _ASDisplayView is itself a UIAccessibilityContainer just add it, UIKit will call the accessiblity
|
|
// methods of the nodes _ASDisplayView
|
|
[elements addObject:subnode.view];
|
|
}
|
|
}
|
|
}
|
|
|
|
@interface _ASDisplayView () {
|
|
NSArray *_accessibilityElements;
|
|
_ASDisplayViewAccessibilityFlags _accessibilityFlags;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation _ASDisplayView (UIAccessibilityContainer)
|
|
|
|
#pragma mark - UIAccessibility
|
|
|
|
- (NSInteger)accessibilityElementCount
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
if (_accessibilityFlags.inAccessibilityElementCount) {
|
|
return [super accessibilityElementCount];
|
|
}
|
|
_accessibilityFlags.inAccessibilityElementCount = YES;
|
|
NSInteger accessibilityElementCount = [self.asyncdisplaykit_node accessibilityElementCount];
|
|
_accessibilityFlags.inAccessibilityElementCount = NO;
|
|
return accessibilityElementCount;
|
|
}
|
|
|
|
- (NSInteger)indexOfAccessibilityElement:(id)element
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
if (_accessibilityFlags.inIndexOfAccessibilityElement) {
|
|
return [super indexOfAccessibilityElement:element];
|
|
}
|
|
_accessibilityFlags.inIndexOfAccessibilityElement = YES;
|
|
NSInteger indexOfAccessibilityElement = [self.asyncdisplaykit_node indexOfAccessibilityElement:element];
|
|
_accessibilityFlags.inIndexOfAccessibilityElement = NO;
|
|
return indexOfAccessibilityElement;
|
|
}
|
|
|
|
- (id)accessibilityElementAtIndex:(NSInteger)index
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
if (_accessibilityFlags.inAccessibilityElementAtIndex) {
|
|
return [super accessibilityElementAtIndex:index];
|
|
}
|
|
_accessibilityFlags.inAccessibilityElementAtIndex = YES;
|
|
id accessibilityElement = [self.asyncdisplaykit_node accessibilityElementAtIndex:index];
|
|
_accessibilityFlags.inAccessibilityElementAtIndex = NO;
|
|
return accessibilityElement;
|
|
}
|
|
|
|
- (void)setAccessibilityElements:(NSArray *)accessibilityElements
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
if (_accessibilityFlags.inSetAccessibilityElements) {
|
|
return [super setAccessibilityElements:accessibilityElements];
|
|
}
|
|
_accessibilityFlags.inSetAccessibilityElements = YES;
|
|
[self.asyncdisplaykit_node setAccessibilityElements:accessibilityElements];
|
|
_accessibilityElements = accessibilityElements;
|
|
_accessibilityFlags.inSetAccessibilityElements = NO;
|
|
}
|
|
|
|
- (NSArray *)accessibilityElements
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
|
|
ASDisplayNode *viewNode = self.asyncdisplaykit_node;
|
|
if (viewNode == nil) {
|
|
return @[];
|
|
}
|
|
|
|
// return [viewNode accessibilityElements];
|
|
|
|
if (_accessibilityElements == nil) {
|
|
_accessibilityElements = [viewNode accessibilityElements];
|
|
}
|
|
return _accessibilityElements;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation ASDisplayNode (AccessibilityInternal)
|
|
|
|
- (NSInteger)accessibilityElementCount
|
|
{
|
|
return [_view accessibilityElementCount];
|
|
}
|
|
|
|
- (NSInteger)indexOfAccessibilityElement:(id)element
|
|
{
|
|
return [_view indexOfAccessibilityElement:element];
|
|
}
|
|
|
|
- (id)accessibilityElementAtIndex:(NSInteger)index
|
|
{
|
|
return [_view accessibilityElementAtIndex:index];
|
|
}
|
|
|
|
- (void)setAccessibilityElements:(NSArray *)accessibilityElements
|
|
{
|
|
_accessibilityElements = accessibilityElements;
|
|
[_view setAccessibilityElements:accessibilityElements];
|
|
}
|
|
|
|
- (NSArray *)accessibilityElements
|
|
{
|
|
if (!self.isNodeLoaded) {
|
|
ASDisplayNodeFailAssert(@"Cannot access accessibilityElements since node is not loaded");
|
|
return @[];
|
|
}
|
|
|
|
// if (_accessibilityElements == nil) {
|
|
NSMutableArray *accessibilityElements = [[NSMutableArray alloc] init];
|
|
CollectAccessibilityElements(self, accessibilityElements);
|
|
SortAccessibilityElements(accessibilityElements);
|
|
_accessibilityElements = accessibilityElements;
|
|
// }
|
|
return _accessibilityElements;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation _ASDisplayView (UIAccessibilityAction)
|
|
|
|
- (BOOL)accessibilityActivate
|
|
{
|
|
return [self.asyncdisplaykit_node accessibilityActivate];
|
|
}
|
|
|
|
- (void)accessibilityIncrement
|
|
{
|
|
[self.asyncdisplaykit_node accessibilityIncrement];
|
|
}
|
|
|
|
- (void)accessibilityDecrement
|
|
{
|
|
[self.asyncdisplaykit_node accessibilityDecrement];
|
|
}
|
|
|
|
- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction
|
|
{
|
|
return [self.asyncdisplaykit_node accessibilityScroll:direction];
|
|
}
|
|
|
|
- (BOOL)accessibilityPerformEscape
|
|
{
|
|
return [self.asyncdisplaykit_node accessibilityPerformEscape];
|
|
}
|
|
|
|
- (BOOL)accessibilityPerformMagicTap
|
|
{
|
|
return [self.asyncdisplaykit_node accessibilityPerformMagicTap];
|
|
}
|
|
|
|
@end
|
|
|
|
#endif
|