Skip to content

Commit baa66f6

Browse files
xuelgongfacebook-github-bot
authored andcommitted
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
1 parent 80857f2 commit baa66f6

File tree

6 files changed

+138
-31
lines changed

6 files changed

+138
-31
lines changed

RNTester/js/examples/Accessibility/AccessibilityExample.js

+126-31
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,30 @@ const React = require('react');
1313
const {
1414
AccessibilityInfo,
1515
Button,
16+
Image,
1617
Text,
1718
View,
1819
TouchableOpacity,
1920
TouchableWithoutFeedback,
2021
Alert,
21-
UIManager,
22-
findNodeHandle,
23-
Platform,
22+
StyleSheet,
2423
} = require('react-native');
2524

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

27+
const checkImageSource = require('./check.png');
28+
const uncheckImageSource = require('./uncheck.png');
29+
const mixedCheckboxImageSource = require('./mixed.png');
30+
31+
const styles = StyleSheet.create({
32+
image: {
33+
width: 20,
34+
height: 20,
35+
resizeMode: 'contain',
36+
marginRight: 10,
37+
},
38+
});
39+
2840
class AccessibilityExample extends React.Component {
2941
render() {
3042
return (
@@ -161,13 +173,6 @@ class CheckboxExample extends React.Component {
161173
this.setState({
162174
checkboxState: checkboxState,
163175
});
164-
165-
if (Platform.OS === 'android') {
166-
UIManager.sendAccessibilityEvent(
167-
findNodeHandle(this),
168-
UIManager.AccessibilityEventTypes.typeViewClicked,
169-
);
170-
}
171176
};
172177

173178
render() {
@@ -195,13 +200,6 @@ class SwitchExample extends React.Component {
195200
this.setState({
196201
switchState: switchState,
197202
});
198-
199-
if (Platform.OS === 'android') {
200-
UIManager.sendAccessibilityEvent(
201-
findNodeHandle(this),
202-
UIManager.AccessibilityEventTypes.typeViewClicked,
203-
);
204-
}
205203
};
206204

207205
render() {
@@ -252,13 +250,6 @@ class SelectionExample extends React.Component {
252250
isSelected: !this.state.isSelected,
253251
});
254252
}
255-
256-
if (Platform.OS === 'android') {
257-
UIManager.sendAccessibilityEvent(
258-
findNodeHandle(this.selectableElement.current),
259-
UIManager.AccessibilityEventTypes.typeViewClicked,
260-
);
261-
}
262253
}}
263254
accessibilityLabel="element 19"
264255
accessibilityState={{
@@ -292,13 +283,6 @@ class ExpandableElementExample extends React.Component {
292283
this.setState({
293284
expandState: expandState,
294285
});
295-
296-
if (Platform.OS === 'android') {
297-
UIManager.sendAccessibilityEvent(
298-
findNodeHandle(this),
299-
UIManager.AccessibilityEventTypes.typeViewClicked,
300-
);
301-
}
302286
};
303287

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

301+
class NestedCheckBox extends React.Component {
302+
state = {
303+
checkbox1: false,
304+
checkbox2: false,
305+
checkbox3: false,
306+
};
307+
308+
_onPress1 = () => {
309+
let checkbox1 = false;
310+
if (this.state.checkbox1 === false) {
311+
checkbox1 = true;
312+
} else if (this.state.checkbox1 === 'mixed') {
313+
checkbox1 = false;
314+
} else {
315+
checkbox1 = false;
316+
}
317+
setTimeout(() => {
318+
this.setState({
319+
checkbox1: checkbox1,
320+
checkbox2: checkbox1,
321+
checkbox3: checkbox1,
322+
});
323+
}, 2000);
324+
};
325+
326+
_onPress2 = () => {
327+
const checkbox2 = !this.state.checkbox2;
328+
329+
this.setState({
330+
checkbox2: checkbox2,
331+
checkbox1:
332+
checkbox2 && this.state.checkbox3
333+
? true
334+
: checkbox2 || this.state.checkbox3
335+
? 'mixed'
336+
: false,
337+
});
338+
};
339+
340+
_onPress3 = () => {
341+
const checkbox3 = !this.state.checkbox3;
342+
343+
this.setState({
344+
checkbox3: checkbox3,
345+
checkbox1:
346+
this.state.checkbox2 && checkbox3
347+
? true
348+
: this.state.checkbox2 || checkbox3
349+
? 'mixed'
350+
: false,
351+
});
352+
};
353+
354+
render() {
355+
return (
356+
<View>
357+
<TouchableOpacity
358+
style={{flex: 1, flexDirection: 'row'}}
359+
onPress={this._onPress1}
360+
accessibilityLabel="Meat"
361+
accessibilityHint="State changes in 2 seconds after clicking."
362+
accessibilityRole="checkbox"
363+
accessibilityState={{checked: this.state.checkbox1}}>
364+
<Image
365+
style={styles.image}
366+
source={
367+
this.state.checkbox1 === 'mixed'
368+
? mixedCheckboxImageSource
369+
: this.state.checkbox1
370+
? checkImageSource
371+
: uncheckImageSource
372+
}
373+
/>
374+
<Text>Meat</Text>
375+
</TouchableOpacity>
376+
<TouchableOpacity
377+
style={{flex: 1, flexDirection: 'row'}}
378+
onPress={this._onPress2}
379+
accessibilityLabel="Beef"
380+
accessibilityRole="checkbox"
381+
accessibilityState={{checked: this.state.checkbox2}}>
382+
<Image
383+
style={styles.image}
384+
source={
385+
this.state.checkbox2 ? checkImageSource : uncheckImageSource
386+
}
387+
/>
388+
<Text>Beef</Text>
389+
</TouchableOpacity>
390+
<TouchableOpacity
391+
style={{flex: 1, flexDirection: 'row'}}
392+
onPress={this._onPress3}
393+
accessibilityLabel="Bacon"
394+
accessibilityRole="checkbox"
395+
accessibilityState={{checked: this.state.checkbox3}}>
396+
<Image
397+
style={styles.image}
398+
source={
399+
this.state.checkbox3 ? checkImageSource : uncheckImageSource
400+
}
401+
/>
402+
<Text>Bacon</Text>
403+
</TouchableOpacity>
404+
</View>
405+
);
406+
}
407+
}
408+
317409
class AccessibilityRoleAndStateExample extends React.Component<{}> {
318410
render() {
319411
return (
@@ -412,6 +504,9 @@ class AccessibilityRoleAndStateExample extends React.Component<{}> {
412504
</View>
413505
<ExpandableElementExample />
414506
<SelectionExample />
507+
<RNTesterBlock title="Nested checkbox with delayed state change">
508+
<NestedCheckBox />
509+
</RNTesterBlock>
415510
</View>
416511
);
417512
}
25.3 KB
Loading
18.6 KB
Loading
Loading

React/Views/RCTViewManager.m

+4
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,13 @@ - (RCTShadowView *)shadowView
206206
}
207207
if (newState.count > 0) {
208208
view.reactAccessibilityElement.accessibilityState = newState;
209+
// Post a layout change notification to make sure VoiceOver get notified for the state
210+
// changes that don't happen upon users' click.
211+
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
209212
} else {
210213
view.reactAccessibilityElement.accessibilityState = nil;
211214
}
215+
212216
}
213217

214218
RCT_CUSTOM_VIEW_PROPERTY(nativeID, NSString *, RCTView)

ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java

+8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import android.text.TextUtils;
1010
import android.view.View;
1111
import android.view.ViewParent;
12+
import android.view.accessibility.AccessibilityEvent;
1213
import androidx.annotation.NonNull;
1314
import androidx.annotation.Nullable;
1415
import androidx.core.view.ViewCompat;
@@ -170,6 +171,13 @@ public void setViewState(@NonNull T view, @Nullable ReadableMap accessibilitySta
170171
&& accessibilityState.getType(STATE_CHECKED) == ReadableType.String)) {
171172
updateViewContentDescription(view);
172173
break;
174+
} else if (view.isAccessibilityFocused()) {
175+
// Internally Talkback ONLY uses TYPE_VIEW_CLICKED for "checked" and
176+
// "selected" announcements. Send a click event to make sure Talkback
177+
// get notified for the state changes that don't happen upon users' click.
178+
// For the state changes that happens immediately, Talkback will skip
179+
// the duplicated click event.
180+
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
173181
}
174182
}
175183
}

0 commit comments

Comments
 (0)