Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ASDisplayViewAccessibility] A few accessibility improvements #1812

Merged
merged 9 commits into from
May 6, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Source/ASExperimentalFeatures.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ typedef NS_OPTIONS(NSUInteger, ASExperimentalFeatures) {
ASExperimentalDrawingGlobal = 1 << 10, // exp_drawing_global
ASExperimentalOptimizeDataControllerPipeline = 1 << 11, // exp_optimize_data_controller_pipeline
ASExperimentalTraitCollectionDidChangeWithPreviousCollection = 1 << 12, // exp_trait_collection_did_change_with_previous_collection
ASExperimentalDoNotCacheAccessibilityElements = 1 << 13, // exp_do_not_cache_accessibility_elements
ASExperimentalFeatureAll = 0xFFFFFFFF
};

Expand Down
3 changes: 2 additions & 1 deletion Source/ASExperimentalFeatures.mm
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
@"exp_oom_bg_dealloc_disable",
@"exp_drawing_global",
@"exp_optimize_data_controller_pipeline",
@"exp_trait_collection_did_change_with_previous_collection"]));
@"exp_trait_collection_did_change_with_previous_collection",
@"exp_do_not_cache_accessibility_elements"]));
if (flags == ASExperimentalFeatureAll) {
return allNames;
}
Expand Down
14 changes: 14 additions & 0 deletions Source/Details/_ASDisplayViewAccessiblity.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,17 @@
// should still work as long as accessibility is enabled, this framework provides no guarantees on
// their correctness. For details, see
// https://developer.apple.com/documentation/objectivec/nsobject/1615147-accessibilityelements

// After recusively collecting all of the accessibility elements of a node, they get sorted. This sort determines
// the order that a screen reader will traverse the elements. By default, we sort these elements based on their
// origin: lower y origin comes first, then lower x origin. If 2 nodes have an equal origin, the node with the smaller
// height is placed before the node with the smaller width. If two nodes have the exact same rect, we throw up our hands
// and return NSOrderedSame.
//
// In general this seems to work fairly well. However, if you want to provide a custom sort you can do so via
// setUserDefinedComparator(). The two elements you are comparing are NSObjects, which conforms to the informal
rcancro marked this conversation as resolved.
Show resolved Hide resolved
// UIAccessibility protocol, so you can safely compare properties like accessibilityFrame.
typedef NSComparisonResult (^ASSortAccessibilityElementsComparator)(NSObject *, NSObject *);

// Use this method to supply your own custom sort comparator used to determine the order of the accessibility elements
void setUserDefinedAccessibilitySortComparator(ASSortAccessibilityElementsComparator userDefinedComparator);
69 changes: 47 additions & 22 deletions Source/Details/_ASDisplayViewAccessiblity.mm
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#ifndef ASDK_ACCESSIBILITY_DISABLE

#import <AsyncDisplayKit/_ASDisplayView.h>
#import <AsyncDisplayKit/_ASDisplayViewAccessiblity.h>
#import <AsyncDisplayKit/ASAvailability.h>
#import <AsyncDisplayKit/ASCollectionNode.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
Expand All @@ -21,43 +22,55 @@

#pragma mark - UIAccessibilityElement

@protocol ASAccessibilityElementPositioning
static ASSortAccessibilityElementsComparator currentAccessibilityComparator = nil;
static ASSortAccessibilityElementsComparator defaultComparator = nil;
rcancro marked this conversation as resolved.
Show resolved Hide resolved

@property (nonatomic, readonly) CGRect accessibilityFrame;

@end

typedef NSComparisonResult (^SortAccessibilityElementsComparator)(id<ASAccessibilityElementPositioning>, id<ASAccessibilityElementPositioning>);
void setUserDefinedAccessibilitySortComparator(ASSortAccessibilityElementsComparator userDefinedComparator) {
currentAccessibilityComparator = userDefinedComparator ?: defaultComparator;
}

