Skip to content

Commit

Permalink
Android: using AccessibilityNodeInfo#addAction to announce Expandable…
Browse files Browse the repository at this point in the history
…/Collapsible State (#34353)

Summary:
>Expandable and Collapsible are unique in the Android Accessibility API, in that they are not represented as properties on the View or AccessibilityNodeInfo, but are only represented as AccessibilityActions on the AccessibilityNodeInfo. This means that Talkback determines whether or not a node is "expandable" or "collapsible", or potentially even both, by looking at the list of AccessibilityActions attached to the AccessibilityNodeInfo.

>When setting the accessibilityState's expandable property, it should correlate to adding an action of either AccessibilityNodeInfoCompat.ACTION_EXPAND or AccessibilityNodeInfoCompat.ACTION_COLLAPSE on the AccessibilityNodeInfo. This work should be done in the ReactAccessibilityDelegate class's

>Currently, this feature is being "faked" by appending to the contentDescription in the BaseViewManager class. This should be removed when this feature is implemented properly.

fixes #30841

## Changelog

[Android] [Fixed] - using AccessibilityNodeInfo#addAction to announce Expandable/Collapsible State

Pull Request resolved: #34353

Test Plan:
- On some components, the state expanded/collapsed is properly announced on focus, on some it is not.
- On some components only the expanded/collapsed state is announced, and not other component text.
- Upon change, state change is not always announced.
- The accessibilityState's "expanded" field does not seem to work on all element types (for example, it has no effect on 's).
- using accessibilityActions it is possible to add an action for expand/collapse, but these are treated as custom actions and must have their own label defined, rather than using Androids built in expand/collapse actions, which Talkback has predefined labels for.

https://snack.expo.io/0YOQfXFBi

