Skip to content

Commit

Permalink
Selected State does not annonce when TextInput Component selected (#3…
Browse files Browse the repository at this point in the history
…1144)

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
  • Loading branch information
fabOnReact authored and facebook-github-bot committed Apr 5, 2021
1 parent e9765a7 commit 7ee2acc
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 13 deletions.
Expand Up @@ -166,8 +166,12 @@ public void setViewState(@NonNull T view, @Nullable ReadableMap accessibilitySta
if (accessibilityState == null) {
return;
}
if (accessibilityState.hasKey("selected")) {
view.setSelected(accessibilityState.getBoolean("selected"));
} else {
view.setSelected(false);
}
view.setTag(R.id.accessibility_state, accessibilityState);
view.setSelected(false);
view.setEnabled(true);

// For states which don't have corresponding methods in
Expand Down
Expand Up @@ -10,26 +10,49 @@
import static org.fest.assertions.api.Assertions.assertThat;

import com.facebook.react.R;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole;
import com.facebook.react.views.view.ReactViewGroup;
import com.facebook.react.views.view.ReactViewManager;
import java.util.Locale;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.rule.PowerMockRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;

@PrepareForTest({Arguments.class})
@RunWith(RobolectricTestRunner.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "androidx.*", "android.*"})
public class BaseViewManagerTest {

@Rule public PowerMockRule rule = new PowerMockRule();

private BaseViewManager mViewManager;
private ReactViewGroup mView;

@Before
public void setUp() {
mViewManager = new ReactViewManager();
mView = new ReactViewGroup(RuntimeEnvironment.application);
PowerMockito.mockStatic(Arguments.class);
PowerMockito.when(Arguments.createMap())
.thenAnswer(
new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
return new JavaOnlyMap();
}
});
}

@Test
Expand All @@ -44,4 +67,13 @@ public void testAccessibilityRoleTurkish() {
mViewManager.setAccessibilityRole(mView, "image");
assertThat(mView.getTag(R.id.accessibility_role)).isEqualTo(AccessibilityRole.IMAGE);
}

@Test
public void testAccessibilityStateSelected() {
WritableMap accessibilityState = Arguments.createMap();
accessibilityState.putBoolean("selected", true);
mViewManager.setViewState(mView, accessibilityState);
assertThat(mView.getTag(R.id.accessibility_state)).isEqualTo(accessibilityState);
assertThat(mView.isSelected()).isEqualTo(true);
}
}
Expand Up @@ -12,6 +12,7 @@
const React = require('react');
const {
AccessibilityInfo,
TextInput,
Button,
Image,
Text,
Expand All @@ -31,12 +32,32 @@ const mixedCheckboxImageSource = require('./mixed.png');
const {createRef} = require('react');

const styles = StyleSheet.create({
default: {
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#0f0f0f',
flex: 1,
fontSize: 13,
padding: 4,
},
touchable: {
backgroundColor: 'blue',
borderColor: 'red',
borderWidth: 1,
borderRadius: 10,
padding: 10,
borderStyle: 'solid',
},
image: {
width: 20,
height: 20,
resizeMode: 'contain',
marginRight: 10,
},
containerAlignCenter: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
},
});

class AccessibilityExample extends React.Component {
Expand Down Expand Up @@ -230,37 +251,53 @@ class SelectionExample extends React.Component {
};

render() {
const {isSelected, isEnabled} = this.state;
let accessibilityHint = 'click me to select';
if (this.state.isSelected) {
if (isSelected) {
accessibilityHint = 'click me to unselect';
}
if (!this.state.isEnabled) {
if (!isEnabled) {
accessibilityHint = 'use the button on the right to enable selection';
}
let buttonTitle = this.state.isEnabled
? 'Disable selection'
: 'Enable selection';

let buttonTitle = isEnabled ? 'Disable selection' : 'Enable selection';
const touchableHint = ` (touching the TouchableOpacity will ${
isSelected ? 'disable' : 'enable'
} accessibilityState.selected)`;
return (
<View style={{flex: 1, flexDirection: 'row'}}>
<View style={styles.containerAlignCenter}>
<TouchableOpacity
ref={this.selectableElement}
accessible={true}
onPress={() => {
if (this.state.isEnabled) {
if (isEnabled) {
this.setState({
isSelected: !this.state.isSelected,
isSelected: !isSelected,
});
} else {
console.warn('selection is disabled, please enable selection.');
}
}}
accessibilityLabel="element 19"
accessibilityState={{
selected: this.state.isSelected,
disabled: !this.state.isEnabled,
selected: isSelected,
disabled: !isEnabled,
}}
style={styles.touchable}
accessibilityHint={accessibilityHint}>
<Text>Selectable element example</Text>
<Text style={{color: 'white'}}>
{`Selectable TouchableOpacity Example ${touchableHint}`}
</Text>
</TouchableOpacity>
<TextInput
accessibilityLabel="element 20"
accessibilityState={{
selected: isSelected,
}}
multiline={true}
placeholder={`TextInput Example - ${
isSelected ? 'enabled' : 'disabled'
} selection`}
/>
<Button
onPress={() => {
this.setState({
Expand Down

0 comments on commit 7ee2acc

Please sign in to comment.