Skip to content

Commit

Permalink
Next
Browse files Browse the repository at this point in the history
  • Loading branch information
maicki committed May 30, 2019
1 parent 4e294ff commit 0f5be75
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 50 deletions.
10 changes: 7 additions & 3 deletions Source/ASDisplayNode+Beta.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,13 @@ typedef struct {
@property (readonly) ASDisplayNodePerformanceMeasurements performanceMeasurements;

/**
* @abstract Whether this node acts as an accessibility container. If set to YES, then this node's accessibility label will represent
* an aggregation of all child nodes' accessibility labels. Nodes in this node's subtree that are also accessibility containers will
* not be included in this aggregation, and will be exposed as separate accessibility elements to UIKit.
* @abstract Whether this node acts as an accessibility container. If set to YES, then this node's
* accessibility label will represent an aggregation of all child nodes' accessibility labels and
* this node's accessibility custom actions will be represented will be the aggregation of all
* child nodes that have a accessibility trait of UIAccessibilityTraitLink,
* UIAccessibilityTraitKeyboardKey or UIAccessibilityTraitButton.
* Nodes in this node's subtree that are also accessibility containers will not be included in this
* aggregation, and will be exposed as separate accessibility elements to UIKit.
*/
@property BOOL isAccessibilityContainer;

Expand Down
35 changes: 24 additions & 11 deletions Source/ASTextNode2.mm
Original file line number Diff line number Diff line change
Expand Up @@ -329,27 +329,39 @@ - (UIAccessibilityTraits)defaultAccessibilityTraits
return UIAccessibilityTraitStaticText;
}

static void ASUpdateAccessibilityFrame(ASTextNodeAccessiblityElement *accessibilityElement, ASTextLayout *layout, UIView *view) {
static void ASUpdateAccessibilityFrame(ASTextNodeAccessiblityElement *accessibilityElement, ASTextLayout *layout, ASDisplayNode *node) {
ASDisplayNode *containerNode = ASFirstNonLayerBackedSupernodeForNode(node);
if (accessibilityElement.accessibilityRange.location == NSNotFound) {
// If no accessibilityRange was specified (as is done for the text element), just use the label's frame.
CGRect range = [layout rectForRange:[ASTextRange rangeWithRange:NSMakeRange(0, accessibilityElement.accessibilityLabel.length)]];
CGRect frame = [layout rectForRange:[ASTextRange rangeWithRange:NSMakeRange(0, accessibilityElement.accessibilityLabel.length)]];
// Handle text is too large for the layout's text container size
if (range.origin.x == INFINITY) {
range = [layout rectForRange:[ASTextRange rangeWithRange:layout.visibleRange]];
if (frame.origin.x == INFINITY) {
frame = [layout rectForRange:[ASTextRange rangeWithRange:layout.visibleRange]];
}
accessibilityElement.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(range, view);
CGRect accessibilityFrame = [containerNode convertRect:frame fromNode:node];
accessibilityElement.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(accessibilityFrame, containerNode.view);
} else {
CGRect frame = [layout rectForRange:[ASTextRange rangeWithRange:accessibilityElement.accessibilityRange]];
accessibilityElement.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(frame, view);
CGRect accessibilityFrame = [containerNode convertRect:frame fromNode:node];
accessibilityElement.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(accessibilityFrame, containerNode.view);
}
}

static ASDisplayNode *ASFirstNonLayerBackedSupernodeForNode(ASDisplayNode *node) {
ASDisplayNode *containerNode = node;
while (containerNode.isLayerBacked) {
containerNode = containerNode.supernode;
}
return containerNode;
}

// Overwrite accessibilityElementAtIndex: so we can update the element's accessibilityFrame when it is requested.
- (id)accessibilityElementAtIndex:(NSInteger)index
{
ASTextNodeAccessiblityElement *accessibilityElement = self.accessibilityElements[index];

ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(_textContainer, _attributedText);
ASUpdateAccessibilityFrame(accessibilityElement, layout, self.view);
ASUpdateAccessibilityFrame(accessibilityElement, layout, self);
return accessibilityElement;
}

Expand All @@ -369,16 +381,17 @@ - (NSArray *)accessibilityElements
return _accessibilityElements;
}

UIView *view = self.view;
// Searc the first node that is not layer backed
ASDisplayNode *containerNode = ASFirstNonLayerBackedSupernodeForNode(self);
UIAccessibilityTraits accessibilityTraits = self.accessibilityTraits;
ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(_textContainer, attributedText);

// Create an accessibility element to represent the label's text. It's not necessary to specify
// a accessibilityRange here, as the entirety of the text is being represented.
ASTextNodeAccessiblityElement *accessibilityElement = [[ASTextNodeAccessiblityElement alloc] initWithAccessibilityContainer:self];
ASTextNodeAccessiblityElement *accessibilityElement = [[ASTextNodeAccessiblityElement alloc] initWithAccessibilityContainer:containerNode.view];
accessibilityElement.accessibilityTraits = accessibilityTraits;
accessibilityElement.accessibilityLabel = self.accessibilityLabel;
ASUpdateAccessibilityFrame(accessibilityElement, layout, view);
ASUpdateAccessibilityFrame(accessibilityElement, layout, self);
[accessibilityElements addObject:accessibilityElement];

if (ASActivateExperimentalFeature(ASExperimentalExposeTextLinksForA11Y)) {
Expand All @@ -393,7 +406,7 @@ - (NSArray *)accessibilityElements
accessibilityElement.accessibilityTraits = accessibilityTraits;
accessibilityElement.accessibilityLabel = [attributedText attributedSubstringFromRange:range].string;
accessibilityElement.accessibilityRange = range;
ASUpdateAccessibilityFrame(accessibilityElement, layout, view);
ASUpdateAccessibilityFrame(accessibilityElement, layout, self);
[accessibilityElements addObject:accessibilityElement];
}];
}
Expand Down
80 changes: 46 additions & 34 deletions Source/Details/_ASDisplayViewAccessiblity.mm
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,18 @@ static void SortAccessibilityElements(NSMutableArray *elements)

