Skip to content

Commit 082a033

Browse files
fabOnReactfacebook-github-bot
authored andcommitted
Android: using AccessibilityNodeInfo#addAction to announce Expandable/Collapsible State (facebook#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 facebook#30841 ## Changelog [Android] [Fixed] - using AccessibilityNodeInfo#addAction to announce Expandable/Collapsible State Pull Request resolved: facebook#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](facebook#34353 (comment)) - Fabric [Tests](facebook#34353 (comment)) Tests 6th September 2022: - [Button which keeps control of extended/collapsed state in JavaScript with onAccessibilityAction, accessibilityActions and accessibiltyState (Paper)](facebook#34353 (comment)) - [TouchableWithoutFeedback keeps control of extended/collapsed state in Android Widget (Paper)](facebook#34353 (comment)) - [TouchableWithoutFeedback keeps control of extended/collapsed state in Android Widget (Fabric)](facebook#34353 (comment)) - [TouchableOpacity announces visible text and triggers expanded/collapsed with onPress and accessiblity menu (Fabric)](facebook#34353 (comment)) Announcing state with custom actions on Fabric (FAIL). The issue is not a regression from this PR, as documented in facebook#34353 (comment). It will be fixed in a separate PR. Reviewed By: NickGerleman Differential Revision: D39893863 Pulled By: blavalla fbshipit-source-id: f6af78b1839ba7d97eca052bd258faae00cbd27b
1 parent 8a847a3 commit 082a033

File tree

4 files changed

+93
-8
lines changed

4 files changed

