From 082a033fbbe7d7094af78bafc3b2048194a02bd5 Mon Sep 17 00:00:00 2001 From: fabriziobertoglio1987 Date: Tue, 8 Nov 2022 12:26:59 -0800 Subject: [PATCH] Android: using AccessibilityNodeInfo#addAction to announce Expandable/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 https://github.com/facebook/react-native/issues/30841 ## Changelog [Android] [Fixed] - using AccessibilityNodeInfo#addAction to announce Expandable/Collapsible State Pull Request resolved: https://github.com/facebook/react-native/pull/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](https://github.com/facebook/react-native/pull/34353#issuecomment-1217425302) - Fabric [Tests](https://github.com/facebook/react-native/pull/34353#issuecomment-1217781734) Tests 6th September 2022: - [Button which keeps control of extended/collapsed state in JavaScript with onAccessibilityAction, accessibilityActions and accessibiltyState (Paper)](https://github.com/facebook/react-native/pull/34353#issuecomment-1237601847) - [TouchableWithoutFeedback keeps control of extended/collapsed state in Android Widget (Paper)](https://github.com/facebook/react-native/pull/34353#issuecomment-1237616304) - [TouchableWithoutFeedback keeps control of extended/collapsed state in Android Widget (Fabric)](https://github.com/facebook/react-native/pull/34353#issuecomment-1237624755) - [TouchableOpacity announces visible text and triggers expanded/collapsed with onPress and accessiblity menu (Fabric)](https://github.com/facebook/react-native/pull/34353#issuecomment-1237627857) Announcing state with custom actions on Fabric (FAIL). The issue is not a regression from this PR, as documented in https://github.com/facebook/react-native/pull/34353#issuecomment-1207744977. It will be fixed in a separate PR. Reviewed By: NickGerleman Differential Revision: D39893863 Pulled By: blavalla fbshipit-source-id: f6af78b1839ba7d97eca052bd258faae00cbd27b --- .../react/uimanager/BaseViewManager.java | 11 ++- .../uimanager/ReactAccessibilityDelegate.java | 16 +++++ .../main/res/views/uimanager/values/ids.xml | 5 +- .../Accessibility/AccessibilityExample.js | 69 +++++++++++++++++++ 4 files changed, 93 insertions(+), 8 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index f32bc44be2d58f..ee6ab32496e6fc 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -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 @@ -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"); @@ -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)); } } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java index 1ec4a23f73fb0e..e6ef3c837975c9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java @@ -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; @@ -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); @@ -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)); diff --git a/ReactAndroid/src/main/res/views/uimanager/values/ids.xml b/ReactAndroid/src/main/res/views/uimanager/values/ids.xml index 998cb3e222caca..6324b85af44673 100644 --- a/ReactAndroid/src/main/res/views/uimanager/values/ids.xml +++ b/ReactAndroid/src/main/res/views/uimanager/values/ids.xml @@ -24,7 +24,10 @@ - + + + + diff --git a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js index d5caca95b71530..f80f587e547e05 100644 --- a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js +++ b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js @@ -70,6 +70,11 @@ const styles = StyleSheet.create({ flexDirection: 'column', justifyContent: 'space-between', }, + button: { + padding: 8, + borderWidth: 1, + borderColor: 'blue', + }, container: { flex: 1, }, @@ -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 ( + <> + + + The following component announces expanded/collapsed state correctly + +