Skip to content

Commit

Permalink
Announce accessibility state changes happening in the background (fac…
Browse files Browse the repository at this point in the history
…ebook#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: facebook#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 baa66f6
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 31 deletions.
157 changes: 126 additions & 31 deletions RNTester/js/examples/Accessibility/AccessibilityExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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={{
Expand Down Expand Up @@ -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() {
Expand All @@ -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 (
Expand Down Expand Up @@ -412,6 +504,9 @@ class AccessibilityRoleAndStateExample extends React.Component<{}> {
</View>
<ExpandableElementExample />
<SelectionExample />
<RNTesterBlock title="Nested checkbox with delayed state change">
<NestedCheckBox />
</RNTesterBlock>
</View>
);
}
Expand Down
Binary file added RNTester/js/examples/Accessibility/check.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added RNTester/js/examples/Accessibility/mixed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added RNTester/js/examples/Accessibility/uncheck.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions React/Views/RCTViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
}
Expand Down

0 comments on commit baa66f6

Please sign in to comment.