Skip to content
Permalink
Browse files

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
  • Loading branch information...
xuelgong authored and facebook-github-bot committed May 20, 2019
1 parent 933e65e commit 14b4668947789220617d6f54bb2fcdcfae844497
@@ -10,6 +10,8 @@

'use strict';

import type {SyntheticEvent} from 'CoreEventTypes';

// This must be kept in sync with the AccessibilityRolesMask in RCTViewManager.m
export type AccessibilityRole =
| 'none'
@@ -51,3 +53,16 @@ export type AccessibilityStates = $ReadOnlyArray<
| 'collapsed'
| 'hasPopup',
>;

// the info associated with an accessibility action
export type AccessibilityActionInfo = $ReadOnly<{
name: string,
label?: string,
}>;

// The info included in the event sent to onAccessibilityAction
export type AccessibilityActionEvent = SyntheticEvent<
$ReadOnly<{
actionName: string,
}>,
>;
@@ -15,7 +15,12 @@ import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType';
import type {Node} from 'react';
import type {ViewStyleProp} from '../../StyleSheet/StyleSheet';
import type {TVViewProps} from '../AppleTV/TVViewPropTypes';
import type {AccessibilityRole, AccessibilityStates} from './ViewAccessibility';
import type {
AccessibilityRole,
AccessibilityStates,
AccessibilityActionEvent,
AccessibilityActionInfo,
} from './ViewAccessibility';

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

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

