Skip to content
Permalink
Browse files
Announce accessibility state changes happening in the background (#26624
)

Summary:
Currently the react native framework doesn't handle the accessibility state changes of the focused item that happen not upon double tapping. Screen reader doesn't get notified when the state of the focused item changes in the background.
To fix this problem, post a layout change notification for every state changes on iOS.
On Android, send a click event whenever state "checked", "selected" or "disabled" changes. In the case that such states changes upon user's clicking, the duplicated click event will be skipped by Talkback.

## Changelog:
[General][Fixed] - Announce accessibility state changes happening in the background
Pull Request resolved: #26624

Test Plan: Add a nested checkbox example which state changes after a delay in the AccessibilityExample.

Differential Revision: D17903205

Pulled By: cpojer

fbshipit-source-id: 9245ee0b79936cf11b408b52d45c59ba3415b9f9
  • Loading branch information
xuelgong authored and facebook-github-bot committed Oct 14, 2019
1 parent 80857f2 commit baa66f63d8af2b772dea8ff8eda50eba264c3faf
Showing 6 changed files with 138 additions and 31 deletions.
@@ -13,18 +13,30 @@ const React = require('react');
const {
AccessibilityInfo,
Button,
Image,
Text,
View,
TouchableOpacity,
TouchableWithoutFeedback,
Alert,
UIManager,
findNodeHandle,
Platform,
StyleSheet,
} = require('react-native');

const RNTesterBlock = require('../../components/RNTesterBlock');

const checkImageSource = require('./check.png');
const uncheckImageSource = require('./uncheck.png');
const mixedCheckboxImageSource = require('./mixed.png');

const styles = StyleSheet.create({
image: {
width: 20,
height: 20,
resizeMode: 'contain',
marginRight: 10,
},
});

class AccessibilityExample extends React.Component {
render() {
return (
@@ -161,13 +173,6 @@ class CheckboxExample extends React.Component {
this.setState({
checkboxState: checkboxState,
});

if (Platform.OS === 'android') {
UIManager.sendAccessibilityEvent(
findNodeHandle(this),
UIManager.AccessibilityEventTypes.typeViewClicked,
);
}
};

render() {
@@ -195,13 +200,6 @@ class SwitchExample extends React.Component {
this.setState({
switchState: switchState,
});

if (Platform.OS === 'android') {
UIManager.sendAccessibilityEvent(
findNodeHandle(this),
UIManager.AccessibilityEventTypes.typeViewClicked,
);
}
};

render() {
@@ -252,13 +250,6 @@ class SelectionExample extends React.Component {
isSelected: !this.state.isSelected,
});
}

if (Platform.OS === 'android') {
UIManager.sendAccessibilityEvent(
findNodeHandle(this.selectableElement.current),
UIManager.AccessibilityEventTypes.typeViewClicked,
);
}
}}
accessibilityLabel="element 19"
accessibilityState={{
@@ -292,13 +283,6 @@ class ExpandableElementExample extends React.Component {
this.setState({
expandState: expandState,
});

if (Platform.OS === 'android') {
UIManager.sendAccessibilityEvent(
findNodeHandle(this),
UIManager.AccessibilityEventTypes.typeViewClicked,
);
}
};

render() {
@@ -314,6 +298,114 @@ class ExpandableElementExample extends React.Component {
}
}

class NestedCheckBox extends React.Component {
state = {
checkbox1: false,
checkbox2: false,
checkbox3: false,
};

_onPress1 = () => {
let checkbox1 = false;
if (this.state.checkbox1 === false) {
checkbox1 = true;
} else if (this.state.checkbox1 === 'mixed') {
checkbox1 = false;
} else {
checkbox1 = false;
}
setTimeout(() => {
this.setState({
checkbox1: checkbox1,
checkbox2: checkbox1,
checkbox3: checkbox1,
});
}, 2000);
};

_onPress2 = () => {
const checkbox2 = !this.state.checkbox2;

this.setState({
checkbox2: checkbox2,
checkbox1:
checkbox2 && this.state.checkbox3
? true
: checkbox2 || this.state.checkbox3
? 'mixed'
: false,
});
};

_onPress3 = () => {
const checkbox3 = !this.state.checkbox3;

this.setState({
checkbox3: checkbox3,
checkbox1:
this.state.checkbox2 && checkbox3
? true
: this.state.checkbox2 || checkbox3
? 'mixed'
: false,
});
};

render() {
return (
<View>
<TouchableOpacity
style={{flex: 1, flexDirection: 'row'}}
onPress={this._onPress1}
accessibilityLabel="Meat"
accessibilityHint="State changes in 2 seconds after clicking."
accessibilityRole="checkbox"
accessibilityState={{checked: this.state.checkbox1}}>
<Image
style={styles.image}
source={
this.state.checkbox1 === 'mixed'
? mixedCheckboxImageSource
: this.state.checkbox1
? checkImageSource
: uncheckImageSource
}
/>
<Text>Meat</Text>
</TouchableOpacity>
<TouchableOpacity
style={{flex: 1, flexDirection: 'row'}}
onPress={this._onPress2}
accessibilityLabel="Beef"
accessibilityRole="checkbox"
accessibilityState={{checked: this.state.checkbox2}}>
<Image
style={styles.image}
source={
this.state.checkbox2 ? checkImageSource : uncheckImageSource
}
/>
<Text>Beef</Text>
</TouchableOpacity>
<TouchableOpacity
style={{flex: 1, flexDirection: 'row'}}
onPress={this._onPress3}
accessibilityLabel="Bacon"
accessibilityRole="checkbox"
accessibilityState={{checked: this.state.checkbox3}}>
<Image
style={styles.image}
source={
this.state.checkbox3 ? checkImageSource : uncheckImageSource
}
/>
<Text>Bacon</Text>
</TouchableOpacity>
</View>
);
}
}

class AccessibilityRoleAndStateExample extends React.Component<{}> {
render() {
return (
@@ -412,6 +504,9 @@ class AccessibilityRoleAndStateExample extends React.Component<{}> {
</View>
<ExpandableElementExample />
<SelectionExample />
<RNTesterBlock title="Nested checkbox with delayed state change">
<NestedCheckBox />
</RNTesterBlock>
</View>
);
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -206,9 +206,13 @@ - (RCTShadowView *)shadowView
}
if (newState.count > 0) {
view.reactAccessibilityElement.accessibilityState = newState;
// Post a layout change notification to make sure VoiceOver get notified for the state
// changes that don't happen upon users' click.
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
} else {
view.reactAccessibilityElement.accessibilityState = nil;
}

}

RCT_CUSTOM_VIEW_PROPERTY(nativeID, NSString *, RCTView)
@@ -9,6 +9,7 @@
import android.text.TextUtils;
import android.view.View;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
@@ -170,6 +171,13 @@ public void setViewState(@NonNull T view, @Nullable ReadableMap accessibilitySta
&& accessibilityState.getType(STATE_CHECKED) == ReadableType.String)) {
updateViewContentDescription(view);
break;
} else if (view.isAccessibilityFocused()) {
// Internally Talkback ONLY uses TYPE_VIEW_CLICKED for "checked" and
// "selected" announcements. Send a click event to make sure Talkback
// get notified for the state changes that don't happen upon users' click.
// For the state changes that happens immediately, Talkback will skip
// the duplicated click event.
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
}
}
}

0 comments on commit baa66f6

Please sign in to comment.