From cfe003238ab8c5686d185f6ce9e0776eeb4bb729 Mon Sep 17 00:00:00 2001 From: Adam Comella Date: Tue, 30 May 2017 04:35:52 -0700 Subject: [PATCH] iOS: Introduce API for making screen reader announcements 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 --- .../AccessibilityInfo.ios.js | 34 ++++++++++++++++--- React/Modules/RCTAccessibilityManager.m | 24 +++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js index f84ff2980..65f7af70d 100644 --- a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js +++ b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js @@ -18,9 +18,11 @@ var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); var AccessibilityManager = NativeModules.AccessibilityManager; var VOICE_OVER_EVENT = 'voiceOverDidChange'; +var ANNOUNCEMENT_DID_FINISH_EVENT = 'announcementDidFinish'; type ChangeEventName = $Enum<{ change: string, + announcementFinished: string }>; var _subscriptions = new Map(); @@ -97,15 +99,30 @@ var AccessibilityInfo = { * - `change`: Fires when the state of the screen reader changes. The argument * to the event handler is a boolean. The boolean is `true` when a screen * reader is enabled and `false` otherwise. + * - `announcementFinished`: iOS-only event. Fires when the screen reader has + * finished making an announcement. The argument to the event handler is a dictionary + * with these keys: + * - `announcement`: The string announced by the screen reader. + * - `success`: A boolean indicating whether the announcement was successfully made. */ addEventListener: function ( eventName: ChangeEventName, handler: Function ): Object { - var listener = RCTDeviceEventEmitter.addListener( - VOICE_OVER_EVENT, - handler - ); + var listener; + + if (eventName === 'change') { + listener = RCTDeviceEventEmitter.addListener( + VOICE_OVER_EVENT, + handler + ); + } else if (eventName === 'announcementFinished') { + listener = RCTDeviceEventEmitter.addListener( + ANNOUNCEMENT_DID_FINISH_EVENT, + handler + ); + } + _subscriptions.set(handler, listener); return { remove: AccessibilityInfo.removeEventListener.bind(null, eventName, handler), @@ -121,6 +138,15 @@ var AccessibilityInfo = { AccessibilityManager.setAccessibilityFocus(reactTag); }, + /** + * iOS-Only. Post a string to be announced by the screen reader. + */ + announceForAccessibility: function( + announcement: string + ): void { + AccessibilityManager.announceForAccessibility(announcement); + }, + /** * Remove an event handler. */ diff --git a/React/Modules/RCTAccessibilityManager.m b/React/Modules/RCTAccessibilityManager.m index 5e415bf12..ee82c70c3 100644 --- a/React/Modules/RCTAccessibilityManager.m +++ b/React/Modules/RCTAccessibilityManager.m @@ -71,6 +71,11 @@ RCT_EXPORT_MODULE() selector:@selector(didReceiveNewVoiceOverStatus:) name:UIAccessibilityVoiceOverStatusChanged object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(accessibilityAnnouncementDidFinish:) + name:UIAccessibilityAnnouncementDidFinishNotification + object:nil]; self.contentSizeCategory = RCTSharedApplication().preferredContentSizeCategory; _isVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning(); @@ -101,6 +106,20 @@ RCT_EXPORT_MODULE() } } +- (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) { @@ -171,6 +190,11 @@ RCT_EXPORT_METHOD(setAccessibilityFocus:(nonnull NSNumber *)reactTag) }); } +RCT_EXPORT_METHOD(announceForAccessibility:(NSString *)announcement) +{ + UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement); +} + RCT_EXPORT_METHOD(getMultiplier:(RCTResponseSenderBlock)callback) { if (callback) {