Tests  15th August 2022:
- Paper [Tests](#34353 (comment))
- Fabric [Tests](#34353 (comment))

Tests 6th September 2022:
- [Button which keeps control of extended/collapsed state in JavaScript with onAccessibilityAction, accessibilityActions and accessibiltyState (Paper)](#34353 (comment))
- [TouchableWithoutFeedback keeps control of extended/collapsed state in Android Widget (Paper)](#34353 (comment))
- [TouchableWithoutFeedback keeps control of extended/collapsed state in Android Widget (Fabric)](#34353 (comment))
- [TouchableOpacity announces visible text and triggers expanded/collapsed with onPress and accessiblity menu (Fabric)](#34353 (comment))

Announcing state with custom actions on Fabric (FAIL).
The issue is not a regression from this PR, as documented in #34353 (comment). It will be fixed in a separate PR.

Reviewed By: NickGerleman

Differential Revision: D39893863

Pulled By: blavalla

fbshipit-source-id: f6af78b1839ba7d97eca052bd258faae00cbd27b
  • Loading branch information
fabOnReact authored and facebook-github-bot committed Nov 8, 2022
1 parent 8a847a3 commit 082a033
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ protected T prepareToRecycleView(@NonNull ThemedReactContext reactContext, T vie
view.setTag(R.id.accessibility_state, null);
view.setTag(R.id.accessibility_actions, null);
view.setTag(R.id.accessibility_value, null);
view.setTag(R.id.accessibility_state_expanded, null);

// This indirectly calls (and resets):
// setTranslationX
Expand Down Expand Up @@ -270,6 +271,9 @@ public void setViewState(@NonNull T view, @Nullable ReadableMap accessibilitySta
if (accessibilityState == null) {
return;
}
if (accessibilityState.hasKey("expanded")) {
view.setTag(R.id.accessibility_state_expanded, accessibilityState.getBoolean("expanded"));
}
if (accessibilityState.hasKey("selected")) {
boolean prevSelected = view.isSelected();
boolean nextSelected = accessibilityState.getBoolean("selected");
Expand Down Expand Up @@ -335,13 +339,6 @@ private void updateViewContentDescription(@NonNull T view) {
&& value.getType() == ReadableType.Boolean
&& value.asBoolean()) {
contentDescription.add(view.getContext().getString(R.string.state_busy_description));
} else if (state.equals(STATE_EXPANDED) && value.getType() == ReadableType.Boolean) {
contentDescription.add(
view.getContext()
.getString(
value.asBoolean()
? R.string.state_expanded_description
: R.string.state_collapsed_description));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
sActionIdMap.put("longpress", AccessibilityActionCompat.ACTION_LONG_CLICK.getId());
sActionIdMap.put("increment", AccessibilityActionCompat.ACTION_SCROLL_FORWARD.getId());
sActionIdMap.put("decrement", AccessibilityActionCompat.ACTION_SCROLL_BACKWARD.getId());
sActionIdMap.put("expand", AccessibilityActionCompat.ACTION_EXPAND.getId());
sActionIdMap.put("collapse", AccessibilityActionCompat.ACTION_COLLAPSE.getId());
}

private final View mView;
Expand Down Expand Up @@ -250,6 +252,14 @@ public void handleMessage(Message msg) {
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
if (host.getTag(R.id.accessibility_state_expanded) != null) {
final boolean accessibilityStateExpanded =
(boolean) host.getTag(R.id.accessibility_state_expanded);
info.addAction(
accessibilityStateExpanded
? AccessibilityNodeInfoCompat.ACTION_COLLAPSE
: AccessibilityNodeInfoCompat.ACTION_EXPAND);
}
final AccessibilityRole accessibilityRole =
(AccessibilityRole) host.getTag(R.id.accessibility_role);
final String accessibilityHint = (String) host.getTag(R.id.accessibility_hint);
Expand Down Expand Up @@ -380,6 +390,12 @@ public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event)

@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
if (action == AccessibilityNodeInfoCompat.ACTION_COLLAPSE) {
host.setTag(R.id.accessibility_state_expanded, false);
}
if (action == AccessibilityNodeInfoCompat.ACTION_EXPAND) {
host.setTag(R.id.accessibility_state_expanded, true);
}
if (mAccessibilityActionsMap.containsKey(action)) {
final WritableMap event = Arguments.createMap();
event.putString("actionName", mAccessibilityActionsMap.get(action));
Expand Down
5 changes: 4 additions & 1 deletion ReactAndroid/src/main/res/views/uimanager/values/ids.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
<!-- tag is used to store accessibilityState -->
<item type="id" name="accessibility_state"/>

<!-- tag is used to store accessibilityLabel tag-->
<!--tag is used to store accessibilityStateExpanded -->
<item type="id" name="accessibility_state_expanded"/>

<!--tag is used to store accessibilityLabel tag-->
<item type="id" name="accessibility_label"/>

<!-- tag is used to store accessibilityActions tag-->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ const styles = StyleSheet.create({
flexDirection: 'column',
justifyContent: 'space-between',
},
button: {
padding: 8,
borderWidth: 1,
borderColor: 'blue',
},
container: {
flex: 1,
},
Expand Down Expand Up @@ -1431,10 +1436,74 @@ function DisplayOptionStatusExample({
);
}

function AccessibilityExpandedExample(): React.Node {
const [expand, setExpanded] = React.useState(false);
const [pressed, setPressed] = React.useState(false);
const expandAction = {name: 'expand'};
const collapseAction = {name: 'collapse'};
return (
<>
<RNTesterBlock title="Collapse/Expanded state change (Paper)">
<Text>
The following component announces expanded/collapsed state correctly
</Text>
<Button
onPress={() => setExpanded(!expand)}
accessibilityState={{expanded: expand}}
accessibilityActions={expand ? [collapseAction] : [expandAction]}
onAccessibilityAction={event => {
switch (event.nativeEvent.actionName) {
case 'expand':
setExpanded(true);
break;
case 'collapse':
setExpanded(false);
break;
}
}}
title="click me to change state"
/>
</RNTesterBlock>

<RNTesterBlock title="Screenreader announces the visible text">
<Text>Announcing expanded/collapse and the visible text.</Text>
<TouchableOpacity
style={styles.button}
onPress={() => setExpanded(!expand)}
accessibilityState={{expanded: expand}}>
<Text>Click me to change state</Text>
</TouchableOpacity>
</RNTesterBlock>

<RNTesterBlock title="expanded/collapsed only managed through the accessibility menu">
<TouchableWithoutFeedback
accessibilityState={{expanded: true}}
accessible={true}>
<View>
<Text>Clicking me does not change state</Text>
</View>
</TouchableWithoutFeedback>
</RNTesterBlock>
</>
);
}

exports.title = 'Accessibility';
exports.documentationURL = 'https://reactnative.dev/docs/accessibilityinfo';
exports.description = 'Examples of using Accessibility APIs.';
exports.examples = [
{
title: 'Accessibility expanded',
render(): React.Element<typeof AccessibilityExpandedExample> {
return <AccessibilityExpandedExample />;
},
},
{
title: 'Accessibility elements',
render(): React.Element<typeof AccessibilityExample> {
return <AccessibilityExample />;
},
},
{
title: 'New accessibility roles and states',
render(): React.Element<typeof AccessibilityRoleAndStateExample> {
Expand Down

0 comments on commit 082a033

Please sign in to comment.