added accessibilityHints implementation on Android

Summary:
Implemented a version of accessibility Hints on android by adding hint text to the end of contentDescription. There is already an existing prop on js and iOS implementation.

Changes involve:
* adding a prop on native android code for accessibilityHints
* setting the accessibilityDelegate after the props are all loaded.
* Instead of directly updating the accessibility delegate, the prop setters now update state by setting the values of the variables. Once all props are set, the accessibility delegate is set based on the props
   * BaseViewManager keeps track of whether or not accessibility props have been set
   * AccessibilityDelegateUtil keeps track of what the props have been set to. (Renamed AccessibilityRoleUtil to AccessibilityDelegateUtil)

Currently, this is the easiest way of emulating the way accessibility hints work on iOS, and I think having an android counter part is better than having nothing.

It's different from iOS in that it will announce the hint before the role, and also cannot be turned off.

Ex:

if I set the accessibility like this:
```
      <View
        style={styles.container}
        accessible={true}
        accessibilityLabel="accessibility label"
        accessibilityRole="button"
        accessibilityStates={['selected']}
        accessibilityHint="accessibility Hint">
        <Text> Tester </Text>
      </View>
```

Talk back will read:
`accessibility label, accessibility Hint, button, selected`

In the future for next steps, I plan on investigating the process of making a second announcement after the first

Reviewed By: achen1

Differential Revision: D9037226

fbshipit-source-id: 8d484e1114eb69aa5f5314b3755b351b8ea80b09
This commit is contained in:
Ziqi Chen 2018-08-08 00:50:37 -07:00 committed by Facebook Github Bot
parent cf3cdeeb90
commit d3f0919816
4 changed files with 57 additions and 16 deletions

View File

