Additional Accessibility Roles and States (#24095)

Summary:
Assistive technologies use the accessibility role of a component to tell the disabled user what the component is, and provide hints about how to use it. Many important roles do not have analog AccessibilityTraits on iOS. This PR adds many critical roles, such as editabletext, checkbox, menu, and switch to name a few.

Accessibility states are used to convey the current state of a component. This PR adds several critical states such as checked, unchecked, on and off.

[general] [change] - Adds critical accessibility roles and states.
Pull Request resolved: https://github.com/facebook/react-native/pull/24095

Differential Revision: D15079245

Pulled By: cpojer

fbshipit-source-id: 941b30eb8f5d565597e5ea3a04687d9809cbe372
This commit is contained in:
Marc Mulcahy
2019-04-25 06:09:35 -07:00
committed by Facebook Github Bot
parent 421ffb05ae
commit 1aeac1c625
10 changed files with 743 additions and 120 deletions

View File

@@ -22,7 +22,32 @@ export type AccessibilityRole =
| 'adjustable'
| 'imagebutton'
| 'header'
| 'summary';
| 'summary'
| 'alert'
| 'checkbox'
| 'combobox'
| 'menu'
| 'menubar'
| 'menuitem'
| 'progressbar'
| 'radio'
| 'radiogroup'
| 'scrollbar'
| 'spinbutton'
| 'switch'
| 'tab'
| 'tablist'
| 'timer'
| 'toolbar';
// This must be kept in sync with the AccessibilityStatesMask in RCTViewManager.m
export type AccessibilityStates = $ReadOnlyArray<'disabled' | 'selected'>;
export type AccessibilityStates = $ReadOnlyArray<
| 'disabled'
| 'selected'
| 'checked'
| 'unchecked'
| 'busy'
| 'expanded'
| 'collapsed'
| 'hasPopup',
>;

View File

@@ -24,7 +24,32 @@ module.exports = {
'imagebutton',
'header',
'summary',
'alert',
'checkbox',
'combobox',
'menu',
'menubar',
'menuitem',
'progressbar',
'radio',
'radiogroup',
'scrollbar',
'spinbutton',
'switch',
'tab',
'tablist',
'timer',
'toolbar',
],
// This must be kept in sync with the AccessibilityStatesMask in RCTViewManager.m
DeprecatedAccessibilityStates: ['selected', 'disabled'],
DeprecatedAccessibilityStates: [
'selected',
'disabled',
'checked',
'unchecked',
'busy',
'expanded',
'collapsed',
'hasPopup',
],
};

View File