/// Sort accessiblity elements first by y and than by x origin.
static void SortAccessibilityElements(NSMutableArray *elements)
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;
defaultComparator = ^NSComparisonResult(NSObject *a, NSObject *b) {
CGPoint originA = a.accessibilityFrame.origin;
CGPoint originB = b.accessibilityFrame.origin;
if (originA.y == originB.y) {
if (originA.x == originB.x) {
// if we have the same origin, favor shorter items. If heights are the same, favor thinner items. If size is the same ¯\_(ツ)_/¯
CGSize sizeA = a.accessibilityFrame.size;
CGSize sizeB = b.accessibilityFrame.size;
if (sizeA.height == sizeB.height) {
if (sizeA.width == sizeB.width) {
return NSOrderedSame;
}
return (sizeA.width < sizeB.width) ? NSOrderedAscending : NSOrderedDescending;
}
return (originA.x < originB.x) ? NSOrderedAscending : NSOrderedDescending;
return (sizeA.height < sizeB.height) ? NSOrderedAscending : NSOrderedDescending;
}
return (originA.y < originB.y) ? NSOrderedAscending : NSOrderedDescending;
};
return (originA.x < originB.x) ? NSOrderedAscending : NSOrderedDescending;
}
return (originA.y < originB.y) ? NSOrderedAscending : NSOrderedDescending;
};

if (!currentAccessibilityComparator) {
currentAccessibilityComparator = defaultComparator;
}
});
[elements sortUsingComparator:comparator];

[elements sortUsingComparator:currentAccessibilityComparator];
}

static CGRect ASAccessibilityFrameForNode(ASDisplayNode *node) {
CALayer *layer = node.layer;
return [layer convertRect:node.bounds toLayer:ASFindWindowOfLayer(layer).layer];
}

@interface ASAccessibilityElement : UIAccessibilityElement<ASAccessibilityElementPositioning>
@interface ASAccessibilityElement : UIAccessibilityElement

@property (nonatomic) ASDisplayNode *node;

Expand Down Expand Up @@ -93,7 +106,7 @@ - (CGRect)accessibilityFrame

#pragma mark - _ASDisplayView / UIAccessibilityContainer

@interface ASAccessibilityCustomAction : UIAccessibilityCustomAction<ASAccessibilityElementPositioning>
@interface ASAccessibilityCustomAction : UIAccessibilityCustomAction

@property (nonatomic) ASDisplayNode *node;

Expand Down Expand Up @@ -226,6 +239,16 @@ static void CollectAccessibilityElements(ASDisplayNode *node, NSMutableArray *el
}

for (ASDisplayNode *subnode in node.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;
}
// If a subnode is outside of the view's window, exclude it
CGRect nodeInWindowCoords = [node convertRect:subnode.frame toNode:nil];
if (view.window && !CGRectIntersectsRect(view.window.frame, nodeInWindowCoords)) {
rcancro marked this conversation as resolved.
Show resolved Hide resolved
continue;
}

