ComponentKit/Accessibility/CKAccessibilityAggregation.mm (231 lines of code) (raw):
/*
* 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 root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "CKAccessibilityAggregation.h"
#import <RenderCore/RCAssert.h>
#import <ComponentKit/CKComponentContext.h>
#import <ComponentKit/CKCompositeComponent.h>
#import <RenderCore/CKInternalHelpers.h>
#import <stack>
#import "CKComponentAccessibility.h"
#import "CKComponentInternal.h"
@interface CKAccessibilityAggregationContext : NSObject
@end
@interface CKAccessibilityAggregatorComponent : CKCompositeComponent
+ (instancetype)newWithComponent:(CKComponent *)component aggregatedAttributes:(CKAccessibilityAggregatedAttributes)attributes;
@end
@interface CKAccessibilityAggregatorElement : UIAccessibilityElement
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithComponent:(CKAccessibilityAggregatorComponent *)component NS_DESIGNATED_INITIALIZER;
@end
@interface CKAccessibilityAggregationCache : NSObject
@property (nonatomic, assign) UIAccessibilityTraits traits;
@property (nonatomic, copy) NSString* label;
@property (nonatomic, copy) NSString* hint;
@property (nonatomic, copy) NSString* value;
@property (nonatomic, copy) NSPointerArray *actions;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithLabel:(NSString *)label
hint:(NSString *)hint
value:(NSString *)value
traits:(UIAccessibilityTraits)traits
actions:(NSPointerArray *)actions NS_DESIGNATED_INITIALIZER;
@end
@implementation CKAccessibilityAggregatorComponent {
CKComponent *_childComponent;
CKAccessibilityAggregatedAttributes _accessibilityAttributes;
CKAccessibilityAggregationCache *_accessibilityAttributesCache;
CKAccessibilityAggregatorElement *_accessibilityElement;
}
+ (instancetype)newWithComponent:(CKComponent *)component aggregatedAttributes:(CKAccessibilityAggregatedAttributes)attributes
{
RCAssertWithCategory(attributes != CKAccessibilityAggregatedAttributeNone,
[component className],
@"A CKAccessibilityAggregatorComponent should not be allocated without at least one aggregated accessibility attribute");
const auto c = [super newWithComponent:component];
if (c) {
c->_childComponent = component;
c->_accessibilityAttributes = attributes;
c->_accessibilityAttributesCache = nil;
}
return c;
}
- (CK::Component::MountResult)mountInContext:(const CK::Component::MountContext &)context
layout:(const RCLayout &)layout
supercomponent:(CKComponent *)supercomponent
{
if (CK::Component::Accessibility::IsAccessibilityEnabled()) {
// Resetting the cached attributes on mount in order to refresh them everytime we remount the component
// and avoid having stale values.
_accessibilityAttributesCache = nil;
_accessibilityElement = [[CKAccessibilityAggregatorElement alloc] initWithComponent:self];
}
return [super mountInContext:context layout:layout supercomponent:supercomponent];
}
- (CKAccessibilityAggregationCache *)cachedAccessibilityAttributes
{
if (!_accessibilityAttributesCache) {
_accessibilityAttributesCache = populateAccessibilityAttributesCache(_accessibilityAttributes, self);
}
return _accessibilityAttributesCache;
}
- (NSArray *)accessibilityElements
{
if (_accessibilityElement) {
return @[_accessibilityElement];
}
return nil;
}
- (NSString *)accessibilityLabel
{
return [self cachedAccessibilityAttributes].label;
}
- (UIAccessibilityTraits)accessibilityTraits
{
return [self cachedAccessibilityAttributes].traits;
}
- (NSString *)accessibilityValue
{
return [self cachedAccessibilityAttributes].value;
}
- (NSString *)accessibilityHint
{
return [self cachedAccessibilityAttributes].hint;
}
- (BOOL)accessibilityActivate
{
NSPointerArray *const actions = [self cachedAccessibilityAttributes].actions;
if ([actions count] == 1) {
return [(NSObject *)[actions pointerAtIndex:0] accessibilityActivate];
} else {
RCAssert(actions.count == 0, @"Multiple actions not supported, yet");
return NO;
}
}
static CKAccessibilityAggregationCache *populateAccessibilityAttributesCache(CKAccessibilityAggregatedAttributes accessibilityAttributes, CKComponent *initialObj)
{
// This value of the context will be pushed on the Thread Local Storage of the current thread, that is the main thread when this
// function is invoked.
// The context pushed here will be read by the CKComponent class in order to determine what are the accessibilityElements,
// given that the logic is different based on the fact that a component is a descendant of an aggregating component or not
const CKComponentContext<CKAccessibilityAggregationContext> aggregationContext([CKAccessibilityAggregationContext new]);
std::stack<NSObject *> stack;
// Let's take advantage of nil messaging
NSMutableString *const aggregatedLabel = ((accessibilityAttributes & CKAccessibilityAggregatedAttributeLabel) > 0) ? [NSMutableString new] : nil;
NSMutableString *const aggregatedValue = ((accessibilityAttributes & CKAccessibilityAggregatedAttributeValue) > 0) ? [NSMutableString new] : nil;
NSMutableString *const aggregatedHint = ((accessibilityAttributes & CKAccessibilityAggregatedAttributeHint) > 0) ? [NSMutableString new] : nil;
NSPointerArray *const aggregatedActions = ((accessibilityAttributes & CKAccessibilityAggregatedAttributeActions) > 0) ? [NSPointerArray weakObjectsPointerArray] : nil;
UIAccessibilityTraits aggregatedTraits = UIAccessibilityTraitNone;
NSObject *current = initialObj;
do {
if (current != initialObj) {
if ([current isAccessibilityElement] && ![current accessibilityElementsHidden]) {
const auto accessibilityLabel = [current accessibilityLabel];
if (aggregatedLabel && [accessibilityLabel length]) {
[aggregatedLabel appendFormat:@"%@\n", accessibilityLabel];
}
aggregatedTraits |= [current accessibilityTraits];
const auto accessibilityValue = [current accessibilityValue];
if (aggregatedValue && [accessibilityValue length]) {
[aggregatedValue appendFormat:@"%@", accessibilityValue];
}
const auto accessibilityHint = [current accessibilityHint];
if (aggregatedHint && [accessibilityHint length]) {
[aggregatedHint appendFormat:@"%@", accessibilityHint];
}
if (!stack.empty()) {
current = stack.top();
stack.pop();
} else {
current = nil;
}
continue;
} else if (CKSubclassOverridesInstanceMethod(class_getSuperclass([current class]), [current class], @selector(accessibilityActivate))
&& ![current accessibilityElementsHidden]) {
// Add a UIView (not a subclass) accessibilityAction only if the view has isAccessibleElement overridden
if ([current isMemberOfClass:[UIView class]]) {
if ([current isAccessibilityElement]) {
[aggregatedActions addPointer:(__bridge void *_Nullable)current];
}
} else {
// This object can be activated, e.g. it can be triggered by a double tap. We should save it
[aggregatedActions addPointer:(__bridge void *_Nullable)current];
}
}
}
const auto axChildren = [current respondsToSelector:@selector(accessibilityChildren)] ? [(id)current accessibilityChildren] : nil;
if ([axChildren count] != 0) {
// In CKAccessibilityAggregatorComponent we have to cycle over the child components,
// because if using the UIKit accessibilityElements there will be an infinite loop
for (NSObject *o in [axChildren reverseObjectEnumerator]) {
stack.push(o);
}
} else {
for (NSObject *o in [[current accessibilityElements] reverseObjectEnumerator]) {
stack.push(o);
}
}
if (!stack.empty()) {
current = stack.top();
stack.pop();
} else {
current = nil;
}
} while (current);
return [[CKAccessibilityAggregationCache alloc]
initWithLabel:aggregatedLabel
hint:aggregatedHint
value:aggregatedValue
traits:aggregatedTraits
actions:aggregatedActions];
}
@end
@implementation CKAccessibilityAggregationCache
- (instancetype)initWithLabel:(NSString *)label
hint:(NSString *)hint
value:(NSString *)value
traits:(UIAccessibilityTraits)traits
actions:(NSPointerArray *)actions
{
if (self = [super init]) {
_label = [label copy];
_value = [value copy];
_hint = [hint copy];
_traits = traits;
_actions = [actions copy];
}
return self;
}
@end
@implementation CKAccessibilityAggregatorElement {
__weak CKAccessibilityAggregatorComponent *_component;
}
- (instancetype)initWithComponent:(CKAccessibilityAggregatorComponent *)component
{
if (self = [super initWithAccessibilityContainer:component]) {
_component = component;
self.isAccessibilityElement = YES;
}
return self;
}
- (NSString *)accessibilityLabel
{
return [_component accessibilityLabel];
}
- (NSString *)accessibilityValue
{
return [_component accessibilityValue];
}
- (NSString *)accessibilityHint
{
return [_component accessibilityHint];
}
- (BOOL)accessibilityActivate
{
return [_component accessibilityActivate];
}
- (UIAccessibilityTraits)accessibilityTraits
{
return [_component accessibilityTraits];
}
- (CGRect)accessibilityFrame
{
return [_component accessibilityFrame];
}
@end
CKComponent *CKComponentWithAccessibilityAggregationWrapper(CKComponent *component, const CKAccessibilityAggregatedAttributes attributes)
{
return [CKAccessibilityAggregatorComponent newWithComponent:component aggregatedAttributes:attributes];
}
@implementation CKAccessibilityAggregationContext
@end
BOOL CKAccessibilityAggregationIsActive()
{
return CKComponentContext<CKAccessibilityAggregationContext>::get() != nil;
}