Skip to content

Commit 14b4668

Browse files
xuelgongfacebook-github-bot
authored andcommitted
Extended Accessibility Actions Support (#24695)
Summary: This is a reconstitution of #24190. It extends accessibility actions to include both a name and user facing label. These extensions support both standard and custom actions. We've also added actions support on Android, and added examples to RNTester showing how both standard and custom accessibility actions are used. ## Changelog [general] [changed] - Enhanced accessibility actions support Pull Request resolved: #24695 Differential Revision: D15391408 Pulled By: cpojer fbshipit-source-id: 5ed48004d46d9887da53baea7fdcd0e7e15c5739
1 parent 933e65e commit 14b4668

File tree

13 files changed

+504
-281
lines changed

13 files changed

+504
-281
lines changed

Libraries/Components/View/ViewAccessibility.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
'use strict';
1212

13+
import type {SyntheticEvent} from 'CoreEventTypes';
14+
1315
// This must be kept in sync with the AccessibilityRolesMask in RCTViewManager.m
1416
export type AccessibilityRole =
1517
| 'none'
@@ -51,3 +53,16 @@ export type AccessibilityStates = $ReadOnlyArray<
5153
| 'collapsed'
5254
| 'hasPopup',
5355
>;
56+
57+
// the info associated with an accessibility action
58+
export type AccessibilityActionInfo = $ReadOnly<{
59+
name: string,
60+
label?: string,
61+
}>;
62+
63+
// The info included in the event sent to onAccessibilityAction
64+
export type AccessibilityActionEvent = SyntheticEvent<
65+
$ReadOnly<{
66+
actionName: string,
67+
}>,
68+
>;

Libraries/Components/View/ViewPropTypes.js

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType';
1515
import type {Node} from 'react';
1616
import type {ViewStyleProp} from '../../StyleSheet/StyleSheet';
1717
import type {TVViewProps} from '../AppleTV/TVViewPropTypes';
18-
import type {AccessibilityRole, AccessibilityStates} from './ViewAccessibility';
18+
import type {
19+
AccessibilityRole,
20+
AccessibilityStates,
21+
AccessibilityActionEvent,
22+
AccessibilityActionInfo,
23+
} from './ViewAccessibility';
1924

2025
export type ViewLayout = Layout;
2126
export type ViewLayoutEvent = LayoutEvent;
@@ -25,9 +30,8 @@ type DirectEventProps = $ReadOnly<{|
2530
* When `accessible` is true, the system will try to invoke this function
2631
* when the user performs an accessibility custom action.
2732
*
28-
* @platform ios
2933
*/
30-
onAccessibilityAction?: ?(string) => void,
34+
onAccessibilityAction?: ?(event: AccessibilityActionEvent) => void,
3135

3236
/**
3337
* When `accessible` is true, the system will try to invoke this function
@@ -321,13 +325,6 @@ type AndroidViewProps = $ReadOnly<{|
321325
|}>;
322326

323327
type IOSViewProps = $ReadOnly<{|
324-
/**
325-
* Provides an array of custom actions available for accessibility.
326-
*
327-
* @platform ios
328-
*/
329-
accessibilityActions?: ?$ReadOnlyArray<string>,
330-
331328
/**
332329
* Prevents view from being inverted if set to true and color inversion is turned on.
333330
*
@@ -417,6 +414,12 @@ export type ViewProps = $ReadOnly<{|
417414
*/
418415
accessibilityStates?: ?AccessibilityStates,
419416

417+
/**
418+
* Provides an array of custom actions available for accessibility.
419+
*
420+
*/
421+
accessibilityActions?: ?$ReadOnlyArray<AccessibilityActionInfo>,
422+
420423
/**
421424
* Used to locate this view in end-to-end tests.
422425
*

RNTester/js/AccessibilityExample.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,72 @@ class AccessibilityRoleAndStateExample extends React.Component<{}> {
410410
}
411411
}
412412

413+
class AccessibilityActionsExample extends React.Component {
414+
render() {
415+
return (
416+
<View>
417+
<RNTesterBlock title="Non-touchable with activate action">
418+
<View
419+
accessible={true}
420+
accessibilityActions={[{name: 'activate'}]}
421+
onAccessibilityAction={event => {
422+
switch (event.nativeEvent.actionName) {
423+
case 'activate':
424+
Alert.alert('Alert', 'View is clicked');
425+
break;
426+
}
427+
}}>
428+
<Text>Click me</Text>
429+
</View>
430+
</RNTesterBlock>
431+
432+
<RNTesterBlock title="View with multiple actions">
433+
<View
434+
accessible={true}
435+
accessibilityActions={[
436+
{name: 'cut', label: 'cut label'},
437+
{name: 'copy', label: 'copy label'},
438+
{name: 'paste', label: 'paste label'},
439+
]}
440+
onAccessibilityAction={event => {
441+
switch (event.nativeEvent.actionName) {
442+
case 'cut':
443+
Alert.alert('Alert', 'cut action success');
444+
break;
445+
case 'copy':
446+
Alert.alert('Alert', 'copy action success');
447+
break;
448+
case 'paste':
449+
Alert.alert('Alert', 'paste action success');
450+
break;
451+
}
452+
}}>
453+
<Text>This view supports many actions.</Text>
454+
</View>
455+
</RNTesterBlock>
456+
457+
<RNTesterBlock title="Adjustable with increment/decrement actions">
458+
<View
459+
accessible={true}
460+
accessibilityRole="adjustable"
461+
accessibilityActions={[{name: 'increment'}, {name: 'decrement'}]}
462+
onAccessibilityAction={event => {
463+
switch (event.nativeEvent.actionName) {
464+
case 'increment':
465+
Alert.alert('Alert', 'increment action success');
466+
break;
467+
case 'decrement':
468+
Alert.alert('Alert', 'decrement action success');
469+
break;
470+
}
471+
}}>
472+
<Text>Slider</Text>
473+
</View>
474+
</RNTesterBlock>
475+
</View>
476+
);
477+
}
478+
}
413479
class ScreenReaderStatusExample extends React.Component<{}> {
414480
state = {
415481
screenReaderEnabled: false,
@@ -483,6 +549,12 @@ exports.examples = [
483549
return <AccessibilityRoleAndStateExample />;
484550
},
485551
},
552+
{
553+
title: 'Accessibility action examples',
554+
render(): React.Element<typeof AccessibilityActionsExample> {
555+
return <AccessibilityActionsExample />;
556+
},
557+
},
486558
{
487559
title: 'Check if the screen reader is enabled',
488560
render(): React.Element<typeof ScreenReaderStatusExample> {

RNTester/js/AccessibilityIOSExample.js

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,33 @@ class AccessibilityIOSExample extends React.Component<Props> {
2121
return (
2222
<RNTesterBlock title="Accessibility iOS APIs">
2323
<View
24-
onAccessibilityTap={() =>
25-
Alert.alert('Alert', 'onAccessibilityTap success')
26-
}
27-
accessible={true}>
24+
onAccessibilityAction={event => {
25+
if (event.nativeEvent.actionName === 'activate') {
26+
Alert.alert('Alert', 'onAccessibilityTap success');
27+
}
28+
}}
29+
accessible={true}
30+
accessibilityActions={[{name: 'activate'}]}>
2831
<Text>Accessibility normal tap example</Text>
2932
</View>
3033
<View
31-
onMagicTap={() => Alert.alert('Alert', 'onMagicTap success')}
32-
accessible={true}>
34+
onAccessibilityAction={event => {
35+
if (event.nativeEvent.actionName === 'magicTap') {
36+
Alert.alert('Alert', 'onMagicTap success');
37+
}
38+
}}
39+
accessible={true}
40+
accessibilityActions={[{name: 'magicTap'}]}>
3341
<Text>Accessibility magic tap example</Text>
3442
</View>
3543
<View
36-
onAccessibilityEscape={() =>
37-
Alert.alert('onAccessibilityEscape success')
38-
}
39-
accessible={true}>
44+
onAccessibilityAction={event => {
45+
if (event.nativeEvent.actionName === 'escape') {
46+
Alert.alert('onAccessibilityEscape success');
47+
}
48+
}}
49+
accessible={true}
50+
accessibilityActions={[{name: 'escape'}]}>
4051
<Text>Accessibility escape example</Text>
4152
</View>
4253
<View accessibilityElementsHidden={true}>

React/Views/RCTView.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait;
2323
/**
2424
* Accessibility event handlers
2525
*/
26+
@property (nonatomic, copy) NSArray <NSDictionary *> *accessibilityActions;
2627
@property (nonatomic, copy) RCTDirectEventBlock onAccessibilityAction;
2728
@property (nonatomic, copy) RCTDirectEventBlock onAccessibilityTap;
2829
@property (nonatomic, copy) RCTDirectEventBlock onMagicTap;

React/Views/RCTView.m

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ - (UIView *)react_findClipView
101101
@implementation RCTView
102102
{
103103
UIColor *_backgroundColor;
104+
NSMutableDictionary<NSString *, NSDictionary *> *accessibilityActionsNameMap;
105+
NSMutableDictionary<NSString *, NSDictionary *> *accessibilityActionsLabelMap;
104106
}
105107

106108
- (instancetype)initWithFrame:(CGRect)frame
@@ -156,33 +158,57 @@ - (NSString *)accessibilityLabel
156158
return RCTRecursiveAccessibilityLabel(self);
157159
}
158160

161+
-(void)setAccessibilityActions:(NSArray *)actions
162+
{
163+
if (!actions) {
164+
return;
165+
}
166+
accessibilityActionsNameMap = [[NSMutableDictionary alloc] init];
167+
accessibilityActionsLabelMap = [[NSMutableDictionary alloc] init];
168+
for (NSDictionary *action in actions) {
169+
if (action[@"name"]) {
170+
accessibilityActionsNameMap[action[@"name"]] = action;
171+
}
172+
if (action[@"label"]) {
173+
accessibilityActionsLabelMap[action[@"label"]] = action;
174+
}
175+
}
176+
_accessibilityActions = [actions copy];
177+
}
178+
159179
- (NSArray <UIAccessibilityCustomAction *> *)accessibilityCustomActions
160180
{
161181
if (!self.accessibilityActions.count) {
162182
return nil;
163183
}
164184

165185
NSMutableArray *actions = [NSMutableArray array];
166-
for (NSString *action in self.accessibilityActions) {
167-
[actions addObject:[[UIAccessibilityCustomAction alloc] initWithName:action
168-
target:self
169-
selector:@selector(didActivateAccessibilityCustomAction:)]];
186+
for (NSDictionary *action in self.accessibilityActions) {
187+
if (action[@"label"]) {
188+
[actions addObject:[[UIAccessibilityCustomAction alloc] initWithName:action[@"label"]
189+
target:self
190+
selector:@selector(didActivateAccessibilityCustomAction:)]];
191+
}
170192
}
171193

172194
return [actions copy];
173195
}
174196

175197
- (BOOL)didActivateAccessibilityCustomAction:(UIAccessibilityCustomAction *)action
176198
{
177-
if (!_onAccessibilityAction) {
199+
if (!_onAccessibilityAction || !accessibilityActionsLabelMap) {
178200
return NO;
179201
}
180202

181-
_onAccessibilityAction(@{
182-
@"action": action.name,
183-
@"target": self.reactTag
184-
});
203+
// iOS defines the name as the localized label, so use our map to convert this back to the non-localized action namne when passing to JS. This allows for standard action names across platforms.
185204

205+
NSDictionary *actionObject = accessibilityActionsLabelMap[action.name];
206+
if (actionObject) {
207+
_onAccessibilityAction(@{
208+
@"actionName": actionObject[@"name"],
209+
@"actionTarget": self.reactTag
210+
});
211+
}
186212
return YES;
187213
}
188214

@@ -327,9 +353,24 @@ - (BOOL)isAccessibilityElement
327353
return NO;
328354
}
329355

356+
- (BOOL)performAccessibilityAction:(NSString *) name
357+
{
358+
if (_onAccessibilityAction && accessibilityActionsNameMap[name]) {
359+
_onAccessibilityAction(@{
360+
@"actionName" : name,
361+
@"actionTarget" : self.reactTag
362+
});
363+
return YES;
364+
}
365+
return NO;
366+
}
367+
330368
- (BOOL)accessibilityActivate
331369
{
332-
if (_onAccessibilityTap) {
370+
if ([self performAccessibilityAction:@"activate"]) {
371+
return YES;
372+
}
373+
else if (_onAccessibilityTap) {
333374
_onAccessibilityTap(nil);
334375
return YES;
335376
} else {
@@ -339,7 +380,9 @@ - (BOOL)accessibilityActivate
339380

340381
- (BOOL)accessibilityPerformMagicTap
341382
{
342-
if (_onMagicTap) {
383+
if ([self performAccessibilityAction:@"magicTap"]) {
384+
return YES;
385+
} else if (_onMagicTap) {
343386
_onMagicTap(nil);
344387
return YES;
345388
} else {
@@ -349,14 +392,26 @@ - (BOOL)accessibilityPerformMagicTap
349392

350393
- (BOOL)accessibilityPerformEscape
351394
{
352-
if (_onAccessibilityEscape) {
395+
if ([self performAccessibilityAction:@"escape"]) {
396+
return YES;
397+
} else if (_onAccessibilityEscape) {
353398
_onAccessibilityEscape(nil);
354399
return YES;
355400
} else {
356401
return NO;
357402
}
358403
}
359404

405+
- (void)accessibilityIncrement
406+
{
407+
[self performAccessibilityAction:@"increment"];
408+
}
409+
410+
- (void)accessibilityDecrement
411+
{
412+
[self performAccessibilityAction:@"decrement"];
413+
}
414+
360415
- (NSString *)description
361416
{
362417
NSString *superDescription = super.description;

React/Views/RCTViewManager.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ - (RCTShadowView *)shadowView
123123

124124
// Acessibility related properties
125125
RCT_REMAP_VIEW_PROPERTY(accessible, reactAccessibilityElement.isAccessibilityElement, BOOL)
126-
RCT_REMAP_VIEW_PROPERTY(accessibilityActions, reactAccessibilityElement.accessibilityActions, NSArray<NSString *>)
126+
RCT_REMAP_VIEW_PROPERTY(accessibilityActions, reactAccessibilityElement.accessibilityActions, NSDictionaryArray)
127127
RCT_REMAP_VIEW_PROPERTY(accessibilityLabel, reactAccessibilityElement.accessibilityLabel, NSString)
128128
RCT_REMAP_VIEW_PROPERTY(accessibilityHint, reactAccessibilityElement.accessibilityHint, NSString)
129129
RCT_REMAP_VIEW_PROPERTY(accessibilityViewIsModal, reactAccessibilityElement.accessibilityViewIsModal, BOOL)

React/Views/UIView+React.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@
116116
/**
117117
* Accessibility properties
118118
*/
119-
@property (nonatomic, copy) NSArray <NSString *> *accessibilityActions;
120119
@property (nonatomic, copy) NSString *accessibilityRole;
121120
@property (nonatomic, copy) NSArray <NSString *> *accessibilityStates;
122121

0 commit comments

Comments
 (0)