Skip to content

Commit cfe0032

Browse files
Adam Comellafacebook-github-bot
Adam Comella
authored andcommitted
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 #14168 Differential Revision: D5137004 Pulled By: javache fbshipit-source-id: b3c10f3dfc716430a16fcc98e1bb6fe52cabd6a5
1 parent f0e4a6c commit cfe0032

File tree

2 files changed

+54
-4
lines changed

2 files changed

+54
-4
lines changed

Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js

+30-4
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
1818
var AccessibilityManager = NativeModules.AccessibilityManager;
1919

2020
var VOICE_OVER_EVENT = 'voiceOverDidChange';
21+
var ANNOUNCEMENT_DID_FINISH_EVENT = 'announcementDidFinish';
2122

2223
type ChangeEventName = $Enum<{
2324
change: string,
25+
announcementFinished: string
2426
}>;
2527

2628
var _subscriptions = new Map();
@@ -97,15 +99,30 @@ var AccessibilityInfo = {
9799
* - `change`: Fires when the state of the screen reader changes. The argument
98100
* to the event handler is a boolean. The boolean is `true` when a screen
99101
* reader is enabled and `false` otherwise.
102+
* - `announcementFinished`: iOS-only event. Fires when the screen reader has
103+
* finished making an announcement. The argument to the event handler is a dictionary
104+
* with these keys:
105+
* - `announcement`: The string announced by the screen reader.
106+
* - `success`: A boolean indicating whether the announcement was successfully made.
100107
*/
101108
addEventListener: function (
102109
eventName: ChangeEventName,
103110
handler: Function
104111
): Object {
105-
var listener = RCTDeviceEventEmitter.addListener(
106-
VOICE_OVER_EVENT,
107-
handler
108-
);
112+
var listener;
113+
114+
if (eventName === 'change') {
115+
listener = RCTDeviceEventEmitter.addListener(
116+
VOICE_OVER_EVENT,
117+
handler
118+
);
119+
} else if (eventName === 'announcementFinished') {
120+
listener = RCTDeviceEventEmitter.addListener(
121+
ANNOUNCEMENT_DID_FINISH_EVENT,
122+
handler
123+
);
124+
}
125+
109126
_subscriptions.set(handler, listener);
110127
return {
111128
remove: AccessibilityInfo.removeEventListener.bind(null, eventName, handler),
@@ -121,6 +138,15 @@ var AccessibilityInfo = {
121138
AccessibilityManager.setAccessibilityFocus(reactTag);
122139
},
123140

141+
/**
142+
* iOS-Only. Post a string to be announced by the screen reader.
143+
*/
144+
announceForAccessibility: function(
145+
announcement: string
146+
): void {
147+
AccessibilityManager.announceForAccessibility(announcement);
148+
},
149+
124150
/**
125151
* Remove an event handler.
126152
*/

React/Modules/RCTAccessibilityManager.m

+24
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ - (instancetype)init
7171
selector:@selector(didReceiveNewVoiceOverStatus:)
7272
name:UIAccessibilityVoiceOverStatusChanged
7373
object:nil];
74+
75+
[[NSNotificationCenter defaultCenter] addObserver:self
76+
selector:@selector(accessibilityAnnouncementDidFinish:)
77+
name:UIAccessibilityAnnouncementDidFinishNotification
78+
object:nil];
7479

7580
self.contentSizeCategory = RCTSharedApplication().preferredContentSizeCategory;
7681
_isVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning();
@@ -101,6 +106,20 @@ - (void)didReceiveNewVoiceOverStatus:(__unused NSNotification *)notification
101106
}
102107
}
103108

109+
- (void)accessibilityAnnouncementDidFinish:(__unused NSNotification *)notification
110+
{
111+
NSDictionary *userInfo = notification.userInfo;
112+
// Response dictionary to populate the event with.
113+
NSDictionary *response = @{@"announcement": userInfo[UIAccessibilityAnnouncementKeyStringValue],
114+
@"success": userInfo[UIAccessibilityAnnouncementKeyWasSuccessful]};
115+
116+
#pragma clang diagnostic push
117+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
118+
[_bridge.eventDispatcher sendDeviceEventWithName:@"announcementDidFinish"
119+
body:response];
120+
#pragma clang diagnostic pop
121+
}
122+
104123
- (void)setContentSizeCategory:(NSString *)contentSizeCategory
105124
{
106125
if (_contentSizeCategory != contentSizeCategory) {
@@ -171,6 +190,11 @@ - (void)setMultipliers:(NSDictionary<NSString *, NSNumber *> *)multipliers
171190
});
172191
}
173192

193+
RCT_EXPORT_METHOD(announceForAccessibility:(NSString *)announcement)
194+
{
195+
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement);
196+
}
197+
174198
RCT_EXPORT_METHOD(getMultiplier:(RCTResponseSenderBlock)callback)
175199
{
176200
if (callback) {

0 commit comments

Comments
 (0)