type IOSViewProps = $ReadOnly<{|
/**
* Provides an array of custom actions available for accessibility.
*
* @platform ios
*/
accessibilityActions?: ?$ReadOnlyArray<string>,

/**
* Prevents view from being inverted if set to true and color inversion is turned on.
*
@@ -417,6 +414,12 @@ export type ViewProps = $ReadOnly<{|
*/
accessibilityStates?: ?AccessibilityStates,

/**
* Provides an array of custom actions available for accessibility.
*
*/
accessibilityActions?: ?$ReadOnlyArray<AccessibilityActionInfo>,

/**
* Used to locate this view in end-to-end tests.
*
@@ -410,6 +410,72 @@ class AccessibilityRoleAndStateExample extends React.Component<{}> {
}
}

class AccessibilityActionsExample extends React.Component {
render() {
return (
<View>
<RNTesterBlock title="Non-touchable with activate action">
<View
accessible={true}
accessibilityActions={[{name: 'activate'}]}
onAccessibilityAction={event => {
switch (event.nativeEvent.actionName) {
case 'activate':
Alert.alert('Alert', 'View is clicked');
break;
}
}}>
<Text>Click me</Text>
</View>
</RNTesterBlock>

<RNTesterBlock title="View with multiple actions">
<View
accessible={true}
accessibilityActions={[
{name: 'cut', label: 'cut label'},
{name: 'copy', label: 'copy label'},
{name: 'paste', label: 'paste label'},
]}
onAccessibilityAction={event => {
switch (event.nativeEvent.actionName) {
case 'cut':
Alert.alert('Alert', 'cut action success');
break;
case 'copy':
Alert.alert('Alert', 'copy action success');
break;
case 'paste':
Alert.alert('Alert', 'paste action success');
break;
}
}}>
<Text>This view supports many actions.</Text>
</View>
</RNTesterBlock>

<RNTesterBlock title="Adjustable with increment/decrement actions">
<View
accessible={true}
accessibilityRole="adjustable"
accessibilityActions={[{name: 'increment'}, {name: 'decrement'}]}
onAccessibilityAction={event => {
switch (event.nativeEvent.actionName) {
case 'increment':
Alert.alert('Alert', 'increment action success');
break;
case 'decrement':
Alert.alert('Alert', 'decrement action success');
break;
}
}}>
<Text>Slider</Text>
</View>
</RNTesterBlock>
</View>
);
}
}
class ScreenReaderStatusExample extends React.Component<{}> {
state = {
screenReaderEnabled: false,
@@ -483,6 +549,12 @@ exports.examples = [
return <AccessibilityRoleAndStateExample />;
},
},
{
title: 'Accessibility action examples',
render(): React.Element<typeof AccessibilityActionsExample> {
return <AccessibilityActionsExample />;
},
},
{
title: 'Check if the screen reader is enabled',
render(): React.Element<typeof ScreenReaderStatusExample> {
@@ -21,22 +21,33 @@ class AccessibilityIOSExample extends React.Component<Props> {
return (
<RNTesterBlock title="Accessibility iOS APIs">
<View
onAccessibilityTap={() =>
Alert.alert('Alert', 'onAccessibilityTap success')
}
accessible={true}>
onAccessibilityAction={event => {
if (event.nativeEvent.actionName === 'activate') {
Alert.alert('Alert', 'onAccessibilityTap success');
}
}}
accessible={true}
accessibilityActions={[{name: 'activate'}]}>
<Text>Accessibility normal tap example</Text>
</View>
<View
onMagicTap={() => Alert.alert('Alert', 'onMagicTap success')}
accessible={true}>
onAccessibilityAction={event => {
if (event.nativeEvent.actionName === 'magicTap') {
Alert.alert('Alert', 'onMagicTap success');
}
}}
accessible={true}
accessibilityActions={[{name: 'magicTap'}]}>
<Text>Accessibility magic tap example</Text>
</View>
<View
onAccessibilityEscape={() =>
Alert.alert('onAccessibilityEscape success')
}
accessible={true}>
onAccessibilityAction={event => {
if (event.nativeEvent.actionName === 'escape') {
Alert.alert('onAccessibilityEscape success');
}
}}
accessible={true}
accessibilityActions={[{name: 'escape'}]}>
<Text>Accessibility escape example</Text>
</View>
<View accessibilityElementsHidden={true}>
@@ -23,6 +23,7 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait;
/**
* Accessibility event handlers
*/
@property (nonatomic, copy) NSArray <NSDictionary *> *accessibilityActions;
@property (nonatomic, copy) RCTDirectEventBlock onAccessibilityAction;
@property (nonatomic, copy) RCTDirectEventBlock onAccessibilityTap;
@property (nonatomic, copy) RCTDirectEventBlock onMagicTap;
@@ -101,6 +101,8 @@ - (UIView *)react_findClipView
@implementation RCTView
{
UIColor *_backgroundColor;
NSMutableDictionary<NSString *, NSDictionary *> *accessibilityActionsNameMap;
NSMutableDictionary<NSString *, NSDictionary *> *accessibilityActionsLabelMap;
}

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

-(void)setAccessibilityActions:(NSArray *)actions
{
if (!actions) {
return;
}
accessibilityActionsNameMap = [[NSMutableDictionary alloc] init];
accessibilityActionsLabelMap = [[NSMutableDictionary alloc] init];
for (NSDictionary *action in actions) {
if (action[@"name"]) {
accessibilityActionsNameMap[action[@"name"]] = action;
}
if (action[@"label"]) {
accessibilityActionsLabelMap[action[@"label"]] = action;
}
}
_accessibilityActions = [actions copy];
}

- (NSArray <UIAccessibilityCustomAction *> *)accessibilityCustomActions
{
if (!self.accessibilityActions.count) {
return nil;
}

NSMutableArray *actions = [NSMutableArray array];
for (NSString *action in self.accessibilityActions) {
[actions addObject:[[UIAccessibilityCustomAction alloc] initWithName:action
target:self
selector:@selector(didActivateAccessibilityCustomAction:)]];
for (NSDictionary *action in self.accessibilityActions) {
if (action[@"label"]) {
[actions addObject:[[UIAccessibilityCustomAction alloc] initWithName:action[@"label"]
target:self
selector:@selector(didActivateAccessibilityCustomAction:)]];
}
}

return [actions copy];
}

- (BOOL)didActivateAccessibilityCustomAction:(UIAccessibilityCustomAction *)action
{
if (!_onAccessibilityAction) {
if (!_onAccessibilityAction || !accessibilityActionsLabelMap) {
return NO;
}

_onAccessibilityAction(@{
@"action": action.name,
@"target": self.reactTag
});
// 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.

NSDictionary *actionObject = accessibilityActionsLabelMap[action.name];
if (actionObject) {
_onAccessibilityAction(@{
@"actionName": actionObject[@"name"],
@"actionTarget": self.reactTag
});
}
return YES;
}

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

- (BOOL)performAccessibilityAction:(NSString *) name
{
if (_onAccessibilityAction && accessibilityActionsNameMap[name]) {
_onAccessibilityAction(@{
@"actionName" : name,
@"actionTarget" : self.reactTag
});
return YES;
}
return NO;
}

- (BOOL)accessibilityActivate
{
if (_onAccessibilityTap) {
if ([self performAccessibilityAction:@"activate"]) {
return YES;
}
else if (_onAccessibilityTap) {
_onAccessibilityTap(nil);
return YES;
} else {
@@ -339,7 +380,9 @@ - (BOOL)accessibilityActivate

- (BOOL)accessibilityPerformMagicTap
{
if (_onMagicTap) {
if ([self performAccessibilityAction:@"magicTap"]) {
return YES;
} else if (_onMagicTap) {
_onMagicTap(nil);
return YES;
} else {
@@ -349,14 +392,26 @@ - (BOOL)accessibilityPerformMagicTap

- (BOOL)accessibilityPerformEscape
{
if (_onAccessibilityEscape) {
if ([self performAccessibilityAction:@"escape"]) {
return YES;
} else if (_onAccessibilityEscape) {
_onAccessibilityEscape(nil);
return YES;
} else {
return NO;
}
}

- (void)accessibilityIncrement
{
[self performAccessibilityAction:@"increment"];
}

- (void)accessibilityDecrement
{
[self performAccessibilityAction:@"decrement"];
}

- (NSString *)description
{
NSString *superDescription = super.description;
@@ -123,7 +123,7 @@ - (RCTShadowView *)shadowView

// Acessibility related properties
RCT_REMAP_VIEW_PROPERTY(accessible, reactAccessibilityElement.isAccessibilityElement, BOOL)
RCT_REMAP_VIEW_PROPERTY(accessibilityActions, reactAccessibilityElement.accessibilityActions, NSArray<NSString *>)
RCT_REMAP_VIEW_PROPERTY(accessibilityActions, reactAccessibilityElement.accessibilityActions, NSDictionaryArray)
RCT_REMAP_VIEW_PROPERTY(accessibilityLabel, reactAccessibilityElement.accessibilityLabel, NSString)
RCT_REMAP_VIEW_PROPERTY(accessibilityHint, reactAccessibilityElement.accessibilityHint, NSString)
RCT_REMAP_VIEW_PROPERTY(accessibilityViewIsModal, reactAccessibilityElement.accessibilityViewIsModal, BOOL)
@@ -116,7 +116,6 @@
/**
* Accessibility properties
*/
@property (nonatomic, copy) NSArray <NSString *> *accessibilityActions;
@property (nonatomic, copy) NSString *accessibilityRole;
@property (nonatomic, copy) NSArray <NSString *> *accessibilityStates;

0 comments on commit 14b4668

Please sign in to comment.
You can’t perform that action at this time.