Skip to content

Commit

Permalink
New Accessibility states API. (facebook#24608)
Browse files Browse the repository at this point in the history
Summary:
As currently defined, accessibilityStates is an array of strings, which represents the state of an object. The array of strings notion doesn't well encapsulate how various states are related, nor enforce any level of correctness.

This PR converts accessibilityStates to an object with a specific definition. So, rather than:

<View
...
accessibilityStates={['unchecked']}>

We have:

<View
accessibilityStates={{'checked': false}}>

And specifically define the checked state to either take a boolean or the "mixed" string (to represent mixed checkboxes).

We feel this API is easier to understand an implement, and provides better semantic definition of the states themselves, and how states are related to one another.

## Changelog

[general] [change] - Convert accessibilityStates to an object instead of an array of strings.
Pull Request resolved: facebook#24608

Differential Revision: D15467980

Pulled By: cpojer

fbshipit-source-id: f0414c0ef6add3f10f7f551d323d82d978754278
  • Loading branch information
Marc Mulcahy authored and facebook-github-bot committed May 23, 2019
1 parent 04564a0 commit 099be9b
Show file tree
Hide file tree
Showing 21 changed files with 242 additions and 43 deletions.
3 changes: 3 additions & 0 deletions Libraries/Components/TextInput/TextInput.js
Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Expand Down
1 change: 1 addition & 0 deletions Libraries/Components/Touchable/TouchableBounce.js
Expand Up @@ -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}
Expand Down
1 change: 1 addition & 0 deletions Libraries/Components/Touchable/TouchableHighlight.js
Expand Up @@ -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,
Expand Down
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions Libraries/Components/Touchable/TouchableOpacity.js
Expand Up @@ -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}
Expand Down
9 changes: 5 additions & 4 deletions Libraries/Components/Touchable/TouchableWithoutFeedback.js
Expand Up @@ -21,7 +21,6 @@ const ensurePositiveDelayProps = require('./ensurePositiveDelayProps');

const {
DeprecatedAccessibilityRoles,
DeprecatedAccessibilityStates,
} = require('../../DeprecatedPropTypes/DeprecatedViewAccessibility');

