diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js
index 61441eb5af2dee..3caf05f6bacb88 100644
--- a/Libraries/Components/TextInput/TextInput.js
+++ b/Libraries/Components/TextInput/TextInput.js
@@ -1104,6 +1104,7 @@ const TextInput = createReactClass({
accessibilityLabel={props.accessibilityLabel}
accessibilityRole={props.accessibilityRole}
accessibilityStates={props.accessibilityStates}
+ accessibilityState={props.accessibilityState}
nativeID={this.props.nativeID}
testID={props.testID}>
{textContainer}
@@ -1156,6 +1157,7 @@ const TextInput = createReactClass({
accessibilityLabel={props.accessibilityLabel}
accessibilityRole={props.accessibilityRole}
accessibilityStates={props.accessibilityStates}
+ accessibilityState={props.accessibilityState}
nativeID={this.props.nativeID}
testID={props.testID}>
{textContainer}
@@ -1213,6 +1215,7 @@ const TextInput = createReactClass({
accessibilityLabel={this.props.accessibilityLabel}
accessibilityRole={this.props.accessibilityRole}
accessibilityStates={this.props.accessibilityStates}
+ accessibilityState={this.props.accessibilityState}
nativeID={this.props.nativeID}
testID={this.props.testID}>
{textContainer}
diff --git a/Libraries/Components/Touchable/TouchableBounce.js b/Libraries/Components/Touchable/TouchableBounce.js
index 1010be60870a24..e9937dfd70a844 100644
--- a/Libraries/Components/Touchable/TouchableBounce.js
+++ b/Libraries/Components/Touchable/TouchableBounce.js
@@ -181,6 +181,7 @@ const TouchableBounce = ((createReactClass({
accessibilityHint={this.props.accessibilityHint}
accessibilityRole={this.props.accessibilityRole}
accessibilityStates={this.props.accessibilityStates}
+ accessibilityState={this.props.accessibilityState}
nativeID={this.props.nativeID}
testID={this.props.testID}
hitSlop={this.props.hitSlop}
diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js
index b9e552937b4949..5d733edb0a2c72 100644
--- a/Libraries/Components/Touchable/TouchableHighlight.js
+++ b/Libraries/Components/Touchable/TouchableHighlight.js
@@ -408,6 +408,7 @@ const TouchableHighlight = ((createReactClass({
accessibilityHint={this.props.accessibilityHint}
accessibilityRole={this.props.accessibilityRole}
accessibilityStates={this.props.accessibilityStates}
+ accessibilityState={this.props.accessibilityState}
style={StyleSheet.compose(
this.props.style,
this.state.extraUnderlayStyle,
diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js
index 1bd0d27cab21da..1f10ff412bd6e7 100644
--- a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js
+++ b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js
@@ -314,6 +314,7 @@ const TouchableNativeFeedback = createReactClass({
accessibilityLabel: this.props.accessibilityLabel,
accessibilityRole: this.props.accessibilityRole,
accessibilityStates: this.props.accessibilityStates,
+ accessibilityState: this.props.accessibilityState,
children,
testID: this.props.testID,
onLayout: this.props.onLayout,
diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js
index 235d47ff9af197..adc6439584aa33 100644
--- a/Libraries/Components/Touchable/TouchableOpacity.js
+++ b/Libraries/Components/Touchable/TouchableOpacity.js
@@ -311,6 +311,7 @@ const TouchableOpacity = ((createReactClass({
accessibilityHint={this.props.accessibilityHint}
accessibilityRole={this.props.accessibilityRole}
accessibilityStates={this.props.accessibilityStates}
+ accessibilityState={this.props.accessibilityState}
style={[this.props.style, {opacity: this.state.anim}]}
nativeID={this.props.nativeID}
testID={this.props.testID}
diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/Libraries/Components/Touchable/TouchableWithoutFeedback.js
index 887a9805309f63..6323cee0ed977e 100755
--- a/Libraries/Components/Touchable/TouchableWithoutFeedback.js
+++ b/Libraries/Components/Touchable/TouchableWithoutFeedback.js
@@ -21,7 +21,6 @@ const ensurePositiveDelayProps = require('./ensurePositiveDelayProps');
const {
DeprecatedAccessibilityRoles,
- DeprecatedAccessibilityStates,
} = require('../../DeprecatedPropTypes/DeprecatedViewAccessibility');
import type {
@@ -33,6 +32,7 @@ import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType';
import type {
AccessibilityRole,
AccessibilityStates,
+ AccessibilityState,
} from '../View/ViewAccessibility';
type TargetEvent = SyntheticEvent<
@@ -52,6 +52,7 @@ const OVERRIDE_PROPS = [
'accessibilityIgnoresInvertColors',
'accessibilityRole',
'accessibilityStates',
+ 'accessibilityState',
'hitSlop',
'nativeID',
'onBlur',
@@ -67,6 +68,7 @@ export type Props = $ReadOnly<{|
accessibilityIgnoresInvertColors?: ?boolean,
accessibilityRole?: ?AccessibilityRole,
accessibilityStates?: ?AccessibilityStates,
+ accessibilityState?: ?AccessibilityState,
children?: ?React.Node,
delayLongPress?: ?number,
delayPressIn?: ?number,
@@ -104,9 +106,8 @@ const TouchableWithoutFeedback = ((createReactClass({
accessibilityHint: PropTypes.string,
accessibilityIgnoresInvertColors: PropTypes.bool,
accessibilityRole: PropTypes.oneOf(DeprecatedAccessibilityRoles),
- accessibilityStates: PropTypes.arrayOf(
- PropTypes.oneOf(DeprecatedAccessibilityStates),
- ),
+ accessibilityStates: PropTypes.array,
+ accessibilityState: PropTypes.object,
/**
* When `accessible` is true (which is the default) this may be called when
* the OS-specific concept of "focus" occurs. Some platforms may not have
diff --git a/Libraries/Components/View/ReactNativeViewAttributes.js b/Libraries/Components/View/ReactNativeViewAttributes.js
index b4a3b69040e311..92837b4bed1820 100644
--- a/Libraries/Components/View/ReactNativeViewAttributes.js
+++ b/Libraries/Components/View/ReactNativeViewAttributes.js
@@ -22,6 +22,7 @@ ReactNativeViewAttributes.UIView = {
accessibilityLiveRegion: true,
accessibilityRole: true,
accessibilityStates: true,
+ accessibilityState: true,
accessibilityHint: true,
importantForAccessibility: true,
nativeID: true,
diff --git a/Libraries/Components/View/ReactNativeViewViewConfig.js b/Libraries/Components/View/ReactNativeViewViewConfig.js
index 0a65d0188203a3..d723f2797e7de1 100644
--- a/Libraries/Components/View/ReactNativeViewViewConfig.js
+++ b/Libraries/Components/View/ReactNativeViewViewConfig.js
@@ -110,6 +110,7 @@ const ReactNativeViewConfig = {
accessibilityLiveRegion: true,
accessibilityRole: true,
accessibilityStates: true,
+ accessibilityState: true,
accessibilityViewIsModal: true,
accessible: true,
alignContent: true,
diff --git a/Libraries/Components/View/ViewAccessibility.js b/Libraries/Components/View/ViewAccessibility.js
index 6f40d3d3c78cf7..711b57a77d68a7 100644
--- a/Libraries/Components/View/ViewAccessibility.js
+++ b/Libraries/Components/View/ViewAccessibility.js
@@ -66,3 +66,11 @@ export type AccessibilityActionEvent = SyntheticEvent<
actionName: string,
}>,
>;
+
+export type AccessibilityState = {
+ disabled?: boolean,
+ selected?: boolean,
+ checked?: ?boolean | 'mixed',
+ busy?: boolean,
+ expanded?: boolean,
+};
diff --git a/Libraries/Components/View/ViewPropTypes.js b/Libraries/Components/View/ViewPropTypes.js
index 861f105b455995..e3a53b8e84d79a 100644
--- a/Libraries/Components/View/ViewPropTypes.js
+++ b/Libraries/Components/View/ViewPropTypes.js
@@ -18,6 +18,7 @@ import type {TVViewProps} from '../AppleTV/TVViewPropTypes';
import type {
AccessibilityRole,
AccessibilityStates,
+ AccessibilityState,
AccessibilityActionEvent,
AccessibilityActionInfo,
} from './ViewAccessibility';
@@ -413,6 +414,7 @@ export type ViewProps = $ReadOnly<{|
* Indicates to accessibility services that UI Component is in a specific State.
*/
accessibilityStates?: ?AccessibilityStates,
+ accessibilityState?: ?AccessibilityState,
/**
* Provides an array of custom actions available for accessibility.
diff --git a/Libraries/DeprecatedPropTypes/DeprecatedViewPropTypes.js b/Libraries/DeprecatedPropTypes/DeprecatedViewPropTypes.js
index 01f035858cb607..343513b5cf3570 100644
--- a/Libraries/DeprecatedPropTypes/DeprecatedViewPropTypes.js
+++ b/Libraries/DeprecatedPropTypes/DeprecatedViewPropTypes.js
@@ -78,6 +78,7 @@ module.exports = {
accessibilityStates: PropTypes.arrayOf(
PropTypes.oneOf(DeprecatedAccessibilityStates),
),
+ accessibilityState: PropTypes.object,
/**
* Indicates to accessibility services whether the user should be notified
* when this view changes. Works for Android API >= 19 only.
diff --git a/Libraries/Text/TextProps.js b/Libraries/Text/TextProps.js
index 2e55b7c4fa69d6..21951fe145c7f0 100644
--- a/Libraries/Text/TextProps.js
+++ b/Libraries/Text/TextProps.js
@@ -20,6 +20,7 @@ import type {TextStyleProp} from '../StyleSheet/StyleSheet';
import type {
AccessibilityRole,
AccessibilityStates,
+ AccessibilityState,
} from '../Components/View/ViewAccessibility';
export type PressRetentionOffset = $ReadOnly<{|
@@ -43,6 +44,7 @@ export type TextProps = $ReadOnly<{|
accessibilityLabel?: ?Stringish,
accessibilityRole?: ?AccessibilityRole,
accessibilityStates?: ?AccessibilityStates,
+ accessibilityState?: ?AccessibilityState,
/**
* Whether font should be scaled down automatically.
diff --git a/RNTester/js/AccessibilityExample.js b/RNTester/js/AccessibilityExample.js
index c5ae027942e4d8..a12602c5291c59 100644
--- a/RNTester/js/AccessibilityExample.js
+++ b/RNTester/js/AccessibilityExample.js
@@ -109,7 +109,7 @@ class AccessibilityExample extends React.Component {
Alert.alert('Button has been pressed!')}
accessibilityRole="button"
- accessibilityStates={['disabled']}
+ accessibilityState={{disabled: true}}
disabled={true}>
@@ -122,7 +122,7 @@ class AccessibilityExample extends React.Component {
+ accessibilityState={{selected: true, disabled: true}}>
This view is selected and disabled.
@@ -132,7 +132,7 @@ class AccessibilityExample extends React.Component {
accessible={true}
accessibilityLabel="Accessibility label."
accessibilityRole="button"
- accessibilityStates={['selected']}
+ accessibilityState={{selected: true}}
accessibilityHint="Accessibility hint.">
Accessible view with label, hint, role, and state
@@ -144,12 +144,18 @@ class AccessibilityExample extends React.Component {
class CheckboxExample extends React.Component {
state = {
- checkboxState: 'checked',
+ checkboxState: true,
};
_onCheckboxPress = () => {
- const checkboxState =
- this.state.checkboxState === 'checked' ? 'unchecked' : 'checked';
+ let checkboxState = false;
+ if (this.state.checkboxState === false) {
+ checkboxState = 'mixed';
+ } else if (this.state.checkboxState === 'mixed') {
+ checkboxState = true;
+ } else {
+ checkboxState = false;
+ }
this.setState({
checkboxState: checkboxState,
@@ -169,7 +175,7 @@ class CheckboxExample extends React.Component {
onPress={this._onCheckboxPress}
accessibilityLabel="element 2"
accessibilityRole="checkbox"
- accessibilityStates={[this.state.checkboxState]}
+ accessibilityState={{checked: this.state.checkboxState}}
accessibilityHint="click me to change state">
Checkbox example
@@ -179,12 +185,11 @@ class CheckboxExample extends React.Component {
class SwitchExample extends React.Component {
state = {
- switchState: 'checked',
+ switchState: true,
};
_onSwitchToggle = () => {
- const switchState =
- this.state.switchState === 'checked' ? 'unchecked' : 'checked';
+ const switchState = !this.state.switchState;
this.setState({
switchState: switchState,
@@ -204,7 +209,7 @@ class SwitchExample extends React.Component {
onPress={this._onSwitchToggle}
accessibilityLabel="element 12"
accessibilityRole="switch"
- accessibilityStates={[this.state.switchState]}
+ accessibilityState={{checked: this.state.switchState}}
accessible={true}>
Switch example
@@ -224,14 +229,11 @@ class SelectionExample extends React.Component {
};
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
@@ -244,9 +246,11 @@ class SelectionExample extends React.Component {
ref={this.selectableElement}
accessible={true}
onPress={() => {
- this.setState({
- isSelected: !this.state.isSelected,
- });
+ if (this.state.isEnabled) {
+ this.setState({
+ isSelected: !this.state.isSelected,
+ });
+ }
if (Platform.OS === 'android') {
UIManager.sendAccessibilityEvent(
@@ -256,7 +260,10 @@ class SelectionExample extends React.Component {
}
}}
accessibilityLabel="element 19"
- accessibilityStates={accessibilityStates}
+ accessibilityState={{
+ selected: this.state.isSelected,
+ disabled: !this.state.isEnabled,
+ }}
accessibilityHint={accessibilityHint}>
Selectable element example
@@ -275,12 +282,11 @@ class SelectionExample extends React.Component {
class ExpandableElementExample extends React.Component {
state = {
- expandState: 'collapsed',
+ expandState: false,
};
_onElementPress = () => {
- const expandState =
- this.state.expandState === 'collapsed' ? 'expanded' : 'collapsed';
+ const expandState = !this.state.expandState;
this.setState({
expandState: expandState,
@@ -299,7 +305,7 @@ class ExpandableElementExample extends React.Component {
Expandable element example
@@ -399,7 +405,7 @@ class AccessibilityRoleAndStateExample extends React.Component<{}> {
State busy example
diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m
index 39cdcaa974ae94..8f550ab57cbdf3 100644
--- a/React/Views/RCTView.m
+++ b/React/Views/RCTView.m
@@ -222,6 +222,15 @@ - (NSString *)accessibilityValue
return @"0";
}
}
+ for (NSString *state in self.accessibilityState) {
+ id val = self.accessibilityState[state];
+ if (!val) {
+ continue;
+ }
+ if ([state isEqualToString:@"checked"] && [val isKindOfClass:[NSNumber class]]) {
+ return [val boolValue] ? @"1" : @"0";
+ }
+ }
}
NSMutableArray *valueComponents = [NSMutableArray new];
static NSDictionary *roleDescriptions = nil;
@@ -255,6 +264,7 @@ - (NSString *)accessibilityValue
@"busy" : @"busy",
@"expanded" : @"expanded",
@"collapsed" : @"collapsed",
+ @"mixed": @"mixed",
};
});
NSString *roleDescription = self.accessibilityRole ? roleDescriptions[self.accessibilityRole]: nil;
@@ -267,8 +277,27 @@ - (NSString *)accessibilityValue
[valueComponents addObject:stateDescription];
}
}
+ for (NSString *state in self.accessibilityState) {
+ id val = self.accessibilityState[state];
+ if (!val) {
+ continue;
+ }
+ if ([state isEqualToString:@"checked"]) {
+ if ([val isKindOfClass:[NSNumber class]]) {
+ [valueComponents addObject:stateDescriptions[[val boolValue] ? @"checked" : @"unchecked"]];
+ } else if ([val isKindOfClass:[NSString class]] && [val isEqualToString:@"mixed"]) {
+ [valueComponents addObject:stateDescriptions[@"mixed"]];
+ }
+ }
+ if ([state isEqualToString:@"expanded"] && [val isKindOfClass:[NSNumber class]]) {
+ [valueComponents addObject:stateDescriptions[[val boolValue] ? @"expanded" : @"collapsed"]];
+ }
+ if ([state isEqualToString:@"busy"] && [val isKindOfClass:[NSNumber class]] && [val boolValue]) {
+ [valueComponents addObject:stateDescriptions[@"busy"]];
+ }
+ }
if (valueComponents.count > 0) {
- return [valueComponents componentsJoinedByString:@", "];
+ return [valueComponents componentsJoinedByString:@", "];
}
return nil;
}
diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m
index 7c538ed2e200bc..bdc9ecccd84b87 100644
--- a/React/Views/RCTViewManager.m
+++ b/React/Views/RCTViewManager.m
@@ -206,6 +206,38 @@ - (RCTShadowView *)shadowView
}
}
+RCT_CUSTOM_VIEW_PROPERTY(accessibilityState, NSDictionary, RCTView)
+{
+ NSDictionary *state = json ? [RCTConvert NSDictionary:json] : nil;
+ NSMutableDictionary *newState = [[NSMutableDictionary alloc] init];
+
+ if (!state) {
+ return;
+ }
+
+ const UIAccessibilityTraits AccessibilityStatesMask = UIAccessibilityTraitNotEnabled | UIAccessibilityTraitSelected;
+ view.reactAccessibilityElement.accessibilityTraits = view.reactAccessibilityElement.accessibilityTraits & ~AccessibilityStatesMask;
+
+ for (NSString *s in state) {
+ id val = [state objectForKey:s];
+ if (!val) {
+ continue;
+ }
+ if ([s isEqualToString:@"selected"] && [val isKindOfClass:[NSNumber class]] && [val boolValue]) {
+ view.reactAccessibilityElement.accessibilityTraits |= UIAccessibilityTraitSelected;
+ } else if ([s isEqualToString:@"disabled"] && [val isKindOfClass:[NSNumber class]] && [val boolValue]) {
+ view.reactAccessibilityElement.accessibilityTraits |= UIAccessibilityTraitNotEnabled;
+ } else {
+ newState[s] = val;
+ }
+ }
+ if (newState.count > 0) {
+ view.reactAccessibilityElement.accessibilityState = newState;
+ } else {
+ view.reactAccessibilityElement.accessibilityState = nil;
+ }
+}
+
RCT_CUSTOM_VIEW_PROPERTY(nativeID, NSString *, RCTView)
{
view.nativeID = json ? [RCTConvert NSString:json] : defaultView.nativeID;
diff --git a/React/Views/UIView+React.h b/React/Views/UIView+React.h
index c4cab30b60a72f..6fbf47a0739299 100644
--- a/React/Views/UIView+React.h
+++ b/React/Views/UIView+React.h
@@ -118,6 +118,7 @@
*/
@property (nonatomic, copy) NSString *accessibilityRole;
@property (nonatomic, copy) NSArray *accessibilityStates;
+@property (nonatomic, copy) NSDictionary *accessibilityState;
/**
* Used in debugging to get a description of the view hierarchy rooted at
diff --git a/React/Views/UIView+React.m b/React/Views/UIView+React.m
index 019eebcc120f7f..d9c4d4190ab226 100644
--- a/React/Views/UIView+React.m
+++ b/React/Views/UIView+React.m
@@ -327,6 +327,16 @@ - (void)setAccessibilityStates:(NSArray *)accessibilityStates
objc_setAssociatedObject(self, @selector(accessibilityStates), accessibilityStates, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
+- (NSDictionary *)accessibilityState
+{
+ return objc_getAssociatedObject(self, _cmd);
+}
+
+- (void)setAccessibilityState:(NSDictionary *)accessibilityState
+{
+ objc_setAssociatedObject(self, @selector(accessibilityState), accessibilityState, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
#pragma mark - Debug
- (void)react_addRecursiveDescriptionToString:(NSMutableString *)string atLevel:(NSUInteger)level
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java
index 0d63d86cb1f15b..94bfdbf91fc47b 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java
@@ -6,14 +6,21 @@
package com.facebook.react.uimanager;
import android.graphics.Color;
+import android.text.TextUtils;
import android.view.View;
import android.view.ViewParent;
+
import androidx.core.view.ViewCompat;
+import java.util.ArrayList;
import java.util.HashMap;
import com.facebook.react.R;
+import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.ReadableMapKeySetIterator;
+import com.facebook.react.bridge.ReadableType;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ReactAccessibilityDelegate;
import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole;
@@ -21,6 +28,7 @@
import com.facebook.react.uimanager.util.ReactFindViewUtil;
import javax.annotation.Nonnull;
+import java.util.ArrayList;
import java.util.Map;
import javax.annotation.Nullable;
@@ -41,6 +49,7 @@ public abstract class BaseViewManager sStateDescription= new HashMap();
+ public static final HashMap sStateDescription = new HashMap();
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);
}
+ // State definition constants -- must match the definition in
+ // ViewAccessibility.js. These only include states for which there
+ // is no native support in android.
+
+ private static final String STATE_CHECKED = "checked"; // Special case for mixed state checkboxes
+ private static final String STATE_BUSY = "busy";
+ private static final String STATE_EXPANDED = "expanded";
+ private static final String STATE_MIXED = "mixed";
+
@ReactProp(name = PROP_BACKGROUND_COLOR, defaultInt = Color.TRANSPARENT, customType = "Color")
public void setBackgroundColor(@Nonnull T view, int backgroundColor) {
view.setBackgroundColor(backgroundColor);
@@ -169,27 +187,66 @@ public void setViewStates(@Nonnull T view, @Nullable ReadableArray accessibility
}
}
+ @ReactProp(name = PROP_ACCESSIBILITY_STATE)
+ public void setViewState(@Nonnull T view, @Nullable ReadableMap accessibilityState) {
+ if (accessibilityState == null) {
+ return;
+ }
+ view.setTag(R.id.accessibility_state, accessibilityState);
+ view.setSelected(false);
+ view.setEnabled(true);
+
+ // For states which don't have corresponding methods in
+ // AccessibilityNodeInfo, update the view's content description
+ // here
+
+ final ReadableMapKeySetIterator i = accessibilityState.keySetIterator();
+ while (i.hasNextKey()) {
+ final String state = i.nextKey();
+ if (state.equals(STATE_BUSY) || state.equals(STATE_EXPANDED) ||
+ (state.equals(STATE_CHECKED) && accessibilityState.getType(STATE_CHECKED) == ReadableType.String)) {
+ updateViewContentDescription(view);
+ break;
+ }
+ }
+ }
+
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 ReadableMap accessibilityState = (ReadableMap) view.getTag(R.id.accessibility_state);
final String accessibilityHint = (String) view.getTag(R.id.accessibility_hint);
- StringBuilder contentDescription = new StringBuilder();
+ final ArrayList contentDescription = new ArrayList();
if (accessibilityLabel != null) {
- contentDescription.append(accessibilityLabel + ", ");
+ contentDescription.add(accessibilityLabel);
}
if (accessibilityStates != null) {
for (int i = 0; i < accessibilityStates.size(); i++) {
- String state = accessibilityStates.getString(i);
+ final String state = accessibilityStates.getString(i);
if (sStateDescription.containsKey(state)) {
- contentDescription.append(view.getContext().getString(sStateDescription.get(state)) + ", ");
+ contentDescription.add(view.getContext().getString(sStateDescription.get(state)));
+ }
+ }
+ }
+ if (accessibilityState != null) {
+ final ReadableMapKeySetIterator i = accessibilityState.keySetIterator();
+ while (i.hasNextKey()) {
+ final String state = i.nextKey();
+ final Dynamic value = accessibilityState.getDynamic(state);
+ if (state.equals(STATE_CHECKED) && value.getType() == ReadableType.String && value.asString().equals(STATE_MIXED)) {
+ contentDescription.add(view.getContext().getString(R.string.state_mixed_description));
+ } else if (state.equals(STATE_BUSY) && value.getType() == ReadableType.Boolean && value.asBoolean()) {
+ contentDescription.add(view.getContext().getString(R.string.state_busy_description));
+ } else if (state.equals(STATE_EXPANDED) && value.getType() == ReadableType.Boolean) {
+ contentDescription.add(view.getContext().getString(value.asBoolean() ? R.string.state_expanded_description : R.string.state_collapsed_description));
}
}
}
if (accessibilityHint != null) {
- contentDescription.append(accessibilityHint + ", ");
+ contentDescription.add(accessibilityHint);
}
- if (contentDescription.length() > 0) {
- view.setContentDescription(contentDescription.toString());
+ if (contentDescription.size() > 0) {
+ view.setContentDescription(TextUtils.join(", ", contentDescription));
}
}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java
index 0924dacfc8e8cd..481f2e2821d669 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java
@@ -18,12 +18,16 @@
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat;
+import android.util.Log;
import android.view.View;
import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.ReadableMapKeySetIterator;
+import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.facebook.react.R;
@@ -39,6 +43,7 @@
public class ReactAccessibilityDelegate extends AccessibilityDelegateCompat {
+ private static final String TAG = "ReactAccessibilityDelegate";
private static int sCounter = 0x3f000000;
public static final HashMap sActionIdMap= new HashMap<>();
@@ -121,6 +126,12 @@ public static AccessibilityRole fromValue(@Nullable String value) {
private final HashMap mAccessibilityActionsMap;
+ // State constants for states which have analogs in AccessibilityNodeInfo
+
+ private static final String STATE_DISABLED = "disabled";
+ private static final String STATE_SELECTED = "selected";
+ private static final String STATE_CHECKED = "checked";
+
public ReactAccessibilityDelegate() {
super();
mAccessibilityActionsMap = new HashMap();
@@ -136,8 +147,12 @@ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCo
// states are changable.
final ReadableArray accessibilityStates = (ReadableArray) host.getTag(R.id.accessibility_states);
+ final ReadableMap accessibilityState = (ReadableMap) host.getTag(R.id.accessibility_state);
if (accessibilityStates != null) {
- setState(info, accessibilityStates, host.getContext());
+ setStates(info, accessibilityStates, host.getContext());
+ }
+ if (accessibilityState != null) {
+ setState(info, accessibilityState, host.getContext());
}
final ReadableArray accessibilityActions = (ReadableArray) host.getTag(R.id.accessibility_actions);
if (accessibilityActions != null) {
@@ -175,7 +190,7 @@ public boolean performAccessibilityAction(View host, int action, Bundle args) {
return super.performAccessibilityAction(host, action, args);
}
- public static void setState(AccessibilityNodeInfoCompat info, ReadableArray accessibilityStates, Context context) {
+ private static void setStates(AccessibilityNodeInfoCompat info, ReadableArray accessibilityStates, Context context) {
for (int i = 0; i < accessibilityStates.size(); i++) {
String state = accessibilityStates.getString(i);
switch (state) {
@@ -188,20 +203,38 @@ public static void setState(AccessibilityNodeInfoCompat info, ReadableArray acce
case "checked":
info.setCheckable(true);
info.setChecked(true);
- if (info.getClassName().equals("android.widget.Switch")) {
+ if (info.getClassName().equals(AccessibilityRole.getValue(AccessibilityRole.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")) {
+ if (info.getClassName().equals(AccessibilityRole.getValue(AccessibilityRole.SWITCH))) {
info.setText(context.getString(R.string.state_off_description));
}
break;
- case "hasPopup":
- info.setCanOpenPopup(true);
- break;
+ }
+ }
+ }
+
+ private static void setState(AccessibilityNodeInfoCompat info, ReadableMap accessibilityState, Context context) {
+ Log.d(TAG, "setState " + accessibilityState);
+ final ReadableMapKeySetIterator i = accessibilityState.keySetIterator();
+ while (i.hasNextKey()) {
+ final String state = i.nextKey();
+ final Dynamic value = accessibilityState.getDynamic(state);
+ if (state.equals(STATE_SELECTED) && value.getType() == ReadableType.Boolean) {
+ info.setSelected(value.asBoolean());
+ } else if (state.equals(STATE_DISABLED) && value.getType() == ReadableType.Boolean) {
+ info.setEnabled(!value.asBoolean());
+ } else if (state.equals(STATE_CHECKED) && value.getType() == ReadableType.Boolean) {
+ final boolean boolValue = value.asBoolean();
+ info.setCheckable(true);
+ info.setChecked(boolValue);
+ if (info.getClassName().equals(AccessibilityRole.getValue(AccessibilityRole.SWITCH))) {
+ info.setText(context.getString(boolValue ? R.string.state_on_description : R.string.state_off_description));
+ }
}
}
}
@@ -281,6 +314,7 @@ public static void setDelegate(final View view) {
if (!ViewCompat.hasAccessibilityDelegate(view)
&& (view.getTag(R.id.accessibility_role) != null ||
view.getTag(R.id.accessibility_states) != null ||
+ view.getTag(R.id.accessibility_state) != null ||
view.getTag(R.id.accessibility_actions) != null)) {
ViewCompat.setAccessibilityDelegate(view, new ReactAccessibilityDelegate());
}
diff --git a/ReactAndroid/src/main/res/views/uimanager/values/ids.xml b/ReactAndroid/src/main/res/views/uimanager/values/ids.xml
index 0ae5d684021781..6dce677f736dca 100644
--- a/ReactAndroid/src/main/res/views/uimanager/values/ids.xml
+++ b/ReactAndroid/src/main/res/views/uimanager/values/ids.xml
@@ -18,6 +18,9 @@
+
+
+
diff --git a/ReactAndroid/src/main/res/views/uimanager/values/strings_unlocalized.xml b/ReactAndroid/src/main/res/views/uimanager/values/strings_unlocalized.xml
index 7950f24a6a7b07..cd3cc03e0d6236 100644
--- a/ReactAndroid/src/main/res/views/uimanager/values/strings_unlocalized.xml
+++ b/ReactAndroid/src/main/res/views/uimanager/values/strings_unlocalized.xml
@@ -96,4 +96,8 @@
name="state_off_description"
translatable="false"
>off
+ mixed