mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-01-12 22:50:10 +08:00
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:
committed by
Facebook Github Bot
parent
421ffb05ae
commit
1aeac1c625
@@ -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',
|
||||
>;
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user