@interface ASAccessibilityElement : UIAccessibilityElement<ASAccessibilityElementPositioning>

@property (nonatomic) ASDisplayNode *node;
@property (nonatomic) ASDisplayNode *containerNode;
@property (nonatomic) ASDisplayNode *node;

+ (ASAccessibilityElement *)accessibilityElementWithContainer:(UIView *)container node:(ASDisplayNode *)node containerNode:(ASDisplayNode *)containerNode;
+ (ASAccessibilityElement *)accessibilityElementWithContainerNode:(ASDisplayNode *)containerNode node:(ASDisplayNode *)node;

@end

@implementation ASAccessibilityElement

+ (ASAccessibilityElement *)accessibilityElementWithContainer:(UIView *)container node:(ASDisplayNode *)node containerNode:(ASDisplayNode *)containerNode
+ (ASAccessibilityElement *)accessibilityElementWithContainerNode:(ASDisplayNode *)containerNode node:(ASDisplayNode *)node
{
ASAccessibilityElement *accessibilityElement = [[ASAccessibilityElement alloc] initWithAccessibilityContainer:container];
ASAccessibilityElement *accessibilityElement = [[ASAccessibilityElement alloc] initWithAccessibilityContainer:containerNode.view];
accessibilityElement.node = node;
accessibilityElement.containerNode = containerNode;
accessibilityElement.accessibilityIdentifier = node.accessibilityIdentifier;
Expand Down Expand Up @@ -97,44 +97,53 @@ - (CGRect)accessibilityFrame

@interface ASAccessibilityCustomAction : UIAccessibilityCustomAction<ASAccessibilityElementPositioning>

@property (nonatomic) UIView *container;
@property (nonatomic) ASDisplayNode *node;
@property (nonatomic) ASDisplayNode *containerNode;
@property (nonatomic) ASDisplayNode *node;

@end

@implementation ASAccessibilityCustomAction

- (CGRect)accessibilityFrame
{
CGRect accessibilityFrame = [self.containerNode convertRect:self.node.bounds fromNode:self.node];
return UIAccessibilityConvertFrameToScreenCoordinates(accessibilityFrame, self.container);
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 CollectUIAccessibilityElementsForNode(ASDisplayNode *node, ASDisplayNode *containerNode, UIView *container, NSMutableArray *elements)
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) {
// 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) {
if (currentNode.isAccessibilityElement) {
UIAccessibilityElement *accessibilityElement = [ASAccessibilityElement accessibilityElementWithContainer:container node:currentNode containerNode:containerNode];
// 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 {
} else if (currentNode.subnodes.count == 0) {
// In leaf nodes that are acting as accessibility container we call
// through to the accessibilityElements method.
// It's important that leaf layer backed nodes that are UIAccessibilityContainer
// implement accessibilityElements with not having to have a view loaded
[elements addObjectsFromArray:currentNode.accessibilityElements];
}
}
});
}

/// Called from the usual acccessibility elements collection function for a container to collect all subnodes accessibilityLabels
static void CollectAccessibilityElementsForContainer(ASDisplayNode *container, UIView *view, NSMutableArray *elements) {
UIAccessibilityElement *accessiblityElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:container containerNode:container];
/// 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];
Expand All @@ -153,21 +162,24 @@ static void CollectAccessibilityElementsForContainer(ASDisplayNode *container, U
node = queue.front();
queue.pop();

// Only handle accessibility containers
if (node != container && node.isAccessibilityContainer) {
CollectAccessibilityElementsForContainer(node, view, elements);
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.node = node;
action.containerNode = node.supernode;
action.container = node.supernode.view;
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 accessibilityElementWithContainer:view node:node containerNode:container];
// 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];
}
}
Expand Down Expand Up @@ -200,27 +212,26 @@ static void CollectAccessibilityElementsForContainer(ASDisplayNode *container, U
[elements addObject:accessiblityElement];
}

