mirror of
https://github.com/status-im/react-native.git
synced 2025-01-10 09:35:48 +00:00
cfe003238a
Summary: This change introduces some APIs that are useful for making announcements through the screen reader on iOS: - `announceForAccessibility`: The screen reader announces the string that is passed in. - `announcementFinished`: An event that fires when the screen reader has finished making an announcement. You can already solve similar problems with RN Android using the `accessibilityLiveRegion` prop. Live regions are a different feature but they can be used to solve the same problem. This commit does not attempt to add live region support in RN iOS because Apple did not build live region support into iOS. Verified that `announceForAccessibility` causes VoiceOver to announce the string when VoiceOver is enabled. Verified that `announcementFinished` fires with the appropriate data in the event object. Additionally, my team has been using this change in our app. Adam Comella Microsoft Corp. Closes https://github.com/facebook/react-native/pull/14168 Differential Revision: D5137004 Pulled By: javache fbshipit-source-id: b3c10f3dfc716430a16fcc98e1bb6fe52cabd6a5
221 lines
7.8 KiB
Objective-C
221 lines
7.8 KiB
Objective-C
/**
|
|
* Copyright (c) 2015-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 "RCTAccessibilityManager.h"
|
|
|
|
#import "RCTUIManager.h"
|
|
#import "RCTBridge.h"
|
|
#import "RCTConvert.h"
|
|
#import "RCTEventDispatcher.h"
|
|
#import "RCTLog.h"
|
|
|
|
NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification = @"RCTAccessibilityManagerDidUpdateMultiplierNotification";
|
|
|
|
@interface RCTAccessibilityManager ()
|
|
|
|
@property (nonatomic, copy) NSString *contentSizeCategory;
|
|
@property (nonatomic, assign) CGFloat multiplier;
|
|
|
|
@end
|
|
|
|
@implementation RCTAccessibilityManager
|
|
|
|
@synthesize bridge = _bridge;
|
|
@synthesize multipliers = _multipliers;
|
|
|
|
RCT_EXPORT_MODULE()
|
|
|
|
+ (NSDictionary<NSString *, NSString *> *)JSToUIKitMap
|
|
{
|
|
static NSDictionary *map = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
map = @{@"extraSmall": UIContentSizeCategoryExtraSmall,
|
|
@"small": UIContentSizeCategorySmall,
|
|
@"medium": UIContentSizeCategoryMedium,
|
|
@"large": UIContentSizeCategoryLarge,
|
|
@"extraLarge": UIContentSizeCategoryExtraLarge,
|
|
@"extraExtraLarge": UIContentSizeCategoryExtraExtraLarge,
|
|
@"extraExtraExtraLarge": UIContentSizeCategoryExtraExtraExtraLarge,
|
|
@"accessibilityMedium": UIContentSizeCategoryAccessibilityMedium,
|
|
@"accessibilityLarge": UIContentSizeCategoryAccessibilityLarge,
|
|
@"accessibilityExtraLarge": UIContentSizeCategoryAccessibilityExtraLarge,
|
|
@"accessibilityExtraExtraLarge": UIContentSizeCategoryAccessibilityExtraExtraLarge,
|
|
@"accessibilityExtraExtraExtraLarge": UIContentSizeCategoryAccessibilityExtraExtraExtraLarge};
|
|
});
|
|
return map;
|
|
}
|
|
|
|
+ (NSString *)UIKitCategoryFromJSCategory:(NSString *)JSCategory
|
|
{
|
|
return [self JSToUIKitMap][JSCategory];
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
if ((self = [super init])) {
|
|
|
|
// TODO: can this be moved out of the startup path?
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(didReceiveNewContentSizeCategory:)
|
|
name:UIContentSizeCategoryDidChangeNotification
|
|
object:RCTSharedApplication()];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(didReceiveNewVoiceOverStatus:)
|
|
name:UIAccessibilityVoiceOverStatusChanged
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(accessibilityAnnouncementDidFinish:)
|
|
name:UIAccessibilityAnnouncementDidFinishNotification
|
|
object:nil];
|
|
|
|
self.contentSizeCategory = RCTSharedApplication().preferredContentSizeCategory;
|
|
_isVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning();
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
- (void)didReceiveNewContentSizeCategory:(NSNotification *)note
|
|
{
|
|
self.contentSizeCategory = note.userInfo[UIContentSizeCategoryNewValueKey];
|
|
}
|
|
|
|
- (void)didReceiveNewVoiceOverStatus:(__unused NSNotification *)notification
|
|
{
|
|
BOOL newIsVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning();
|
|
if (_isVoiceOverEnabled != newIsVoiceOverEnabled) {
|
|
_isVoiceOverEnabled = newIsVoiceOverEnabled;
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
[_bridge.eventDispatcher sendDeviceEventWithName:@"voiceOverDidChange"
|
|
body:@(_isVoiceOverEnabled)];
|
|
#pragma clang diagnostic pop
|
|
}
|
|
}
|
|
|
|
- (void)accessibilityAnnouncementDidFinish:(__unused NSNotification *)notification
|
|
{
|
|
NSDictionary *userInfo = notification.userInfo;
|
|
// Response dictionary to populate the event with.
|
|
NSDictionary *response = @{@"announcement": userInfo[UIAccessibilityAnnouncementKeyStringValue],
|
|
@"success": userInfo[UIAccessibilityAnnouncementKeyWasSuccessful]};
|
|
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
[_bridge.eventDispatcher sendDeviceEventWithName:@"announcementDidFinish"
|
|
body:response];
|
|
#pragma clang diagnostic pop
|
|
}
|
|
|
|
- (void)setContentSizeCategory:(NSString *)contentSizeCategory
|
|
{
|
|
if (_contentSizeCategory != contentSizeCategory) {
|
|
_contentSizeCategory = [contentSizeCategory copy];
|
|
[self invalidateMultiplier];
|
|
}
|
|
}
|
|
|
|
- (void)invalidateMultiplier
|
|
{
|
|
self.multiplier = [self multiplierForContentSizeCategory:_contentSizeCategory];
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:RCTAccessibilityManagerDidUpdateMultiplierNotification object:self];
|
|
}
|
|
|
|
- (CGFloat)multiplierForContentSizeCategory:(NSString *)category
|
|
{
|
|
NSNumber *m = self.multipliers[category];
|
|
if (m.doubleValue <= 0.0) {
|
|
RCTLogError(@"Can't determinte multiplier for category %@. Using 1.0.", category);
|
|
m = @1.0;
|
|
}
|
|
return m.doubleValue;
|
|
}
|
|
|
|
- (void)setMultipliers:(NSDictionary<NSString *, NSNumber *> *)multipliers
|
|
{
|
|
if (_multipliers != multipliers) {
|
|
_multipliers = [multipliers copy];
|
|
[self invalidateMultiplier];
|
|
}
|
|
}
|
|
|
|
- (NSDictionary<NSString *, NSNumber *> *)multipliers
|
|
{
|
|
if (_multipliers == nil) {
|
|
_multipliers = @{UIContentSizeCategoryExtraSmall: @0.823,
|
|
UIContentSizeCategorySmall: @0.882,
|
|
UIContentSizeCategoryMedium: @0.941,
|
|
UIContentSizeCategoryLarge: @1.0,
|
|
UIContentSizeCategoryExtraLarge: @1.118,
|
|
UIContentSizeCategoryExtraExtraLarge: @1.235,
|
|
UIContentSizeCategoryExtraExtraExtraLarge: @1.353,
|
|
UIContentSizeCategoryAccessibilityMedium: @1.786,
|
|
UIContentSizeCategoryAccessibilityLarge: @2.143,
|
|
UIContentSizeCategoryAccessibilityExtraLarge: @2.643,
|
|
UIContentSizeCategoryAccessibilityExtraExtraLarge: @3.143,
|
|
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge: @3.571};
|
|
}
|
|
return _multipliers;
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(setAccessibilityContentSizeMultipliers:(NSDictionary *)JSMultipliers)
|
|
{
|
|
NSMutableDictionary<NSString *, NSNumber *> *multipliers = [NSMutableDictionary new];
|
|
for (NSString *__nonnull JSCategory in JSMultipliers) {
|
|
NSNumber *m = [RCTConvert NSNumber:JSMultipliers[JSCategory]];
|
|
NSString *UIKitCategory = [[self class] UIKitCategoryFromJSCategory:JSCategory];
|
|
multipliers[UIKitCategory] = m;
|
|
}
|
|
self.multipliers = multipliers;
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(setAccessibilityFocus:(nonnull NSNumber *)reactTag)
|
|
{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
UIView *view = [self.bridge.uiManager viewForReactTag:reactTag];
|
|
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, view);
|
|
});
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(announceForAccessibility:(NSString *)announcement)
|
|
{
|
|
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement);
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(getMultiplier:(RCTResponseSenderBlock)callback)
|
|
{
|
|
if (callback) {
|
|
callback(@[ @(self.multiplier) ]);
|
|
}
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(getCurrentVoiceOverState:(RCTResponseSenderBlock)callback
|
|
error:(__unused RCTResponseSenderBlock)error)
|
|
{
|
|
callback(@[@(_isVoiceOverEnabled)]);
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation RCTBridge (RCTAccessibilityManager)
|
|
|
|
- (RCTAccessibilityManager *)accessibilityManager
|
|
{
|
|
return [self moduleForClass:[RCTAccessibilityManager class]];
|
|
}
|
|
|
|
@end
|