@ -23,7 +23,7 @@ import javax.annotation.Nullable;
* AccessibilityNodeInfo. * AccessibilityNodeInfo.
*/ */
public class AccessibilityRoleUtil { public class AccessibilityDelegateUtil {
/** /**
* These roles are defined by Google's TalkBack screen reader, and this list should be kept up to * These roles are defined by Google's TalkBack screen reader, and this list should be kept up to
@ -41,7 +41,9 @@ public class AccessibilityRoleUtil {
IMAGEBUTTON("android.widget.ImageView"), IMAGEBUTTON("android.widget.ImageView"),
KEYBOARDKEY("android.inputmethodservice.Keyboard$Key"), KEYBOARDKEY("android.inputmethodservice.Keyboard$Key"),
TEXT("android.widget.ViewGroup"), TEXT("android.widget.ViewGroup"),
ADJUSTABLE("android.widget.SeekBar"); ADJUSTABLE("android.widget.SeekBar"),
SUMMARY("android.widget.ViewGroup"),
HEADER("android.widget.ViewGroup");
@Nullable private final String mValue; @Nullable private final String mValue;
@ -64,11 +66,11 @@ public class AccessibilityRoleUtil {
} }
} }
private AccessibilityRoleUtil() { private AccessibilityDelegateUtil() {
// No instances // No instances
} }
public static void setRole(final View view, final AccessibilityRole role) { public static void setDelegate(final View view) {
// if a view already has an accessibility delegate, replacing it could cause problems, // if a view already has an accessibility delegate, replacing it could cause problems,
// so leave it alone. // so leave it alone.
if (!ViewCompat.hasAccessibilityDelegate(view)) { if (!ViewCompat.hasAccessibilityDelegate(view)) {
@ -79,7 +81,18 @@ public class AccessibilityRoleUtil {
public void onInitializeAccessibilityNodeInfo( public void onInitializeAccessibilityNodeInfo(
View host, AccessibilityNodeInfoCompat info) { View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info); super.onInitializeAccessibilityNodeInfo(host, info);
setRole(info, role, view.getContext()); String accessibilityHint = (String) view.getTag(R.id.accessibility_hint);
AccessibilityRole accessibilityRole = getAccessibilityRole((String) view.getTag(R.id.accessibility_role));
setRole(info, accessibilityRole, view.getContext());
if (!(accessibilityHint == null)) {
String contentDescription=(String)info.getContentDescription();
if (contentDescription != null) {
contentDescription = contentDescription + ", " + accessibilityHint;
info.setContentDescription(contentDescription);
} else {
info.setContentDescription(accessibilityHint);
}
}
} }
}); });
} }
@ -89,7 +102,7 @@ public class AccessibilityRoleUtil {
* Strings for setting the Role Description in english * Strings for setting the Role Description in english
*/ */
//TODO: Eventually support fot other languages on talkback //TODO: Eventually support for other languages on talkback
public static void setRole(AccessibilityNodeInfoCompat nodeInfo, final AccessibilityRole role, final Context context) { public static void setRole(AccessibilityNodeInfoCompat nodeInfo, final AccessibilityRole role, final Context context) {
nodeInfo.setClassName(role.getValue()); nodeInfo.setClassName(role.getValue());
@ -118,16 +131,10 @@ public class AccessibilityRoleUtil {
/** /**
* Method for setting accessibilityRole on view properties. * Method for setting accessibilityRole on view properties.
*/ */
public static void updateAccessibilityRole(View view, String role) { public static AccessibilityRole getAccessibilityRole(String role) {
if (role == null) { if (role == null) {
view.setAccessibilityDelegate(null); return AccessibilityRole.NONE;
}
try {
setRole(view, AccessibilityRole.valueOf(role.toUpperCase()));
} catch (NullPointerException e) {
view.setAccessibilityDelegate(null);
} catch (IllegalArgumentException e) {
view.setAccessibilityDelegate(null);
} }
return AccessibilityRole.valueOf(role.toUpperCase());
} }
} }

View File

@ -28,6 +28,7 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
private static final String PROP_RENDER_TO_HARDWARE_TEXTURE = "renderToHardwareTextureAndroid"; private static final String PROP_RENDER_TO_HARDWARE_TEXTURE = "renderToHardwareTextureAndroid";
private static final String PROP_ACCESSIBILITY_LABEL = "accessibilityLabel"; private static final String PROP_ACCESSIBILITY_LABEL = "accessibilityLabel";
private static final String PROP_ACCESSIBILITY_COMPONENT_TYPE = "accessibilityComponentType"; private static final String PROP_ACCESSIBILITY_COMPONENT_TYPE = "accessibilityComponentType";
private static final String PROP_ACCESSIBILITY_HINT = "accessibilityHint";
private static final String PROP_ACCESSIBILITY_LIVE_REGION = "accessibilityLiveRegion"; private static final String PROP_ACCESSIBILITY_LIVE_REGION = "accessibilityLiveRegion";
private static final String PROP_ACCESSIBILITY_ROLE = "accessibilityRole"; private static final String PROP_ACCESSIBILITY_ROLE = "accessibilityRole";
private static final String PROP_ACCESSIBILITY_STATES = "accessibilityStates"; private static final String PROP_ACCESSIBILITY_STATES = "accessibilityStates";
@ -119,9 +120,24 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
AccessibilityHelper.updateAccessibilityComponentType(view, accessibilityComponentType); AccessibilityHelper.updateAccessibilityComponentType(view, accessibilityComponentType);
} }
@ReactProp(name = PROP_ACCESSIBILITY_HINT)
public void setAccessibilityHint(T view, String accessibilityHint) {
view.setTag(R.id.accessibility_hint, accessibilityHint);
}
@ReactProp(name = PROP_ACCESSIBILITY_ROLE) @ReactProp(name = PROP_ACCESSIBILITY_ROLE)
public void setAccessibilityRole(T view, String accessibilityRole) { public void setAccessibilityRole(T view, String accessibilityRole) {
AccessibilityRoleUtil.updateAccessibilityRole(view, accessibilityRole); if (accessibilityRole == null) {
return;
}
try {
AccessibilityDelegateUtil.AccessibilityRole.valueOf(accessibilityRole.toUpperCase());
} catch (NullPointerException e) {
throw new IllegalArgumentException("Invalid Role " + accessibilityRole + " Passed In");
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid Role " + accessibilityRole + " Passed In");
}
view.setTag(R.id.accessibility_role, accessibilityRole);
} }
@ReactProp(name = PROP_ACCESSIBILITY_STATES) @ReactProp(name = PROP_ACCESSIBILITY_STATES)
@ -239,4 +255,14 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
view.setScaleY(1); view.setScaleY(1);
view.setCameraDistance(0); view.setCameraDistance(0);
} }
private void updateViewAccessibility(T view) {
AccessibilityDelegateUtil.setDelegate(view);
}
@Override
protected void onAfterUpdateTransaction(T view) {
super.onAfterUpdateTransaction(view);
updateViewAccessibility(view);
}
} }

View File

@ -8,4 +8,11 @@
<!-- tag is used to store the nativeID tag --> <!-- tag is used to store the nativeID tag -->
<item type="id" name="view_tag_instance_handle"/> <item type="id" name="view_tag_instance_handle"/>
<!--tag is used to store accessibilityHint tag-->
<item type="id" name="accessibility_hint"/>
<!--tag is used to store accessibilityRole tag-->
<item type="id" name="accessibility_role"/>
</resources> </resources>

View File

@ -16,6 +16,7 @@ rn_robolectric_test(
deps = [ deps = [
YOGA_TARGET, YOGA_TARGET,
react_native_dep("libraries/fbcore/src/test/java/com/facebook/powermock:powermock"), react_native_dep("libraries/fbcore/src/test/java/com/facebook/powermock:powermock"),
react_native_dep("third-party/android/support/v4:lib-support-v4"),
react_native_dep("third-party/java/fest:fest"), react_native_dep("third-party/java/fest:fest"),
react_native_dep("third-party/java/jsr-305:jsr-305"), react_native_dep("third-party/java/jsr-305:jsr-305"),
react_native_dep("third-party/java/junit:junit"), react_native_dep("third-party/java/junit:junit"),