Skip to content

Commit 4d13579

Browse files
peterc1731facebook-github-bot
authored andcommitted
feat: support queuing accessibility announcements on ios (#32637)
Summary: The current implementation of `AccessibilityInfo.announceForAccessibility` will immediately interrupt any existing in progress speech with the announcement. Sometimes this is desirable behaviour, but often you will want to wait until existing speech is finished before reading the new announcement. This change gives us that option. My personal use case for this feature is a custom text input. When typing on iOS with voiceover enabled, each character is read out after being selected. I wanted to add some additional information after each character to help with the context of what has changed in the input, but I didn't want to override the reading of the character itself. This feature is supported natively on iOS by constructing an `NSAttributedString` with the property [`accessibilitySpeechQueueAnnouncement`](https://developer.apple.com/documentation/foundation/nsattributedstring/key/2865770-accessibilityspeechqueueannounce), so this change just adds an extra parameter to `AccessibilityInfo.announceForAccessibility` which controls the value of that property on the native side. Adding this as an extra optional parameter with false as the default ensures that existing uses of the function won't be affected. Unfortunately, this feature doesn't appear to be supported on Android, so the new second property will be iOS only. ## Changelog [iOS] [Added] - add new argument to announceForAccessibility to allow queueing on iOS Pull Request resolved: #32637 Test Plan: I've updated the `announceForAccessibility` section in RNTester with multiple buttons to demonstrate the difference between `queue: false` (default) and `queue: true` and show they work as intended. Here's the expectation for each button: - "Announce for Accessibility Immediately": on press, should start reading the button label, then be interrupted by the announcement - "Announce for Accessibility Queued": on press, should read the button label then read the announcement afterwards - "Announce for Accessibility Queue Multiple": on press, should read the button label, then read three announcements sequentially, no interruptions You can see the realisation of those expectations in the following video recorded on an iPhone 12 running iOS 15.0.2: https://user-images.githubusercontent.com/14826539/142770536-d57bfd69-eba5-444d-9c89-4bf4851ea062.mov I've also tested the same way on an iPhone 8 running iOS 13.4 and it works exactly the same. Reviewed By: yungsters Differential Revision: D32637989 Pulled By: philIip fbshipit-source-id: 3e90add523f11eb0eb34ea623211249263f257e2
1 parent 2bb91ae commit 4d13579

File tree

5 files changed

+110
-5
lines changed

5 files changed

+110
-5
lines changed

Libraries/Components/AccessibilityInfo/AccessibilityInfo.js

+24
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,30 @@ const AccessibilityInfo = {
341341
}
342342
},
343343

344+
/**
345+
* Post a string to be announced by the screen reader.
346+
* - `announcement`: The string announced by the screen reader.
347+
* - `options`: An object that configures the reading options.
348+
* - `queue`: The announcement will be queued behind existing announcements. iOS only.
349+
*/
350+
announceForAccessibilityWithOptions(
351+
announcement: string,
352+
options: {queue?: boolean},
353+
): void {
354+
if (Platform.OS === 'android') {
355+
NativeAccessibilityInfoAndroid?.announceForAccessibility(announcement);
356+
} else {
357+
if (NativeAccessibilityManagerIOS?.announceForAccessibilityWithOptions) {
358+
NativeAccessibilityManagerIOS?.announceForAccessibilityWithOptions(
359+
announcement,
360+
options,
361+
);
362+
} else {
363+
NativeAccessibilityManagerIOS?.announceForAccessibility(announcement);
364+
}
365+
}
366+
},
367+
344368
/**
345369
* @deprecated Use `remove` on the EventSubscription from `addEventListener`.
346370
*/

Libraries/Components/AccessibilityInfo/NativeAccessibilityManager.js

+4
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ export interface Spec extends TurboModule {
5252
|}) => void;
5353
+setAccessibilityFocus: (reactTag: number) => void;
5454
+announceForAccessibility: (announcement: string) => void;
55+
+announceForAccessibilityWithOptions?: (
56+
announcement: string,
57+
options: {queue?: boolean},
58+
) => void;
5559
}
5660

5761
export default (TurboModuleRegistry.get<Spec>('AccessibilityManager'): ?Spec);

React/CoreModules/RCTAccessibilityManager.mm

+22
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,28 @@ static void setMultipliers(
301301
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement);
302302
}
303303

