Skip to content

Commit

Permalink
feat: support queuing accessibility announcements on ios (#32637)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
peterc1731 authored and facebook-github-bot committed Dec 2, 2021
1 parent 2bb91ae commit 4d13579
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 5 deletions.
24 changes: 24 additions & 0 deletions Libraries/Components/AccessibilityInfo/AccessibilityInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,30 @@ const AccessibilityInfo = {
}
},
/**
* Post a string to be announced by the screen reader.
* - `announcement`: The string announced by the screen reader.
* - `options`: An object that configures the reading options.
* - `queue`: The announcement will be queued behind existing announcements. iOS only.
*/
announceForAccessibilityWithOptions(
announcement: string,
options: {queue?: boolean},
): void {
if (Platform.OS === 'android') {
NativeAccessibilityInfoAndroid?.announceForAccessibility(announcement);
} else {
if (NativeAccessibilityManagerIOS?.announceForAccessibilityWithOptions) {
NativeAccessibilityManagerIOS?.announceForAccessibilityWithOptions(
announcement,
options,
);
} else {
NativeAccessibilityManagerIOS?.announceForAccessibility(announcement);
}
}
},
/**
* @deprecated Use `remove` on the EventSubscription from `addEventListener`.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export interface Spec extends TurboModule {
|}) => void;
+setAccessibilityFocus: (reactTag: number) => void;
+announceForAccessibility: (announcement: string) => void;
+announceForAccessibilityWithOptions?: (
announcement: string,
options: {queue?: boolean},
) => void;
}

export default (TurboModuleRegistry.get<Spec>('AccessibilityManager'): ?Spec);
22 changes: 22 additions & 0 deletions React/CoreModules/RCTAccessibilityManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,28 @@ static void setMultipliers(
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement);
}

RCT_EXPORT_METHOD(announceForAccessibilityWithOptions
: (NSString *)announcement options
: (JS::NativeAccessibilityManager::SpecAnnounceForAccessibilityWithOptionsOptions &)options)
{
if (@available(iOS 11.0, *)) {
NSMutableDictionary<NSString *, NSNumber *> *attrsDictionary = [NSMutableDictionary new];
if (options.queue()) {
attrsDictionary[UIAccessibilitySpeechAttributeQueueAnnouncement] = @(*(options.queue()) ? YES : NO);
}

if (attrsDictionary.count > 0) {
NSAttributedString *announcementWithAttrs = [[NSAttributedString alloc] initWithString:announcement
attributes:attrsDictionary];
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementWithAttrs);
} else {
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement);
}
} else {
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement);
}
}

RCT_EXPORT_METHOD(getMultiplier : (RCTResponseSenderBlock)callback)
{
if (callback) {
Expand Down
6 changes: 3 additions & 3 deletions packages/rn-tester/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,7 @@ SPEC CHECKSUMS:
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662
FBLazyVector: b81a2b70c72d8b0aefb652cea22c11e9ffd02949
FBReactNativeSpec: 755b7fee1b08aefd74fb2fa9f7312b253719d536
FBReactNativeSpec: 37e065c0cfc5da966014bf62b50edb066d8206cd
Flipper: 30e8eeeed6abdc98edaf32af0cda2f198be4b733
Flipper-Boost-iOSX: fd1e2b8cbef7e662a122412d7ac5f5bea715403c
Flipper-DoubleConversion: 57ffbe81ef95306cc9e69c4aa3aeeeeb58a6a28c
Expand Down Expand Up @@ -923,10 +923,10 @@ SPEC CHECKSUMS:
React-RCTTest: 12bbd7fc2e72bd9920dc7286c5b8ef96639582b6
React-RCTText: e9146b2c0550a83d1335bfe2553760070a2d75c7
React-RCTVibration: 50be9c390f2da76045ef0dfdefa18b9cf9f35cfa
React-rncore: d09af3a25cbff0b484776785676c28f3729e07f5
React-rncore: c57d93f56e2d385bdbda34eae2d20d4d3c0c8b4a
React-runtimeexecutor: 4b0c6eb341c7d3ceb5e2385cb0fdb9bf701024f3
ReactCommon: 7a2714d1128f965392b6f99a8b390e3aa38c9569
ScreenshotManager: e8a3fc9b2e24b81127b36cb4ebe0eed65090c949
ScreenshotManager: 9f69049876d8aafafa13a1a635baa8f7e168eee4
Yoga: c0d06f5380d34e939f55420669a60fe08b79bd75
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -864,10 +864,65 @@ class FakeSliderExample extends React.Component<{}, FakeSliderExampleState> {

class AnnounceForAccessibility extends React.Component<{}> {
_handleOnPress = () =>
AccessibilityInfo.announceForAccessibility('Announcement Test');
setTimeout(
() => AccessibilityInfo.announceForAccessibility('Announcement Test'),
1000,
);

_handleOnPressQueued = () =>
setTimeout(
() =>
AccessibilityInfo.announceForAccessibilityWithOptions(
'Queued Announcement Test',
{queue: true},
),
1000,
);

_handleOnPressQueueMultiple = () => {
setTimeout(
() =>
AccessibilityInfo.announceForAccessibilityWithOptions(
'First Queued Announcement Test',
{queue: true},
),
1000,
);
setTimeout(
() =>
AccessibilityInfo.announceForAccessibilityWithOptions(
'Second Queued Announcement Test',
{queue: true},
),
1100,
);
setTimeout(
() =>
AccessibilityInfo.announceForAccessibilityWithOptions(
'Third Queued Announcement Test',
{queue: true},
),
1200,
);
};

render(): React.Node {
return (
return Platform.OS === 'ios' ? (
<View>
<Button
onPress={this._handleOnPress}
title="Announce for Accessibility Immediately"
/>
<Button
onPress={this._handleOnPressQueued}
title="Announce for Accessibility Queued"
/>
<Button
onPress={this._handleOnPressQueueMultiple}
title="Announce for Accessibility Queue Multiple"
/>
</View>
) : (
<View>
<Button
onPress={this._handleOnPress}
Expand Down

0 comments on commit 4d13579

Please sign in to comment.