if (subnode.isAccessibilityElement) {
// An accessiblityElement can either be a UIView or a UIAccessibilityElement
if (subnode.isLayerBacked) {
Expand Down Expand Up @@ -275,7 +298,9 @@ - (NSArray *)accessibilityElements
return @[];
}

if (_accessibilityElements == nil) {
// when items become hidden/visible we have to manually clear the _accessibilityElements in order to get an updated version
// Instead, let's try computing the elements every time and see how badly it affects performance.
if (_accessibilityElements == nil || ASActivateExperimentalFeature(ASExperimentalDoNotCacheAccessibilityElements)) {
_accessibilityElements = [viewNode accessibilityElements];
}
return _accessibilityElements;
Expand Down
154 changes: 154 additions & 0 deletions Tests/ASDisplayViewAccessibilityTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@
#import <XCTest/XCTest.h>

#import <AsyncDisplayKit/_ASDisplayView.h>
#import <AsyncDisplayKit/_ASDisplayViewAccessiblity.h>
#import <AsyncDisplayKit/ASButtonNode.h>
#import <AsyncDisplayKit/ASDisplayNode.h>
#import <AsyncDisplayKit/ASDisplayNode+Beta.h>
#import <AsyncDisplayKit/ASTextNode.h>
#import <AsyncDisplayKit/ASConfiguration.h>
#import <AsyncDisplayKit/ASConfigurationInternal.h>
#import <AsyncDisplayKit/ASViewController.h>
#import <OCMock/OCMock.h>
#import "ASDisplayNodeTestsHelper.h"

extern void SortAccessibilityElements(NSMutableArray *elements);

@interface ASDisplayViewAccessibilityTests : XCTestCase
@end

Expand Down Expand Up @@ -251,4 +255,154 @@ - (void)testThatAccessibilityElementsOverrideWorks {
XCTAssertEqual(elements.firstObject, label);
}

- (void)testHiddenAccessibilityElements {
ASDisplayNode *containerNode = [[ASDisplayNode alloc] init];
containerNode.frame = CGRectMake(0, 0, 100, 200);

ASTextNode *label = [[ASTextNode alloc] init];
label.attributedText = [[NSAttributedString alloc] initWithString:@"test label"];
label.frame = CGRectMake(0, 0, 100, 20);

ASTextNode *hiddenLabel = [[ASTextNode alloc] init];
hiddenLabel.attributedText = [[NSAttributedString alloc] initWithString:@"hidden label"];
hiddenLabel.frame = CGRectMake(0, 24, 100, 20);
hiddenLabel.hidden = YES;

[containerNode addSubnode:label];
[containerNode addSubnode:hiddenLabel];

// force load
__unused UIView *view = containerNode.view;

NSArray *elements = [containerNode accessibilityElements];
XCTAssertTrue(elements.count == 1);
XCTAssertEqual(elements.firstObject, label.view);
}

- (void)testTransparentAccessibilityElements {
ASDisplayNode *containerNode = [[ASDisplayNode alloc] init];
containerNode.frame = CGRectMake(0, 0, 100, 200);

ASTextNode *label = [[ASTextNode alloc] init];
label.attributedText = [[NSAttributedString alloc] initWithString:@"test label"];
label.frame = CGRectMake(0, 0, 100, 20);

ASTextNode *hiddenLabel = [[ASTextNode alloc] init];
hiddenLabel.attributedText = [[NSAttributedString alloc] initWithString:@"hidden label"];
hiddenLabel.frame = CGRectMake(0, 24, 100, 20);
hiddenLabel.alpha = 0.0;

[containerNode addSubnode:label];
[containerNode addSubnode:hiddenLabel];

// force load
__unused UIView *view = containerNode.view;

NSArray *elements = [containerNode accessibilityElements];
XCTAssertTrue(elements.count == 1);
XCTAssertEqual(elements.firstObject, label.view);
}

- (void)testAccessibilityElementsNotInAppWindow {

UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
ASDisplayNode *node = [[ASDisplayNode alloc] init];
node.automaticallyManagesSubnodes = YES;

ASViewController *vc = [[ASViewController alloc] initWithNode:node];
window.rootViewController = vc;
[window makeKeyAndVisible];
[window layoutIfNeeded];

CGSize windowSize = window.frame.size;
ASTextNode *label = [[ASTextNode alloc] init];
label.attributedText = [[NSAttributedString alloc] initWithString:@"on screen"];
label.frame = CGRectMake(0, 0, 100, 20);

ASTextNode *partiallyOnScreenNodeY = [[ASTextNode alloc] init];
partiallyOnScreenNodeY.attributedText = [[NSAttributedString alloc] initWithString:@"partially on screen y"];
partiallyOnScreenNodeY.frame = CGRectMake(0, windowSize.height - 10, 100, 20);

ASTextNode *partiallyOnScreenNodeX = [[ASTextNode alloc] init];
partiallyOnScreenNodeX.attributedText = [[NSAttributedString alloc] initWithString:@"partially on screen x"];
partiallyOnScreenNodeX.frame = CGRectMake(windowSize.width - 10, 100, 100, 20);

ASTextNode *offScreenNodeY = [[ASTextNode alloc] init];
offScreenNodeY.attributedText = [[NSAttributedString alloc] initWithString:@"off screen y"];
offScreenNodeY.frame = CGRectMake(0, windowSize.height + 10, 100, 20);

ASTextNode *offScreenNodeX = [[ASTextNode alloc] init];
offScreenNodeX.attributedText = [[NSAttributedString alloc] initWithString:@"off screen x"];
offScreenNodeX.frame = CGRectMake(windowSize.width + 1, 200, 100, 20);

ASTextNode *offScreenNode = [[ASTextNode alloc] init];
offScreenNode.attributedText = [[NSAttributedString alloc] initWithString:@"off screen"];
offScreenNode.frame = CGRectMake(windowSize.width + 1, windowSize.height + 1, 100, 20);

[node addSubnode:label];
[node addSubnode:partiallyOnScreenNodeY];
[node addSubnode:partiallyOnScreenNodeX];
[node addSubnode:offScreenNodeY];
[node addSubnode:offScreenNodeX];
[node addSubnode:offScreenNode];

NSArray *elements = [node accessibilityElements];
XCTAssertTrue(elements.count == 3);
XCTAssertEqual(elements[0], label.view);
XCTAssertEqual(elements[1], partiallyOnScreenNodeX.view);
XCTAssertEqual(elements[2], partiallyOnScreenNodeY.view);
}

- (void)testAccessibilitySort {
ASDisplayNode *node1 = [[ASDisplayNode alloc] init];
node1.accessibilityFrame = CGRectMake(0, 0, 50, 200);

ASDisplayNode *node2 = [[ASDisplayNode alloc] init];
node2.accessibilityFrame = CGRectMake(0, 0, 100, 200);

ASDisplayNode *node3 = [[ASDisplayNode alloc] init];
node3.accessibilityFrame = CGRectMake(0, 1, 100, 200);

ASDisplayNode *node4 = [[ASDisplayNode alloc] init];
node4.accessibilityFrame = CGRectMake(1, 1, 100, 200);

NSMutableArray *elements = [@[node2, node4, node3, node1] mutableCopy];
SortAccessibilityElements(elements);
XCTAssertEqual(elements[0], node1);
XCTAssertEqual(elements[1], node2);
XCTAssertEqual(elements[2], node3);
XCTAssertEqual(elements[3], node4);
}

- (void)testCustomAccessibilitySort {

// silly custom sorter that puts items with the largest height first.
setUserDefinedAccessibilitySortComparator(^NSComparisonResult(NSObject *a, NSObject *b) {
if (a.accessibilityFrame.size.height == b.accessibilityFrame.size.height) {
return NSOrderedSame;
}
return a.accessibilityFrame.size.height > b.accessibilityFrame.size.height ? NSOrderedAscending : NSOrderedDescending;
});

ASDisplayNode *node1 = [[ASDisplayNode alloc] init];
node1.accessibilityFrame = CGRectMake(0, 0, 50, 300);

ASDisplayNode *node2 = [[ASDisplayNode alloc] init];
node2.accessibilityFrame = CGRectMake(0, 0, 100, 250);

ASDisplayNode *node3 = [[ASDisplayNode alloc] init];
node3.accessibilityFrame = CGRectMake(0, 0, 100, 200);

ASDisplayNode *node4 = [[ASDisplayNode alloc] init];
node4.accessibilityFrame = CGRectMake(0, 0, 100, 150);

NSMutableArray *elements = [@[node2, node4, node3, node1] mutableCopy];
SortAccessibilityElements(elements);
XCTAssertEqual(elements[0], node1);
XCTAssertEqual(elements[1], node2);
XCTAssertEqual(elements[2], node3);
XCTAssertEqual(elements[3], node4);
}


@end