mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
New Accessibility states API. (#24608)
Summary:
As currently defined, accessibilityStates is an array of strings, which represents the state of an object. The array of strings notion doesn't well encapsulate how various states are related, nor enforce any level of correctness.
This PR converts accessibilityStates to an object with a specific definition. So, rather than:
<View
...
accessibilityStates={['unchecked']}>
We have:
<View
accessibilityStates={{'checked': false}}>
And specifically define the checked state to either take a boolean or the "mixed" string (to represent mixed checkboxes).
We feel this API is easier to understand an implement, and provides better semantic definition of the states themselves, and how states are related to one another.
## Changelog
[general] [change] - Convert accessibilityStates to an object instead of an array of strings.
Pull Request resolved: https://github.com/facebook/react-native/pull/24608
Differential Revision: D15467980
Pulled By: cpojer
fbshipit-source-id: f0414c0ef6add3f10f7f551d323d82d978754278
This commit is contained in:
committed by
Facebook Github Bot
parent
04564a0322
commit
099be9b356
@@ -6,14 +6,21 @@
|
||||
package com.facebook.react.uimanager;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewParent;
|
||||
|
||||
import androidx.core.view.ViewCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
import com.facebook.react.R;
|
||||
import com.facebook.react.bridge.Dynamic;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReadableMapKeySetIterator;
|
||||
import com.facebook.react.bridge.ReadableType;
|
||||
import com.facebook.react.common.MapBuilder;
|
||||
import com.facebook.react.uimanager.ReactAccessibilityDelegate;
|
||||
import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole;
|
||||
@@ -21,6 +28,7 @@ import com.facebook.react.uimanager.annotations.ReactProp;
|
||||
import com.facebook.react.uimanager.util.ReactFindViewUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@@ -41,6 +49,7 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
|
||||
private static final String PROP_ACCESSIBILITY_LIVE_REGION = "accessibilityLiveRegion";
|
||||
private static final String PROP_ACCESSIBILITY_ROLE = "accessibilityRole";
|
||||
private static final String PROP_ACCESSIBILITY_STATES = "accessibilityStates";
|
||||
private static final String PROP_ACCESSIBILITY_STATE = "accessibilityState";
|
||||
private static final String PROP_ACCESSIBILITY_ACTIONS = "accessibilityActions";
|
||||
private static final String PROP_IMPORTANT_FOR_ACCESSIBILITY = "importantForAccessibility";
|
||||
|
||||
@@ -64,13 +73,22 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
|
||||
new MatrixMathHelper.MatrixDecompositionContext();
|
||||
private static double[] sTransformDecompositionArray = new double[16];
|
||||
|
||||
public static final HashMap<String, Integer> sStateDescription= new HashMap<String, Integer>();
|
||||
public static final HashMap<String, Integer> sStateDescription = new HashMap<String, Integer>();
|
||||
static {
|
||||
sStateDescription.put("busy", R.string.state_busy_description);
|
||||
sStateDescription.put("expanded", R.string.state_expanded_description);
|
||||
sStateDescription.put("collapsed", R.string.state_collapsed_description);
|
||||
}
|
||||
|
||||
// State definition constants -- must match the definition in
|
||||
// ViewAccessibility.js. These only include states for which there
|
||||
// is no native support in android.
|
||||
|
||||
private static final String STATE_CHECKED = "checked"; // Special case for mixed state checkboxes
|
||||
private static final String STATE_BUSY = "busy";
|
||||
private static final String STATE_EXPANDED = "expanded";
|
||||
private static final String STATE_MIXED = "mixed";
|
||||
|
||||
@ReactProp(name = PROP_BACKGROUND_COLOR, defaultInt = Color.TRANSPARENT, customType = "Color")
|
||||
public void setBackgroundColor(@Nonnull T view, int backgroundColor) {
|
||||
view.setBackgroundColor(backgroundColor);
|
||||
@@ -169,27 +187,66 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_ACCESSIBILITY_STATE)
|
||||
public void setViewState(@Nonnull T view, @Nullable ReadableMap accessibilityState) {
|
||||
if (accessibilityState == null) {
|
||||
return;
|
||||
}
|
||||
view.setTag(R.id.accessibility_state, accessibilityState);
|
||||
view.setSelected(false);
|
||||
view.setEnabled(true);
|
||||
|
||||
// For states which don't have corresponding methods in
|
||||
// AccessibilityNodeInfo, update the view's content description
|
||||
// here
|
||||
|
||||
final ReadableMapKeySetIterator i = accessibilityState.keySetIterator();
|
||||
while (i.hasNextKey()) {
|
||||
final String state = i.nextKey();
|
||||
if (state.equals(STATE_BUSY) || state.equals(STATE_EXPANDED) ||
|
||||
(state.equals(STATE_CHECKED) && accessibilityState.getType(STATE_CHECKED) == ReadableType.String)) {
|
||||
updateViewContentDescription(view);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateViewContentDescription(@Nonnull T view) {
|
||||
final String accessibilityLabel = (String) view.getTag(R.id.accessibility_label);
|
||||
final ReadableArray accessibilityStates = (ReadableArray) view.getTag(R.id.accessibility_states);
|
||||
final ReadableMap accessibilityState = (ReadableMap) view.getTag(R.id.accessibility_state);
|
||||
final String accessibilityHint = (String) view.getTag(R.id.accessibility_hint);
|
||||
StringBuilder contentDescription = new StringBuilder();
|
||||
final ArrayList<String> contentDescription = new ArrayList<String>();
|
||||
if (accessibilityLabel != null) {
|
||||
contentDescription.append(accessibilityLabel + ", ");
|
||||
contentDescription.add(accessibilityLabel);
|
||||
}
|
||||
if (accessibilityStates != null) {
|
||||
for (int i = 0; i < accessibilityStates.size(); i++) {
|
||||
String state = accessibilityStates.getString(i);
|
||||
final String state = accessibilityStates.getString(i);
|
||||
if (sStateDescription.containsKey(state)) {
|
||||
contentDescription.append(view.getContext().getString(sStateDescription.get(state)) + ", ");
|
||||
contentDescription.add(view.getContext().getString(sStateDescription.get(state)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (accessibilityState != null) {
|
||||
final ReadableMapKeySetIterator i = accessibilityState.keySetIterator();
|
||||
while (i.hasNextKey()) {
|
||||
final String state = i.nextKey();
|
||||
final Dynamic value = accessibilityState.getDynamic(state);
|
||||
if (state.equals(STATE_CHECKED) && value.getType() == ReadableType.String && value.asString().equals(STATE_MIXED)) {
|
||||
contentDescription.add(view.getContext().getString(R.string.state_mixed_description));
|
||||
} else if (state.equals(STATE_BUSY) && 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (accessibilityHint != null) {
|
||||
contentDescription.append(accessibilityHint + ", ");
|
||||
contentDescription.add(accessibilityHint);
|
||||
}
|
||||
if (contentDescription.length() > 0) {
|
||||
view.setContentDescription(contentDescription.toString());
|
||||
if (contentDescription.size() > 0) {
|
||||
view.setContentDescription(TextUtils.join(", ", contentDescription));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+41
-7
@@ -18,12 +18,16 @@ import androidx.core.view.AccessibilityDelegateCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.Dynamic;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReadableMapKeySetIterator;
|
||||
import com.facebook.react.bridge.ReadableType;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||
import com.facebook.react.R;
|
||||
@@ -39,6 +43,7 @@ import javax.annotation.Nullable;
|
||||
|
||||
public class ReactAccessibilityDelegate extends AccessibilityDelegateCompat {
|
||||
|
||||
private static final String TAG = "ReactAccessibilityDelegate";
|
||||
private static int sCounter = 0x3f000000;
|
||||
|
||||
public static final HashMap<String, Integer> sActionIdMap= new HashMap<>();
|
||||
@@ -121,6 +126,12 @@ public class ReactAccessibilityDelegate extends AccessibilityDelegateCompat {
|
||||
|
||||
private final HashMap<Integer, String> mAccessibilityActionsMap;
|
||||
|
||||
// State constants for states which have analogs in AccessibilityNodeInfo
|
||||
|
||||
private static final String STATE_DISABLED = "disabled";
|
||||
private static final String STATE_SELECTED = "selected";
|
||||
private static final String STATE_CHECKED = "checked";
|
||||
|
||||
public ReactAccessibilityDelegate() {
|
||||
super();
|
||||
mAccessibilityActionsMap = new HashMap<Integer, String>();
|
||||
@@ -136,8 +147,12 @@ public class ReactAccessibilityDelegate extends AccessibilityDelegateCompat {
|
||||
|
||||
// states are changable.
|
||||
final ReadableArray accessibilityStates = (ReadableArray) host.getTag(R.id.accessibility_states);
|
||||
final ReadableMap accessibilityState = (ReadableMap) host.getTag(R.id.accessibility_state);
|
||||
if (accessibilityStates != null) {
|
||||
setState(info, accessibilityStates, host.getContext());
|
||||
setStates(info, accessibilityStates, host.getContext());
|
||||
}
|
||||
if (accessibilityState != null) {
|
||||
setState(info, accessibilityState, host.getContext());
|
||||
}
|
||||
final ReadableArray accessibilityActions = (ReadableArray) host.getTag(R.id.accessibility_actions);
|
||||
if (accessibilityActions != null) {
|
||||
@@ -175,7 +190,7 @@ public class ReactAccessibilityDelegate extends AccessibilityDelegateCompat {
|
||||
return super.performAccessibilityAction(host, action, args);
|
||||
}
|
||||
|
||||
public static void setState(AccessibilityNodeInfoCompat info, ReadableArray accessibilityStates, Context context) {
|
||||
private static void setStates(AccessibilityNodeInfoCompat info, ReadableArray accessibilityStates, Context context) {
|
||||
for (int i = 0; i < accessibilityStates.size(); i++) {
|
||||
String state = accessibilityStates.getString(i);
|
||||
switch (state) {
|
||||
@@ -188,20 +203,38 @@ public class ReactAccessibilityDelegate extends AccessibilityDelegateCompat {
|
||||
case "checked":
|
||||
info.setCheckable(true);
|
||||
info.setChecked(true);
|
||||
if (info.getClassName().equals("android.widget.Switch")) {
|
||||
if (info.getClassName().equals(AccessibilityRole.getValue(AccessibilityRole.SWITCH))) {
|
||||
info.setText(context.getString(R.string.state_on_description));
|
||||
}
|
||||
break;
|
||||
case "unchecked":
|
||||
info.setCheckable(true);
|
||||
info.setChecked(false);
|
||||
if (info.getClassName().equals("android.widget.Switch")) {
|
||||
if (info.getClassName().equals(AccessibilityRole.getValue(AccessibilityRole.SWITCH))) {
|
||||
info.setText(context.getString(R.string.state_off_description));
|
||||
}
|
||||
break;
|
||||
case "hasPopup":
|
||||
info.setCanOpenPopup(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void setState(AccessibilityNodeInfoCompat info, ReadableMap accessibilityState, Context context) {
|
||||
Log.d(TAG, "setState " + accessibilityState);
|
||||
final ReadableMapKeySetIterator i = accessibilityState.keySetIterator();
|
||||
while (i.hasNextKey()) {
|
||||
final String state = i.nextKey();
|
||||
final Dynamic value = accessibilityState.getDynamic(state);
|
||||
if (state.equals(STATE_SELECTED) && value.getType() == ReadableType.Boolean) {
|
||||
info.setSelected(value.asBoolean());
|
||||
} else if (state.equals(STATE_DISABLED) && value.getType() == ReadableType.Boolean) {
|
||||
info.setEnabled(!value.asBoolean());
|
||||
} else if (state.equals(STATE_CHECKED) && value.getType() == ReadableType.Boolean) {
|
||||
final boolean boolValue = value.asBoolean();
|
||||
info.setCheckable(true);
|
||||
info.setChecked(boolValue);
|
||||
if (info.getClassName().equals(AccessibilityRole.getValue(AccessibilityRole.SWITCH))) {
|
||||
info.setText(context.getString(boolValue ? R.string.state_on_description : R.string.state_off_description));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -281,6 +314,7 @@ public class ReactAccessibilityDelegate extends AccessibilityDelegateCompat {
|
||||
if (!ViewCompat.hasAccessibilityDelegate(view)
|
||||
&& (view.getTag(R.id.accessibility_role) != null ||
|
||||
view.getTag(R.id.accessibility_states) != null ||
|
||||
view.getTag(R.id.accessibility_state) != null ||
|
||||
view.getTag(R.id.accessibility_actions) != null)) {
|
||||
ViewCompat.setAccessibilityDelegate(view, new ReactAccessibilityDelegate());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user