Skip to content

Commit 7ee2acc

Browse files
fabOnReactfacebook-github-bot
authored andcommitted
Selected State does not annonce when TextInput Component selected (#31144)
Summary: This issue fixes #30955 and is a follow up to pr #24608 which added the basic Accessibility functionalities to React Native. TextInput should announce "selected" to the user when screenreader focused. The focus is moved to the TextInput by navigating with the screenreader to the TextInput. This PR adds call to View#setSelected in BaseViewManager https://developer.android.com/reference/android/view/View#setSelected(boolean) The View#setSelected method definition https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/view/View.java ```java /** * Changes the selection state of this view. A view can be selected or not. * Note that selection is not the same as focus. Views are typically * selected in the context of an AdapterView like ListView or GridView; * the selected view is the view that is highlighted. * * param selected true if the view must be selected, false otherwise */ public void setSelected(boolean selected) { if (((mPrivateFlags & PFLAG_SELECTED) != 0) != selected) { // ... hidden logic if (selected) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); } // ... hidden logic } } ``` VoiceOver and TalkBack was tested with video samples included below. ## Changelog <!-- Help reviewers and the release process by writing your own changelog entry. For an example, see: https://github.com/facebook/react-native/wiki/Changelog --> [Android] [Fixed] - Fix Selected State does not announce when TextInput Component selected on Android Pull Request resolved: #31144 Test Plan: **<details><summary>CLICK TO OPEN TESTS RESULTS</summary>** <p> **ENABLE THE AUDIO** to hear the TalkBack announcing **SELECTED** when the user taps on the TextInput ```javascript <TextInput accessibilityLabel="element 20" accessibilityState={{ selected: true, }} /> ``` | selected is true | |:-------------------------:| | <video src="https://user-images.githubusercontent.com/24992535/111652826-afc4f000-8807-11eb-9c79-8c51d7bf455b.mp4" width="700" height="" /> | ```javascript <TextInput accessibilityLabel="element 20" accessibilityState={{ selected: false, }} /> ``` | selected is false | |:-------------------------:| | <video src="https://user-images.githubusercontent.com/24992535/111652919-c10dfc80-8807-11eb-8244-83db6c327bcd.mp4" width="700" height="" /> | The functionality does not present issues on iOS | iOS testing | |:-------------------------:| | <video src="https://user-images.githubusercontent.com/24992535/111647656-f401c180-8802-11eb-9fa9-a4c211cf1665.mp4" width="400" height="" /> | </p> </details> </p> </details> Reviewed By: blavalla Differential Revision: D27306166 Pulled By: kacieb fbshipit-source-id: 1b3cb37b2d0875cf53f6f1bff4bf095a877b2f0e
1 parent e9765a7 commit 7ee2acc

File tree

3 files changed

+86
-13
lines changed

3 files changed

+86
-13
lines changed

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,12 @@ public void setViewState(@NonNull T view, @Nullable ReadableMap accessibilitySta
166166
if (accessibilityState == null) {
167167
return;
168168
}
169+
if (accessibilityState.hasKey("selected")) {
170+
view.setSelected(accessibilityState.getBoolean("selected"));
171+
} else {
172+
view.setSelected(false);
173+
}
169174
view.setTag(R.id.accessibility_state, accessibilityState);
170-
view.setSelected(false);
171175
view.setEnabled(true);
172176

173177
// For states which don't have corresponding methods in

ReactAndroid/src/test/java/com/facebook/react/uimanager/BaseViewManagerTest.java

+32
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,49 @@
1010
import static org.fest.assertions.api.Assertions.assertThat;
1111

1212
import com.facebook.react.R;
13+
import com.facebook.react.bridge.Arguments;
14+
import com.facebook.react.bridge.JavaOnlyMap;
15+
import com.facebook.react.bridge.WritableMap;
1316
import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole;
1417
import com.facebook.react.views.view.ReactViewGroup;
1518
import com.facebook.react.views.view.ReactViewManager;
1619
import java.util.Locale;
1720
import org.junit.Before;
21+
import org.junit.Rule;
1822
import org.junit.Test;
1923
import org.junit.runner.RunWith;
24+
import org.mockito.invocation.InvocationOnMock;
25+
import org.mockito.stubbing.Answer;
26+
import org.powermock.api.mockito.PowerMockito;
27+
import org.powermock.core.classloader.annotations.PowerMockIgnore;
28+
import org.powermock.core.classloader.annotations.PrepareForTest;
29+
import org.powermock.modules.junit4.rule.PowerMockRule;
2030
import org.robolectric.RobolectricTestRunner;
2131
import org.robolectric.RuntimeEnvironment;
2232

33+
@PrepareForTest({Arguments.class})
2334
@RunWith(RobolectricTestRunner.class)
35+
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "androidx.*", "android.*"})
2436
public class BaseViewManagerTest {
2537

38+
@Rule public PowerMockRule rule = new PowerMockRule();
39+
2640
private BaseViewManager mViewManager;
2741
private ReactViewGroup mView;
2842

2943
@Before
3044
public void setUp() {
3145
mViewManager = new ReactViewManager();
3246
mView = new ReactViewGroup(RuntimeEnvironment.application);
47+
PowerMockito.mockStatic(Arguments.class);
48+
PowerMockito.when(Arguments.createMap())
49+
.thenAnswer(
50+
new Answer<Object>() {
51+
@Override
52+
public Object answer(InvocationOnMock invocation) throws Throwable {
53+
return new JavaOnlyMap();
54+
}
55+
});
3356
}
3457

3558
@Test
@@ -44,4 +67,13 @@ public void testAccessibilityRoleTurkish() {
4467
mViewManager.setAccessibilityRole(mView, "image");
4568
assertThat(mView.getTag(R.id.accessibility_role)).isEqualTo(AccessibilityRole.IMAGE);
4669
}
70+
71+
@Test
72+
public void testAccessibilityStateSelected() {
73+
WritableMap accessibilityState = Arguments.createMap();
74+
accessibilityState.putBoolean("selected", true);
75+
mViewManager.setViewState(mView, accessibilityState);
76+
assertThat(mView.getTag(R.id.accessibility_state)).isEqualTo(accessibilityState);
77+
assertThat(mView.isSelected()).isEqualTo(true);
78+
}
4779
}

packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js

+49-12
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
const React = require('react');
1313
const {
1414
AccessibilityInfo,
15+
TextInput,
1516
Button,
1617
Image,
1718
Text,
@@ -31,12 +32,32 @@ const mixedCheckboxImageSource = require('./mixed.png');
3132
const {createRef} = require('react');
3233

3334
const styles = StyleSheet.create({
35+
default: {
36+
borderWidth: StyleSheet.hairlineWidth,
37+
borderColor: '#0f0f0f',
38+
flex: 1,
39+
fontSize: 13,
40+
padding: 4,
41+
},
42+
touchable: {
43+
backgroundColor: 'blue',
44+
borderColor: 'red',
45+
borderWidth: 1,
46+
borderRadius: 10,
47+
padding: 10,
48+
borderStyle: 'solid',
49+
},
3450
image: {
3551
width: 20,
3652
height: 20,
3753
resizeMode: 'contain',
3854
marginRight: 10,
3955
},
56+
containerAlignCenter: {
57+
display: 'flex',
58+
flexDirection: 'column',
59+
justifyContent: 'space-between',
60+
},
4061
});
4162

4263
class AccessibilityExample extends React.Component {
@@ -230,37 +251,53 @@ class SelectionExample extends React.Component {
230251
};
231252

232253
render() {
254+
const {isSelected, isEnabled} = this.state;
233255
let accessibilityHint = 'click me to select';
234-
if (this.state.isSelected) {
256+
if (isSelected) {
235257
accessibilityHint = 'click me to unselect';
236258
}
237-
if (!this.state.isEnabled) {
259+
if (!isEnabled) {
238260
accessibilityHint = 'use the button on the right to enable selection';
239261
}
240-
let buttonTitle = this.state.isEnabled
241-
? 'Disable selection'
242-
: 'Enable selection';
243-
262+
let buttonTitle = isEnabled ? 'Disable selection' : 'Enable selection';
263+
const touchableHint = ` (touching the TouchableOpacity will ${
264+
isSelected ? 'disable' : 'enable'
265+
} accessibilityState.selected)`;
244266
return (
245-
<View style={{flex: 1, flexDirection: 'row'}}>
267+
<View style={styles.containerAlignCenter}>
246268
<TouchableOpacity
247269
ref={this.selectableElement}
248270
accessible={true}
249271
onPress={() => {
250-
if (this.state.isEnabled) {
272+
if (isEnabled) {
251273
this.setState({
252-
isSelected: !this.state.isSelected,
274+
isSelected: !isSelected,
253275
});
276+
} else {
277+
console.warn('selection is disabled, please enable selection.');
254278
}
255279
}}
256280
accessibilityLabel="element 19"
257281
accessibilityState={{
258-
selected: this.state.isSelected,
259-
disabled: !this.state.isEnabled,
282+
selected: isSelected,
283+
disabled: !isEnabled,
260284
}}
285+
style={styles.touchable}
261286
accessibilityHint={accessibilityHint}>
262-
<Text>Selectable element example</Text>
287+
<Text style={{color: 'white'}}>
288+
{`Selectable TouchableOpacity Example ${touchableHint}`}
289+
</Text>
263290
</TouchableOpacity>
291+
<TextInput
292+
accessibilityLabel="element 20"
293+
accessibilityState={{
294+
selected: isSelected,
295+
}}
296+
multiline={true}
297+
placeholder={`TextInput Example - ${
298+
isSelected ? 'enabled' : 'disabled'
299+
} selection`}
300+
/>
264301
<Button
265302
onPress={() => {
266303
this.setState({

0 commit comments

Comments
 (0)