import type {
Expand All @@ -33,6 +32,7 @@ import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType';
import type {
AccessibilityRole,
AccessibilityStates,
AccessibilityState,
} from '../View/ViewAccessibility';

type TargetEvent = SyntheticEvent<
Expand All @@ -52,6 +52,7 @@ const OVERRIDE_PROPS = [
'accessibilityIgnoresInvertColors',
'accessibilityRole',
'accessibilityStates',
'accessibilityState',
'hitSlop',
'nativeID',
'onBlur',
Expand All @@ -67,6 +68,7 @@ export type Props = $ReadOnly<{|
accessibilityIgnoresInvertColors?: ?boolean,
accessibilityRole?: ?AccessibilityRole,
accessibilityStates?: ?AccessibilityStates,
accessibilityState?: ?AccessibilityState,
children?: ?React.Node,
delayLongPress?: ?number,
delayPressIn?: ?number,
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Libraries/Components/View/ReactNativeViewAttributes.js
Expand Up @@ -22,6 +22,7 @@ ReactNativeViewAttributes.UIView = {
accessibilityLiveRegion: true,
accessibilityRole: true,
accessibilityStates: true,
accessibilityState: true,
accessibilityHint: true,
importantForAccessibility: true,
nativeID: true,
Expand Down
1 change: 1 addition & 0 deletions Libraries/Components/View/ReactNativeViewViewConfig.js
Expand Up @@ -110,6 +110,7 @@ const ReactNativeViewConfig = {
accessibilityLiveRegion: true,
accessibilityRole: true,
accessibilityStates: true,
accessibilityState: true,
accessibilityViewIsModal: true,
accessible: true,
alignContent: true,
Expand Down
8 changes: 8 additions & 0 deletions Libraries/Components/View/ViewAccessibility.js
Expand Up @@ -66,3 +66,11 @@ export type AccessibilityActionEvent = SyntheticEvent<
actionName: string,
}>,
>;

export type AccessibilityState = {
disabled?: boolean,
selected?: boolean,
checked?: ?boolean | 'mixed',
busy?: boolean,
expanded?: boolean,
};
2 changes: 2 additions & 0 deletions Libraries/Components/View/ViewPropTypes.js
Expand Up @@ -18,6 +18,7 @@ import type {TVViewProps} from '../AppleTV/TVViewPropTypes';
import type {
AccessibilityRole,
AccessibilityStates,
AccessibilityState,
AccessibilityActionEvent,
AccessibilityActionInfo,
} from './ViewAccessibility';
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions Libraries/DeprecatedPropTypes/DeprecatedViewPropTypes.js
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions Libraries/Text/TextProps.js
Expand Up @@ -20,6 +20,7 @@ import type {TextStyleProp} from '../StyleSheet/StyleSheet';
import type {
AccessibilityRole,
AccessibilityStates,
AccessibilityState,
} from '../Components/View/ViewAccessibility';

export type PressRetentionOffset = $ReadOnly<{|
Expand All @@ -43,6 +44,7 @@ export type TextProps = $ReadOnly<{|
accessibilityLabel?: ?Stringish,
accessibilityRole?: ?AccessibilityRole,
accessibilityStates?: ?AccessibilityStates,
accessibilityState?: ?AccessibilityState,

/**
* Whether font should be scaled down automatically.
Expand Down
52 changes: 29 additions & 23 deletions RNTester/js/AccessibilityExample.js
Expand Up @@ -109,7 +109,7 @@ class AccessibilityExample extends React.Component {
<TouchableOpacity
onPress={() => Alert.alert('Button has been pressed!')}
accessibilityRole="button"
accessibilityStates={['disabled']}
accessibilityState={{disabled: true}}
disabled={true}>
<View>
<Text>
Expand All @@ -122,7 +122,7 @@ class AccessibilityExample extends React.Component {
<RNTesterBlock title="View with multiple states">
<View
accessible={true}
accessibilityStates={['selected', 'disabled']}>
accessibilityState={{selected: true, disabled: true}}>
<Text>This view is selected and disabled.</Text>
</View>
</RNTesterBlock>
Expand All @@ -132,7 +132,7 @@ class AccessibilityExample extends React.Component {
accessible={true}
accessibilityLabel="Accessibility label."
accessibilityRole="button"
accessibilityStates={['selected']}
accessibilityState={{selected: true}}
accessibilityHint="Accessibility hint.">
<Text>Accessible view with label, hint, role, and state</Text>
</View>
Expand All @@ -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,
Expand All @@ -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">
<Text>Checkbox example</Text>
</TouchableOpacity>
Expand All @@ -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,
Expand All @@ -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}>
<Text>Switch example</Text>
</TouchableOpacity>
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -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}>
<Text>Selectable element example</Text>
</TouchableOpacity>
Expand All @@ -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,
Expand All @@ -299,7 +305,7 @@ class ExpandableElementExample extends React.Component {
<TouchableOpacity
onPress={this._onElementPress}
accessibilityLabel="element 18"
accessibilityStates={[this.state.expandState]}
accessibilityState={{expanded: this.state.expandState}}
accessibilityHint="click me to change state">
<Text>Expandable element example</Text>
</TouchableOpacity>
Expand Down Expand Up @@ -399,7 +405,7 @@ class AccessibilityRoleAndStateExample extends React.Component<{}> {
</View>
<View
accessibilityLabel="element 17"
accessibilityStates={['busy']}
accessibilityState={{busy: true}}
accessible={true}>
<Text>State busy example</Text>
</View>
Expand Down
31 changes: 30 additions & 1 deletion React/Views/RCTView.m
Expand Up @@ -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<NSString *, NSString *> *roleDescriptions = nil;
Expand Down Expand Up @@ -255,6 +264,7 @@ - (NSString *)accessibilityValue
@"busy" : @"busy",
@"expanded" : @"expanded",
@"collapsed" : @"collapsed",
@"mixed": @"mixed",
};
});
NSString *roleDescription = self.accessibilityRole ? roleDescriptions[self.accessibilityRole]: nil;
Expand All @@ -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;
}
Expand Down

0 comments on commit 099be9b

Please sign in to comment.