304+
RCT_EXPORT_METHOD(announceForAccessibilityWithOptions
305+
: (NSString *)announcement options
306+
: (JS::NativeAccessibilityManager::SpecAnnounceForAccessibilityWithOptionsOptions &)options)
307+
{
308+
if (@available(iOS 11.0, *)) {
309+
NSMutableDictionary<NSString *, NSNumber *> *attrsDictionary = [NSMutableDictionary new];
310+
if (options.queue()) {
311+
attrsDictionary[UIAccessibilitySpeechAttributeQueueAnnouncement] = @(*(options.queue()) ? YES : NO);
312+
}
313+
314+
if (attrsDictionary.count > 0) {
315+
NSAttributedString *announcementWithAttrs = [[NSAttributedString alloc] initWithString:announcement
316+
attributes:attrsDictionary];
317+
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementWithAttrs);
318+
} else {
319+
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement);
320+
}
321+
} else {
322+
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement);
323+
}
324+
}
325+
304326
RCT_EXPORT_METHOD(getMultiplier : (RCTResponseSenderBlock)callback)
305327
{
306328
if (callback) {

packages/rn-tester/Podfile.lock

+3-3
Original file line numberDiff line numberDiff line change
@@ -881,7 +881,7 @@ SPEC CHECKSUMS:
881881
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
882882
DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662
883883
FBLazyVector: b81a2b70c72d8b0aefb652cea22c11e9ffd02949
884-
FBReactNativeSpec: 755b7fee1b08aefd74fb2fa9f7312b253719d536
884+
FBReactNativeSpec: 37e065c0cfc5da966014bf62b50edb066d8206cd
885885
Flipper: 30e8eeeed6abdc98edaf32af0cda2f198be4b733
886886
Flipper-Boost-iOSX: fd1e2b8cbef7e662a122412d7ac5f5bea715403c
887887
Flipper-DoubleConversion: 57ffbe81ef95306cc9e69c4aa3aeeeeb58a6a28c
@@ -923,10 +923,10 @@ SPEC CHECKSUMS:
923923
React-RCTTest: 12bbd7fc2e72bd9920dc7286c5b8ef96639582b6
924924
React-RCTText: e9146b2c0550a83d1335bfe2553760070a2d75c7
925925
React-RCTVibration: 50be9c390f2da76045ef0dfdefa18b9cf9f35cfa
926-
React-rncore: d09af3a25cbff0b484776785676c28f3729e07f5
926+
React-rncore: c57d93f56e2d385bdbda34eae2d20d4d3c0c8b4a
927927
React-runtimeexecutor: 4b0c6eb341c7d3ceb5e2385cb0fdb9bf701024f3
928928
ReactCommon: 7a2714d1128f965392b6f99a8b390e3aa38c9569
929-
ScreenshotManager: e8a3fc9b2e24b81127b36cb4ebe0eed65090c949
929+
ScreenshotManager: 9f69049876d8aafafa13a1a635baa8f7e168eee4
930930
Yoga: c0d06f5380d34e939f55420669a60fe08b79bd75
931931
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
932932

packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js

+57-2
Original file line numberDiff line numberDiff line change
@@ -864,10 +864,65 @@ class FakeSliderExample extends React.Component<{}, FakeSliderExampleState> {
864864

865865
class AnnounceForAccessibility extends React.Component<{}> {
866866
_handleOnPress = () =>
867-
AccessibilityInfo.announceForAccessibility('Announcement Test');
867+
setTimeout(
868+
() => AccessibilityInfo.announceForAccessibility('Announcement Test'),
869+
1000,
870+
);
871+
872+
_handleOnPressQueued = () =>
873+
setTimeout(
874+
() =>
875+
AccessibilityInfo.announceForAccessibilityWithOptions(
876+
'Queued Announcement Test',
877+
{queue: true},
878+
),
879+
1000,
880+
);
881+
882+
_handleOnPressQueueMultiple = () => {
883+
setTimeout(
884+
() =>
885+
AccessibilityInfo.announceForAccessibilityWithOptions(
886+
'First Queued Announcement Test',
887+
{queue: true},
888+
),
889+
1000,
890+
);
891+
setTimeout(
892+
() =>
893+
AccessibilityInfo.announceForAccessibilityWithOptions(
894+
'Second Queued Announcement Test',
895+
{queue: true},
896+
),
897+
1100,
898+
);
899+
setTimeout(
900+
() =>
901+
AccessibilityInfo.announceForAccessibilityWithOptions(
902+
'Third Queued Announcement Test',
903+
{queue: true},
904+
),
905+
1200,
906+
);
907+
};
868908

869909
render(): React.Node {
870-
return (
910+
return Platform.OS === 'ios' ? (
911+
<View>
912+
<Button
913+
onPress={this._handleOnPress}
914+
title="Announce for Accessibility Immediately"
915+
/>
916+
<Button
917+
onPress={this._handleOnPressQueued}
918+
title="Announce for Accessibility Queued"
919+
/>
920+
<Button
921+
onPress={this._handleOnPressQueueMultiple}
922+
title="Announce for Accessibility Queue Multiple"
923+
/>
924+
</View>
925+
) : (
871926
<View>
872927
<Button
873928
onPress={this._handleOnPress}

0 commit comments

Comments
 (0)