@@ -12,10 +12,14 @@
const React = require('react');
const {
AccessibilityInfo,
Button,
Text,
View,
TouchableOpacity,
Alert,
UIManager,
findNodeHandle,
Platform,
} = require('react-native');
const RNTesterBlock = require('./RNTesterBlock');
@@ -138,6 +142,274 @@ class AccessibilityExample extends React.Component {
}
}
class CheckboxExample extends React.Component {
state = {
checkboxState: 'checked',
};
_onCheckboxPress = () => {
const checkboxState =
this.state.checkboxState === 'checked' ? 'unchecked' : 'checked';
this.setState({
checkboxState: checkboxState,
});
if (Platform.OS === 'android') {
UIManager.sendAccessibilityEvent(
findNodeHandle(this),
UIManager.AccessibilityEventTypes.typeViewClicked,
);
}
};
render() {
return (
<TouchableOpacity
onPress={this._onCheckboxPress}
accessibilityLabel="element 2"
accessibilityRole="checkbox"
accessibilityStates={[this.state.checkboxState]}
accessibilityHint="click me to change state">
<Text>Checkbox example</Text>
</TouchableOpacity>
);
}
}
class SwitchExample extends React.Component {
state = {
switchState: 'checked',
};
_onSwitchToggle = () => {
const switchState =
this.state.switchState === 'checked' ? 'unchecked' : 'checked';
this.setState({
switchState: switchState,
});
if (Platform.OS === 'android') {
UIManager.sendAccessibilityEvent(
findNodeHandle(this),
UIManager.AccessibilityEventTypes.typeViewClicked,
);
}
};
render() {
return (
<TouchableOpacity
onPress={this._onSwitchToggle}
accessibilityLabel="element 12"
accessibilityRole="switch"
accessibilityStates={[this.state.switchState]}
accessible={true}>
<Text>Switch example</Text>
</TouchableOpacity>
);
}
}
class SelectionExample extends React.Component {
constructor(props) {
super(props);
this.selectableElement = React.createRef();
}
state = {
isSelected: true,
isEnabled: false,
};
render() {
let accessibilityStates = [];
let accessibilityHint = 'click me to select';
if (this.state.isSelected) {
accessibilityStates.push('selected');
accessibilityHint = 'click me to unselect';
}
if (!this.state.isEnabled) {
accessibilityStates.push('disabled');
accessibilityHint = 'use the button on the right to enable selection';
}
let buttonTitle = this.state.isEnabled
? 'Disable selection'
: 'Enable selection';
return (
<View style={{flex: 1, flexDirection: 'row'}}>
<TouchableOpacity
ref={this.selectableElement}
accessible={true}
onPress={() => {
this.setState({
isSelected: !this.state.isSelected,
});
if (Platform.OS === 'android') {
UIManager.sendAccessibilityEvent(
findNodeHandle(this.selectableElement.current),
UIManager.AccessibilityEventTypes.typeViewClicked,
);
}
}}
accessibilityLabel="element 19"
accessibilityStates={accessibilityStates}
accessibilityHint={accessibilityHint}>
<Text>Selectable element example</Text>
</TouchableOpacity>
<Button
onPress={() => {
this.setState({
isEnabled: !this.state.isEnabled,
});
}}
title={buttonTitle}
/>
</View>
);
}
}
class ExpandableElementExample extends React.Component {
state = {
expandState: 'collapsed',
};
_onElementPress = () => {
const expandState =
this.state.expandState === 'collapsed' ? 'expanded' : 'collapsed';
this.setState({
expandState: expandState,
});
if (Platform.OS === 'android') {
UIManager.sendAccessibilityEvent(
findNodeHandle(this),
UIManager.AccessibilityEventTypes.typeViewClicked,
);
}
};
render() {
return (
<TouchableOpacity
onPress={this._onElementPress}
accessibilityLabel="element 18"
accessibilityStates={[this.state.expandState]}
accessibilityHint="click me to change state">
<Text>Expandable element example</Text>
</TouchableOpacity>
);
}
}
class AccessibilityRoleAndStateExample extends React.Component<{}> {
render() {
return (
<View>
<View
accessibilityLabel="element 1"
accessibilityRole="alert"
accessible={true}>
<Text>Alert example</Text>
</View>
<CheckboxExample />
<View
accessibilityLabel="element 3"
accessibilityRole="combobox"
accessible={true}>
<Text>Combobox example</Text>
</View>
<View
accessibilityLabel="element 4"
accessibilityRole="menu"
accessible={true}>
<Text>Menu example</Text>
</View>
<View
accessibilityLabel="element 5"
accessibilityRole="menubar"
accessible={true}>
<Text>Menu bar example</Text>
</View>
<View
accessibilityLabel="element 6"
accessibilityRole="menuitem"
accessible={true}>
<Text>Menu item example</Text>
</View>
<View
accessibilityLabel="element 7"
accessibilityRole="progressbar"
accessible={true}>
<Text>Progress bar example</Text>
</View>
<View
accessibilityLabel="element 8"
accessibilityRole="radio"
accessible={true}>
<Text>Radio button example</Text>
</View>
<View
accessibilityLabel="element 9"
accessibilityRole="radiogroup"
accessible={true}>
<Text>Radio group example</Text>
</View>
<View
accessibilityLabel="element 10"
accessibilityRole="scrollbar"
accessible={true}>
<Text>Scrollbar example</Text>
</View>
<View
accessibilityLabel="element 11"
accessibilityRole="spinbutton"
accessible={true}>
<Text>Spin button example</Text>
</View>
<SwitchExample />
<View
accessibilityLabel="element 13"
accessibilityRole="tab"
accessible={true}>
<Text>Tab example</Text>
</View>
<View
accessibilityLabel="element 14"
accessibilityRole="tablist"
accessible={true}>
<Text>Tab list example</Text>
</View>
<View
accessibilityLabel="element 15"
accessibilityRole="timer"
accessible={true}>
<Text>Timer example</Text>
</View>
<View
accessibilityLabel="element 16"
accessibilityRole="toolbar"
accessible={true}>
<Text>Toolbar example</Text>
</View>
<View
accessibilityLabel="element 17"
accessibilityStates={['busy']}
accessible={true}>
<Text>State busy example</Text>
</View>
<ExpandableElementExample />
<SelectionExample />
</View>
);
}
}
class ScreenReaderStatusExample extends React.Component<{}> {
state = {
screenReaderEnabled: false,
@@ -189,6 +461,12 @@ exports.examples = [
return <AccessibilityExample />;
},
},
{
title: 'New accessibility roles and states',
render(): React.Element<typeof AccessibilityRoleAndStateExamples> {
return <AccessibilityRoleAndStateExample />;
},
},
{
title: 'Check if the screen reader is enabled',
render(): React.Element<typeof ScreenReaderStatusExample> {

View File

@@ -12,6 +12,8 @@
#import <React/RCTPointerEvents.h>
#import <React/RCTView.h>
extern const UIAccessibilityTraits SwitchAccessibilityTrait;
@protocol RCTAutoInsetsProtocol;
@class RCTView;
@@ -30,6 +32,8 @@
* Accessibility properties
*/
@property (nonatomic, copy) NSArray <NSString *> *accessibilityActions;
@property (nonatomic, copy) NSString *accessibilityRole;
@property (nonatomic, copy) NSArray <NSString *> *accessibilityStates;
/**
* Used to control how touch events are processed.

View File

@@ -15,6 +15,8 @@
#import "UIView+React.h"
#import "RCTI18nUtil.h"
UIAccessibilityTraits const SwitchAccessibilityTrait = 0x20000000000001;
@implementation UIView (RCTViewUnmounting)
- (void)react_remountAllSubviews
@@ -184,6 +186,67 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
return YES;
}
- (NSString *)accessibilityValue
{
if ((self.accessibilityTraits & SwitchAccessibilityTrait) == SwitchAccessibilityTrait) {
for (NSString *state in _accessibilityStates) {
if ([state isEqualToString:@"checked"]) {
return @"1";
} else if ([state isEqualToString:@"unchecked"]) {
return @"0";
}
}
}
NSMutableArray *valueComponents = [NSMutableArray new];
static NSDictionary<NSString *, NSString *> *roleDescriptions = nil;
static dispatch_once_t onceToken1;
dispatch_once(&onceToken1, ^{
roleDescriptions = @{
@"alert" : @"alert",
@"checkbox" : @"checkbox",
@"combobox" : @"combo box",
@"menu" : @"menu",
@"menubar" : @"menu bar",
@"menuitem" : @"menu item",
@"progressbar" : @"progress bar",
@"radio" : @"radio button",
@"radiogroup" : @"radio group",
@"scrollbar" : @"scroll bar",
@"spinbutton" : @"spin button",
@"switch" : @"switch",
@"tab" : @"tab",
@"tablist" : @"tab list",
@"timer" : @"timer",
@"toolbar" : @"tool bar",
};
});
static NSDictionary<NSString *, NSString *> *stateDescriptions = nil;
static dispatch_once_t onceToken2;
dispatch_once(&onceToken2, ^{
stateDescriptions = @{
@"checked" : @"checked",
@"unchecked" : @"not checked",
@"busy" : @"busy",
@"expanded" : @"expanded",
@"collapsed" : @"collapsed",
};
});
NSString *roleDescription = _accessibilityRole ? roleDescriptions[_accessibilityRole]: nil;
if (roleDescription) {
[valueComponents addObject:roleDescription];
}
for (NSString *state in _accessibilityStates) {
NSString *stateDescription = state ? stateDescriptions[state] : nil;
if (stateDescription) {
[valueComponents addObject:stateDescription];
}
}
if (valueComponents.count > 0) {
return [valueComponents componentsJoinedByString:@", "];
}
return nil;
}
- (void)setPointerEvents:(RCTPointerEvents)pointerEvents
{
_pointerEvents = pointerEvents;

View File

@@ -46,6 +46,22 @@ RCT_MULTI_ENUM_CONVERTER(UIAccessibilityTraits, (@{
@"adjustable": @(UIAccessibilityTraitAdjustable),
@"allowsDirectInteraction": @(UIAccessibilityTraitAllowsDirectInteraction),
@"pageTurn": @(UIAccessibilityTraitCausesPageTurn),
@"alert": @(UIAccessibilityTraitNone),
@"checkbox": @(UIAccessibilityTraitNone),
@"combobox": @(UIAccessibilityTraitNone),
@"menu": @(UIAccessibilityTraitNone),
@"menubar": @(UIAccessibilityTraitNone),
@"menuitem": @(UIAccessibilityTraitNone),
@"progressbar": @(UIAccessibilityTraitNone),
@"radio": @(UIAccessibilityTraitNone),
@"radiogroup": @(UIAccessibilityTraitNone),
@"scrollbar": @(UIAccessibilityTraitNone),
@"spinbutton": @(UIAccessibilityTraitNone),
@"switch": @(SwitchAccessibilityTrait),
@"tab": @(UIAccessibilityTraitNone),
@"tablist": @(UIAccessibilityTraitNone),
@"timer": @(UIAccessibilityTraitNone),
@"toolbar": @(UIAccessibilityTraitNone),
}), UIAccessibilityTraitNone, unsignedLongLongValue)
@end
@@ -150,12 +166,44 @@ RCT_CUSTOM_VIEW_PROPERTY(transform, CATransform3D, RCTView)
RCT_CUSTOM_VIEW_PROPERTY(accessibilityRole, UIAccessibilityTraits, RCTView)
{
// This mask must be kept in sync with the AccessibilityRoles enum defined in ViewAccessibility.js and DeprecatedViewAccessibility.js
const UIAccessibilityTraits AccessibilityRolesMask = UIAccessibilityTraitNone | UIAccessibilityTraitButton | UIAccessibilityTraitLink | UIAccessibilityTraitSearchField | UIAccessibilityTraitImage | UIAccessibilityTraitKeyboardKey | UIAccessibilityTraitStaticText | UIAccessibilityTraitAdjustable | UIAccessibilityTraitHeader | UIAccessibilityTraitSummaryElement;
const UIAccessibilityTraits AccessibilityRolesMask = UIAccessibilityTraitNone | UIAccessibilityTraitButton | UIAccessibilityTraitLink | UIAccessibilityTraitSearchField | UIAccessibilityTraitImage | UIAccessibilityTraitKeyboardKey | UIAccessibilityTraitStaticText | UIAccessibilityTraitAdjustable | UIAccessibilityTraitHeader | UIAccessibilityTraitSummaryElement | SwitchAccessibilityTrait;
view.reactAccessibilityElement.accessibilityTraits = view.reactAccessibilityElement.accessibilityTraits & ~AccessibilityRolesMask;
UIAccessibilityTraits newTraits = json ? [RCTConvert UIAccessibilityTraits:json] : defaultView.accessibilityTraits;
UIAccessibilityTraits maskedTraits = newTraits & AccessibilityRolesMask;
view.reactAccessibilityElement.accessibilityTraits = (view.reactAccessibilityElement.accessibilityTraits & ~AccessibilityRolesMask) | maskedTraits;
if (newTraits != UIAccessibilityTraitNone) {
UIAccessibilityTraits maskedTraits = newTraits & AccessibilityRolesMask;
view.reactAccessibilityElement.accessibilityTraits |= maskedTraits;
} else {
NSString *role = json ? [RCTConvert NSString:json] : @"";
((RCTView *)view.reactAccessibilityElement).accessibilityRole = role;
}
}
RCT_CUSTOM_VIEW_PROPERTY(accessibilityStates, NSArray<NSString *>, RCTView)
{
NSArray<NSString *> *states = json ? [RCTConvert NSStringArray:json] : nil;
NSMutableArray *newStates = [NSMutableArray new];
if (!states) {
return;
}
const UIAccessibilityTraits AccessibilityStatesMask = UIAccessibilityTraitNotEnabled | UIAccessibilityTraitSelected;
view.reactAccessibilityElement.accessibilityTraits = view.reactAccessibilityElement.accessibilityTraits & ~AccessibilityStatesMask;
for (NSString *state in states) {
if ([state isEqualToString:@"selected"]) {
view.reactAccessibilityElement.accessibilityTraits |= UIAccessibilityTraitSelected;
} else if ([state isEqualToString:@"disabled"]) {
view.reactAccessibilityElement.accessibilityTraits |= UIAccessibilityTraitNotEnabled;
} else {
[newStates addObject:state];
}
}
if (newStates.count > 0) {
((RCTView *)view.reactAccessibilityElement).accessibilityStates = newStates;
} else {
((RCTView *)view.reactAccessibilityElement).accessibilityStates = nil;
}
}
RCT_CUSTOM_VIEW_PROPERTY(nativeID, NSString *, RCTView)

View File

@@ -17,63 +17,75 @@ import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat;
import android.view.View;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.R;
import java.util.Locale;
import javax.annotation.Nullable;
/**
* Utility class that handles the addition of a "role" for accessibility to either a View or
* AccessibilityNodeInfo.
* Utility class that handles the addition of a "role" for accessibility to
* either a View or AccessibilityNodeInfo.
*/
public class AccessibilityDelegateUtil {
/**
* These roles are defined by Google's TalkBack screen reader, and this list should be kept up to
* date with their implementation. Details can be seen in their source code here:
* These roles are defined by Google's TalkBack screen reader, and this list
* should be kept up to date with their implementation. Details can be seen in
* their source code here:
*
* <p>https://github.com/google/talkback/blob/master/utils/src/main/java/Role.java
* <p>
* https://github.com/google/talkback/blob/master/utils/src/main/java/Role.java
*/
public enum AccessibilityRole {
NONE,
BUTTON,
LINK,
SEARCH,
IMAGE,
IMAGEBUTTON,
KEYBOARDKEY,
TEXT,
ADJUSTABLE,
SUMMARY,
HEADER;
NONE, BUTTON, LINK, SEARCH, IMAGE, IMAGEBUTTON, KEYBOARDKEY, TEXT, ADJUSTABLE, SUMMARY, HEADER, ALERT, CHECKBOX,
COMBOBOX, MENU, MENUBAR, MENUITEM, PROGRESSBAR, RADIO, RADIOGROUP, SCROLLBAR, SPINBUTTON,
SWITCH, TAB, TABLIST, TIMER, TOOLBAR;
public static String getValue(AccessibilityRole role) {
switch (role) {
case NONE:
return null;
case BUTTON:
return "android.widget.Button";
case LINK:
return "android.widget.ViewGroup";
case SEARCH:
return "android.widget.EditText";
case IMAGE:
return "android.widget.ImageView";
case IMAGEBUTTON:
return "android.widget.ImageView";
case KEYBOARDKEY:
return "android.inputmethodservice.Keyboard$Key";
case TEXT:
return "android.widget.ViewGroup";
case ADJUSTABLE:
return "android.widget.SeekBar";
case SUMMARY:
return "android.widget.ViewGroup";
case HEADER:
return "android.widget.ViewGroup";
default:
throw new IllegalArgumentException("Invalid accessibility role value: " + role);
case BUTTON:
return "android.widget.Button";
case SEARCH:
return "android.widget.EditText";
case IMAGE:
return "android.widget.ImageView";
case IMAGEBUTTON:
return "android.widget.ImageButon";
case KEYBOARDKEY:
return "android.inputmethodservice.Keyboard$Key";
case TEXT:
return "android.widget.TextView";
case ADJUSTABLE:
return "android.widget.SeekBar";
case CHECKBOX:
return "android.widget.CheckBox";
case RADIO:
return "android.widget.RadioButton";
case SPINBUTTON:
return "android.widget.SpinButton";
case SWITCH:
return "android.widget.Switch";
case NONE:
case LINK:
case SUMMARY:
case HEADER:
case ALERT:
case COMBOBOX:
case MENU:
case MENUBAR:
case MENUITEM:
case PROGRESSBAR:
case RADIOGROUP:
case SCROLLBAR:
case TAB:
case TABLIST:
case TIMER:
case TOOLBAR:
return "android.view.View";
default:
throw new IllegalArgumentException("Invalid accessibility role value: " + role);
}
}
@@ -92,32 +104,55 @@ public class AccessibilityDelegateUtil {
}
public static void setDelegate(final View view) {
final String accessibilityHint = (String) view.getTag(R.id.accessibility_hint);
final AccessibilityRole accessibilityRole = (AccessibilityRole) view.getTag(R.id.accessibility_role);
// if a view already has an accessibility delegate, replacing it could cause problems,
// if a view already has an accessibility delegate, replacing it could cause
// problems,
// so leave it alone.
if (!ViewCompat.hasAccessibilityDelegate(view) &&
(accessibilityHint != null || accessibilityRole != null)) {
ViewCompat.setAccessibilityDelegate(
view,
new AccessibilityDelegateCompat() {
@Override
public void onInitializeAccessibilityNodeInfo(
View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
if (!(accessibilityHint == null)) {
String contentDescription=(String)info.getContentDescription();
if (contentDescription != null) {
contentDescription = contentDescription + ", " + accessibilityHint;
info.setContentDescription(contentDescription);
} else {
info.setContentDescription(accessibilityHint);
}
}
setRole(info, accessibilityRole, view.getContext());
if (!ViewCompat.hasAccessibilityDelegate(view)
&& (accessibilityRole != null || view.getTag(R.id.accessibility_states) != null)) {
ViewCompat.setAccessibilityDelegate(view, new AccessibilityDelegateCompat() {
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
setRole(info, accessibilityRole, view.getContext());
// states are changable.
ReadableArray accessibilityStates = (ReadableArray) view.getTag(R.id.accessibility_states);
if (accessibilityStates != null) {
setState(info, accessibilityStates, view.getContext());
}
});
}
});
}
}
public static void setState(AccessibilityNodeInfoCompat info, ReadableArray accessibilityStates, Context context) {
for (int i = 0; i < accessibilityStates.size(); i++) {
String state = accessibilityStates.getString(i);
switch (state) {
case "selected":
info.setSelected(true);
break;
case "disabled":
info.setEnabled(false);
break;
case "checked":
info.setCheckable(true);
info.setChecked(true);
if (info.getClassName().equals("android.widget.Switch")) {
info.setText(context.getString(R.string.state_on_description));
}
break;
case "unchecked":
info.setCheckable(true);
info.setChecked(false);
if (info.getClassName().equals("android.widget.Switch")) {
info.setText(context.getString(R.string.state_off_description));
}
break;
case "hasPopup":
info.setCanOpenPopup(true);
break;
}
}
}
@@ -125,50 +160,85 @@ public class AccessibilityDelegateUtil {
* Strings for setting the Role Description in english
*/
//TODO: Eventually support for other languages on talkback
// TODO: Eventually support for other languages on talkback
public static void setRole(AccessibilityNodeInfoCompat nodeInfo, AccessibilityRole role, final Context context) {
if (role == null) {
role = AccessibilityRole.NONE;
}
nodeInfo.setClassName(AccessibilityRole.getValue(role));
if (Locale.getDefault().getLanguage().equals(new Locale("en").getLanguage())) {
if (role.equals(AccessibilityRole.LINK)) {
nodeInfo.setRoleDescription(context.getString(R.string.link_description));
if (role.equals(AccessibilityRole.LINK)) {
nodeInfo.setRoleDescription(context.getString(R.string.link_description));
if (nodeInfo.getContentDescription() != null) {
SpannableString spannable = new SpannableString(nodeInfo.getContentDescription());
spannable.setSpan(new URLSpan(""), 0, spannable.length(), 0);
nodeInfo.setContentDescription(spannable);
}
if (nodeInfo.getContentDescription() != null) {
SpannableString spannable = new SpannableString(nodeInfo.getContentDescription());
spannable.setSpan(new URLSpan(""), 0, spannable.length(), 0);
nodeInfo.setContentDescription(spannable);
}
if (nodeInfo.getText() != null) {
SpannableString spannable = new SpannableString(nodeInfo.getText());
spannable.setSpan(new URLSpan(""), 0, spannable.length(), 0);
nodeInfo.setText(spannable);
}
}
if (role.equals(AccessibilityRole.SEARCH)) {
nodeInfo.setRoleDescription(context.getString(R.string.search_description));
}
if (role.equals(AccessibilityRole.IMAGE)) {
nodeInfo.setRoleDescription(context.getString(R.string.image_description));
}
if (role.equals(AccessibilityRole.IMAGEBUTTON)) {
nodeInfo.setRoleDescription(context.getString(R.string.image_button_description));
}
if (role.equals(AccessibilityRole.ADJUSTABLE)) {
nodeInfo.setRoleDescription(context.getString(R.string.adjustable_description));
}
if (role.equals(AccessibilityRole.HEADER)) {
nodeInfo.setRoleDescription(context.getString(R.string.header_description));
final AccessibilityNodeInfoCompat.CollectionItemInfoCompat itemInfo =
AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(0, 1, 0, 1, true);
nodeInfo.setCollectionItemInfo(itemInfo);
if (nodeInfo.getText() != null) {
SpannableString spannable = new SpannableString(nodeInfo.getText());
spannable.setSpan(new URLSpan(""), 0, spannable.length(), 0);
nodeInfo.setText(spannable);
}
}
if (role.equals(AccessibilityRole.SEARCH)) {
nodeInfo.setRoleDescription(context.getString(R.string.search_description));
}
if (role.equals(AccessibilityRole.IMAGE)) {
nodeInfo.setRoleDescription(context.getString(R.string.image_description));
}
if (role.equals(AccessibilityRole.IMAGEBUTTON)) {
nodeInfo.setRoleDescription(context.getString(R.string.imagebutton_description));
nodeInfo.setClickable(true);
}
if (role.equals(AccessibilityRole.SUMMARY)) {
nodeInfo.setRoleDescription(context.getString(R.string.summary_description));
}
if (role.equals(AccessibilityRole.HEADER)) {
nodeInfo.setRoleDescription(context.getString(R.string.header_description));
final AccessibilityNodeInfoCompat.CollectionItemInfoCompat itemInfo =
AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(0, 1, 0, 1, true);
nodeInfo.setCollectionItemInfo(itemInfo);
}
if (role.equals(AccessibilityRole.ALERT)) {
nodeInfo.setRoleDescription(context.getString(R.string.alert_description));
}
if (role.equals(AccessibilityRole.COMBOBOX)) {
nodeInfo.setRoleDescription(context.getString(R.string.combobox_description));
}
if (role.equals(AccessibilityRole.MENU)) {
nodeInfo.setRoleDescription(context.getString(R.string.menu_description));
}
if (role.equals(AccessibilityRole.MENUBAR)) {
nodeInfo.setRoleDescription(context.getString(R.string.menubar_description));
}
if (role.equals(AccessibilityRole.MENUITEM)) {
nodeInfo.setRoleDescription(context.getString(R.string.menuitem_description));
}
if (role.equals(AccessibilityRole.PROGRESSBAR)) {
nodeInfo.setRoleDescription(context.getString(R.string.progressbar_description));
}
if (role.equals(AccessibilityRole.RADIOGROUP)) {
nodeInfo.setRoleDescription(context.getString(R.string.radiogroup_description));
}
if (role.equals(AccessibilityRole.SCROLLBAR)) {
nodeInfo.setRoleDescription(context.getString(R.string.scrollbar_description));
}
if (role.equals(AccessibilityRole.SPINBUTTON)) {
nodeInfo.setRoleDescription(context.getString(R.string.spinbutton_description));
}
if (role.equals(AccessibilityRole.TAB)) {
nodeInfo.setRoleDescription(context.getString(R.string.rn_tab_description));
}
if (role.equals(AccessibilityRole.TABLIST)) {
nodeInfo.setRoleDescription(context.getString(R.string.tablist_description));
}
if (role.equals(AccessibilityRole.TIMER)) {
nodeInfo.setRoleDescription(context.getString(R.string.timer_description));
}
if (role.equals(AccessibilityRole.TOOLBAR)) {
nodeInfo.setRoleDescription(context.getString(R.string.toolbar_description));
}
}
}

View File

@@ -9,6 +9,9 @@ import android.graphics.Color;
import android.view.View;
import android.view.ViewParent;
import androidx.core.view.ViewCompat;
import java.util.HashMap;
import com.facebook.react.R;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.uimanager.AccessibilityDelegateUtil.AccessibilityRole;
@@ -57,6 +60,13 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
new MatrixMathHelper.MatrixDecompositionContext();
private static double[] sTransformDecompositionArray = new double[16];
public static final HashMap<String, Integer> sStateDescription= new HashMap<String, Integer>();
static {
sStateDescription.put("busy", R.string.state_busy_description);
sStateDescription.put("expanded", R.string.state_expanded_description);
sStateDescription.put("collapsed", R.string.state_collapsed_description);
}
@ReactProp(name = PROP_BACKGROUND_COLOR, defaultInt = Color.TRANSPARENT, customType = "Color")
public void setBackgroundColor(@Nonnull T view, int backgroundColor) {
view.setBackgroundColor(backgroundColor);
@@ -112,12 +122,14 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
@ReactProp(name = PROP_ACCESSIBILITY_LABEL)
public void setAccessibilityLabel(@Nonnull T view, String accessibilityLabel) {
view.setContentDescription(accessibilityLabel);
view.setTag(R.id.accessibility_label, accessibilityLabel);
updateViewContentDescription(view);
}
@ReactProp(name = PROP_ACCESSIBILITY_HINT)
public void setAccessibilityHint(@Nonnull T view, String accessibilityHint) {
view.setTag(R.id.accessibility_hint, accessibilityHint);
updateViewContentDescription(view);
}
@ReactProp(name = PROP_ACCESSIBILITY_ROLE)
@@ -125,27 +137,47 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
if (accessibilityRole == null) {
return;
}
view.setTag(R.id.accessibility_role, AccessibilityRole.fromValue(accessibilityRole));
}
@ReactProp(name = PROP_ACCESSIBILITY_STATES)
public void setViewStates(@Nonnull T view, @Nullable ReadableArray accessibilityStates) {
view.setSelected(false);
view.setEnabled(true);
if (accessibilityStates == null) {
return;
}
view.setTag(R.id.accessibility_states, accessibilityStates);
for (int i = 0; i < accessibilityStates.size(); i++) {
String state = accessibilityStates.getString(i);
if (state.equals("selected")) {
view.setSelected(true);
} else if (state.equals("disabled")) {
view.setEnabled(false);
if (sStateDescription.containsKey(state)) {
updateViewContentDescription(view);
}
}
}
private void updateViewContentDescription(@Nonnull T view) {
final String accessibilityLabel = (String) view.getTag(R.id.accessibility_label);
final ReadableArray accessibilityStates = (ReadableArray) view.getTag(R.id.accessibility_states);
final String accessibilityHint = (String) view.getTag(R.id.accessibility_hint);
StringBuilder contentDescription = new StringBuilder();
if (accessibilityLabel != null) {
contentDescription.append(accessibilityLabel + ", ");
}
if (accessibilityStates != null) {
for (int i = 0; i < accessibilityStates.size(); i++) {
String state = accessibilityStates.getString(i);
if (sStateDescription.containsKey(state)) {
contentDescription.append(view.getContext().getString(sStateDescription.get(state)) + ", ");
}
}
}
if (accessibilityHint != null) {
contentDescription.append(accessibilityHint + ", ");
}
if (contentDescription.length() > 0) {
view.setContentDescription(contentDescription.toString());
}
}
@ReactProp(name = PROP_IMPORTANT_FOR_ACCESSIBILITY)
public void setImportantForAccessibility(@Nonnull T view, @Nullable String importantForAccessibility) {
if (importantForAccessibility == null || importantForAccessibility.equals("auto")) {

View File

@@ -15,4 +15,10 @@
<!--tag is used to store accessibilityRole tag-->
<item type="id" name="accessibility_role"/>
<!--tag is used to store accessibilityStates tag-->
<item type="id" name="accessibility_states"/>
<!--tag is used to store accessibilityLabel tag-->
<item type="id" name="accessibility_label"/>
</resources>

View File

@@ -2,26 +2,98 @@
<resources>
<string
name="link_description"
translatable="false"
translatable="true"
>Link</string>
<string
name="search_description"
translatable="false"
translatable="true"
>Search Field</string>
<string
name="image_description"
translatable="false"
translatable="true"
>Image</string>
<string
name="image_button_description"
translatable="false"
name="imagebutton_description"
translatable="true"
>Button, Image</string>
<string
name="adjustable_description"
translatable="false"
>Adjustable</string>
<string
name="header_description"
translatable="false"
>Heading</string>
<string
name="alert_description"
translatable="true"
>Alert</string>
<string
name="combobox_description"
translatable="true"
>Combo Box</string>
<string
name="menu_description"
translatable="true"
>Menu</string>
<string
name="menubar_description"
translatable="true"
>Menu Bar</string>
<string
name="menuitem_description"
translatable="true"
>Menu Item</string>
<string
name="progressbar_description"
translatable="true"
>Progress Bar</string>
<string
name="radiogroup_description"
translatable="true"
>Radio Group</string>
<string
name="scrollbar_description"
translatable="true"
>Scroll Bar</string>
<string
name="spinbutton_description"
translatable="true"
>Spin Button</string>
<string
name="rn_tab_description"
translatable="true"
>Tab</string>
<string
name="tablist_description"
translatable="true"
>Tab List</string>
<string
name="timer_description"
translatable="true"
>Timer</string>
<string
name="toolbar_description"
translatable="true"
>Tool Bar</string>
<string
name="summary_description"
translatable="true"
>Summary</string>
<string
name="state_busy_description"
translatable="true"
>busy</string>
<string
name="state_expanded_description"
translatable="true"
>expanded</string>
<string
name="state_collapsed_description"
translatable="true"
>collapsed</string>
<string
name="state_on_description"
translatable="true"
>on</string>
<string
name="state_off_description"
translatable="true"
>off</string>
</resources>