+93
-8
lines changed

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ protected T prepareToRecycleView(@NonNull ThemedReactContext reactContext, T vie
7979
view.setTag(R.id.accessibility_state, null);
8080
view.setTag(R.id.accessibility_actions, null);
8181
view.setTag(R.id.accessibility_value, null);
82+
view.setTag(R.id.accessibility_state_expanded, null);
8283

8384
// This indirectly calls (and resets):
8485
// setTranslationX
@@ -270,6 +271,9 @@ public void setViewState(@NonNull T view, @Nullable ReadableMap accessibilitySta
270271
if (accessibilityState == null) {
271272
return;
272273
}
274+
if (accessibilityState.hasKey("expanded")) {
275+
view.setTag(R.id.accessibility_state_expanded, accessibilityState.getBoolean("expanded"));
276+
}
273277
if (accessibilityState.hasKey("selected")) {
274278
boolean prevSelected = view.isSelected();
275279
boolean nextSelected = accessibilityState.getBoolean("selected");
@@ -335,13 +339,6 @@ private void updateViewContentDescription(@NonNull T view) {
335339
&& value.getType() == ReadableType.Boolean
336340
&& value.asBoolean()) {
337341
contentDescription.add(view.getContext().getString(R.string.state_busy_description));
338-
} else if (state.equals(STATE_EXPANDED) && value.getType() == ReadableType.Boolean) {
339-
contentDescription.add(
340-
view.getContext()
341-
.getString(
342-
value.asBoolean()
343-
? R.string.state_expanded_description
344-
: R.string.state_collapsed_description));
345342
}
346343
}
347344
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
6767
sActionIdMap.put("longpress", AccessibilityActionCompat.ACTION_LONG_CLICK.getId());
6868
sActionIdMap.put("increment", AccessibilityActionCompat.ACTION_SCROLL_FORWARD.getId());
6969
sActionIdMap.put("decrement", AccessibilityActionCompat.ACTION_SCROLL_BACKWARD.getId());
70+
sActionIdMap.put("expand", AccessibilityActionCompat.ACTION_EXPAND.getId());
71+
sActionIdMap.put("collapse", AccessibilityActionCompat.ACTION_COLLAPSE.getId());
7072
}
7173

7274
private final View mView;
@@ -250,6 +252,14 @@ public void handleMessage(Message msg) {
250252
@Override
251253
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
252254
super.onInitializeAccessibilityNodeInfo(host, info);
255+
if (host.getTag(R.id.accessibility_state_expanded) != null) {
256+
final boolean accessibilityStateExpanded =
257+
(boolean) host.getTag(R.id.accessibility_state_expanded);
258+
info.addAction(
259+
accessibilityStateExpanded
260+
? AccessibilityNodeInfoCompat.ACTION_COLLAPSE
261+
: AccessibilityNodeInfoCompat.ACTION_EXPAND);
262+
}
253263
final AccessibilityRole accessibilityRole =
254264
(AccessibilityRole) host.getTag(R.id.accessibility_role);
255265
final String accessibilityHint = (String) host.getTag(R.id.accessibility_hint);
@@ -380,6 +390,12 @@ public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event)
380390

381391
@Override
382392
public boolean performAccessibilityAction(View host, int action, Bundle args) {
393+
if (action == AccessibilityNodeInfoCompat.ACTION_COLLAPSE) {
394+
host.setTag(R.id.accessibility_state_expanded, false);
395+
}
396+
if (action == AccessibilityNodeInfoCompat.ACTION_EXPAND) {
397+
host.setTag(R.id.accessibility_state_expanded, true);
398+
}
383399
if (mAccessibilityActionsMap.containsKey(action)) {
384400
final WritableMap event = Arguments.createMap();
385401
event.putString("actionName", mAccessibilityActionsMap.get(action));

ReactAndroid/src/main/res/views/uimanager/values/ids.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
<!-- tag is used to store accessibilityState -->
2525
<item type="id" name="accessibility_state"/>
2626

27-
<!-- tag is used to store accessibilityLabel tag-->
27+
<!--tag is used to store accessibilityStateExpanded -->
28+
<item type="id" name="accessibility_state_expanded"/>
29+
30+
<!--tag is used to store accessibilityLabel tag-->
2831
<item type="id" name="accessibility_label"/>
2932

3033
<!-- tag is used to store accessibilityActions tag-->

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ const styles = StyleSheet.create({
7070
flexDirection: 'column',
7171
justifyContent: 'space-between',
7272
},
73+
button: {
74+
padding: 8,
75+
borderWidth: 1,
76+
borderColor: 'blue',
77+
},
7378
container: {
7479
flex: 1,
7580
},
@@ -1431,10 +1436,74 @@ function DisplayOptionStatusExample({
14311436
);
14321437
}
14331438

1439+
function AccessibilityExpandedExample(): React.Node {
1440+
const [expand, setExpanded] = React.useState(false);
1441+
const [pressed, setPressed] = React.useState(false);
1442+
const expandAction = {name: 'expand'};
1443+
const collapseAction = {name: 'collapse'};
1444+
return (
1445+
<>
1446+
<RNTesterBlock title="Collapse/Expanded state change (Paper)">
1447+
<Text>
1448+
The following component announces expanded/collapsed state correctly
1449+
</Text>
1450+
<Button
1451+
onPress={() => setExpanded(!expand)}
1452+
accessibilityState={{expanded: expand}}
1453+
accessibilityActions={expand ? [collapseAction] : [expandAction]}
1454+
onAccessibilityAction={event => {
1455+
switch (event.nativeEvent.actionName) {
1456+
case 'expand':
1457+
setExpanded(true);
1458+
break;
1459+
case 'collapse':
1460+
setExpanded(false);
1461+
break;
1462+
}
1463+
}}
1464+
title="click me to change state"
1465+
/>
1466+
</RNTesterBlock>
1467+
1468+
<RNTesterBlock title="Screenreader announces the visible text">
1469+
<Text>Announcing expanded/collapse and the visible text.</Text>
1470+
<TouchableOpacity
1471+
style={styles.button}
1472+
onPress={() => setExpanded(!expand)}
1473+
accessibilityState={{expanded: expand}}>
1474+
<Text>Click me to change state</Text>
1475+
</TouchableOpacity>
1476+
</RNTesterBlock>
1477+
1478+
<RNTesterBlock title="expanded/collapsed only managed through the accessibility menu">
1479+
<TouchableWithoutFeedback
1480+
accessibilityState={{expanded: true}}
1481+
accessible={true}>
1482+
<View>
1483+
<Text>Clicking me does not change state</Text>
1484+
</View>
1485+
</TouchableWithoutFeedback>
1486+
</RNTesterBlock>
1487+
</>
1488+
);
1489+
}
1490+
14341491
exports.title = 'Accessibility';
14351492
exports.documentationURL = 'https://reactnative.dev/docs/accessibilityinfo';
14361493
exports.description = 'Examples of using Accessibility APIs.';
14371494
exports.examples = [
1495+
{
1496+
title: 'Accessibility expanded',
1497+
render(): React.Element<typeof AccessibilityExpandedExample> {
1498+
return <AccessibilityExpandedExample />;
1499+
},
1500+
},
1501+
{
1502+
title: 'Accessibility elements',
1503+
render(): React.Element<typeof AccessibilityExample> {
1504+
return <AccessibilityExample />;
1505+
},
1506+
},
14381507
{
14391508
title: 'New accessibility roles and states',
14401509
render(): React.Element<typeof AccessibilityRoleAndStateExample> {

0 commit comments

Comments
 (0)