/// Collect all accessibliity elements for a given view and view node
static void CollectAccessibilityElementsForView(UIView *view, NSMutableArray *elements)
/// Collect all accessibliity elements for a given node
static void CollectAccessibilityElements(ASDisplayNode *node, NSMutableArray *elements)
{
ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray");

ASDisplayNode *node = view.asyncdisplaykit_node;

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) {
CollectAccessibilityElementsForContainer(node, view, elements);
AggregateSublabelsOrCustomActionsForContainerNode(node, elements);
return;
}

// Handle rasterize case
// Handle a rasterize node
if (node.rasterizesSubtree) {
CollectUIAccessibilityElementsForNode(node, node, view, elements);
CollectAccessibilityElementsForLayerBackedOrRasterizedNode(node, node, elements);
return;
}

Expand All @@ -230,16 +241,17 @@ static void CollectAccessibilityElementsForView(UIView *view, NSMutableArray *el
// 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];
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 and collect all of the UIAccessibilityElement
CollectUIAccessibilityElementsForNode(subnode, node, view, elements);
} else if ([subnode accessibilityElementCount] > 0) {
// Go down the hierarchy for layer backed subnodes which are also accessibility containe
// 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];
Expand Down Expand Up @@ -350,7 +362,7 @@ - (NSArray *)accessibilityElements

if (_accessibilityElements == nil) {
NSMutableArray *accessibilityElements = [[NSMutableArray alloc] init];
CollectAccessibilityElementsForView(self.view, accessibilityElements);
CollectAccessibilityElements(self, accessibilityElements);
SortAccessibilityElements(accessibilityElements);
_accessibilityElements = accessibilityElements;
}
Expand Down
59 changes: 57 additions & 2 deletions Tests/ASTextNode2Tests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ - (void)testTruncation
XCTAssertTrue(_textNode.isTruncated, @"Text Node should be truncated");
}

- (void)testAccessibility
- (void)testBasicAccessibility
{
XCTAssertFalse(_textNode.isAccessibilityElement, @"Is not an accessiblity element as it's a UIAccessibilityContainer");
XCTAssertTrue(_textNode.accessibilityTraits == UIAccessibilityTraitStaticText,
Expand All @@ -104,7 +104,62 @@ - (void)testAccessibility
[_textNode.accessibilityElements[0] accessibilityLabel], _textNode.accessibilityLabel);
}

- (void)testExposeA11YLinks
- (void)testAccessibilityLayerBackedContainerAndTextNode2
{
ASDisplayNode *container = [[ASDisplayNode alloc] init];
container.frame = CGRectMake(50, 50, 200, 600);
container.backgroundColor = [UIColor grayColor];

ASDisplayNode *layerBackedContainer = [[ASDisplayNode alloc] init];
layerBackedContainer.layerBacked = YES;
layerBackedContainer.frame = CGRectMake(50, 50, 200, 600);
layerBackedContainer.backgroundColor = [UIColor grayColor];
[container addSubnode:layerBackedContainer];

ASTextNode2 *text = [[ASTextNode2 alloc] init];
text.layerBacked = YES;
text.attributedText = [[NSAttributedString alloc] initWithString:@"hello"];
text.frame = CGRectMake(50, 100, 200, 200);
[layerBackedContainer addSubnode:text];

ASTextNode2 *text2 = [[ASTextNode2 alloc] init];
text2.layerBacked = YES;
text2.attributedText = [[NSAttributedString alloc] initWithString:@"world"];
text2.frame = CGRectMake(50, 100, 200, 200);
[layerBackedContainer addSubnode:text2];

NSArray<UIAccessibilityElement *> *elements = container.view.accessibilityElements;
XCTAssertTrue(elements.count == 2);
XCTAssertTrue([[elements[0] accessibilityLabel] isEqualToString:@"hello"]);
XCTAssertTrue([[elements[1] accessibilityLabel] isEqualToString:@"world"]);
}

- (void)testAccessibilityLayerBackedTextNode2
{
ASDisplayNode *container = [[ASDisplayNode alloc] init];
container.frame = CGRectMake(50, 50, 200, 600);
container.backgroundColor = [UIColor grayColor];

ASTextNode2 *text = [[ASTextNode2 alloc] init];
text.layerBacked = YES;
text.attributedText = [[NSAttributedString alloc] initWithString:@"hello"];
text.frame = CGRectMake(50, 100, 200, 200);
[container addSubnode:text];

// Trigger calculation of layouts on both nodes manually otherwise the internal
// text container will not have any size
// (void)[text layoutThatFits:ASSizeRangeMake(CGSizeZero, container.frame.size)];
// (void)[container layoutThatFits:ASSizeRangeMake(CGSizeZero, container.frame.size)];
// [container layoutIfNeeded];
// [container.layer displayIfNeeded];

NSArray<UIAccessibilityElement *> *elements = container.view.accessibilityElements;
XCTAssertTrue(elements.count == 1);
XCTAssertTrue([[elements.firstObject accessibilityLabel] isEqualToString:@"hello"]);
// TODO: Also check for accessibilityFrame
}

- (void)testAccessibilityExposeA11YLinks
{
NSString *link = @"https://texturegroup.com";
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"Texture Website: %@", link]];
Expand Down

0 comments on commit 0f5be75

Please sign in to comment.