added accessibilityRole Prop, added functionality support for role on android

Summary:
Added a new property to View for Accessibility called `accessibilityRole`. This property merges functionality of existing properties: `accessibilityTraits` (iOS) and `accessibilityComponentType` (android).

Currently, nine values are supported with equivalent behavior as `accessibilityTraits` (iOS) when `accessibilityRole` is set on iOS Voiceover and Android TalkBack

```
  | 'none'
  | 'button'
  | 'link'
  | 'search'
  | 'image'
  | 'keyboardkey'
  | 'text'
  | 'adjustable'
  | 'tabbar'
```
They currently support similar behavior on talkback on Android and voice over on iOS
Does not break functionality of existing properties, but have not tested for behavior of setting both this one and the old one.

* iOS - I added a property accessibilityRoles, and basically remapped it to the same thing as accessibilityTraits. I also added in enum mappings for keyboardkey and tabbar.
* Android - Also added a property accessibilityRoles, from the Android side. For the underlying native functionality, I built a helper class that is based off of AccessibilityRolesUtil.java from the accessibility team. Biggest changes made are that I defined my own enums if needed, and also set some properties to match the functionality of iOS Accessibility Traits. I also handled the logic for switch/case statements of setting roles for the android side on this file. Also, I currently haven't localized strings for setRoleDescription, but plan to.
* Javascript - I added a view property accessibilityRoles in ViewPropTypes.

Reviewed By: blavalla

Differential Revision: D8756225

fbshipit-source-id: e03eec40cce86042551764f433e1defe7ee41b35
This commit is contained in:
Ziqi Chen 2018-07-10 12:09:02 -07:00 committed by Facebook Github Bot
parent ef3d8b23c3
commit c27b495a89
5 changed files with 155 additions and 0 deletions

View File

@ -39,6 +39,14 @@ export type AccessibilityComponentType =
| 'radiobutton_checked'
| 'radiobutton_unchecked';
export type AccessibilityRole =
| 'none'
| 'button'
| 'image'
| 'keyboardkey'
| 'text'
| 'tabbar';
module.exports = {
AccessibilityTraits: [
'none',
@ -65,4 +73,12 @@ module.exports = {
'radiobutton_checked',
'radiobutton_unchecked',
],
AccessibilityRoles: [
'none',
'button',
'image',
'keyboardkey',
'text',
'tabbar',
],
};

View File

@ -20,11 +20,13 @@ const ViewStylePropTypes = require('ViewStylePropTypes');
const {
AccessibilityComponentTypes,
AccessibilityTraits,
AccessibilityRoles,
} = require('ViewAccessibility');
import type {
AccessibilityComponentType,
AccessibilityTrait,
AccessibilityRole,
} from 'ViewAccessibility';
import type {EdgeInsetsProp} from 'EdgeInsetsPropType';
import type {TVViewProps} from 'TVViewPropTypes';
@ -89,6 +91,7 @@ export type ViewProps = $ReadOnly<{|
importantForAccessibility?: 'auto' | 'yes' | 'no' | 'no-hide-descendants',
accessibilityIgnoresInvertColors?: boolean,
accessibilityTraits?: AccessibilityTrait | Array<AccessibilityTrait>,
accessibilityRole?: AccessibilityRole,
accessibilityViewIsModal?: boolean,
accessibilityElementsHidden?: boolean,
children?: ?React.Node,
@ -139,6 +142,12 @@ module.exports = {
*/
accessibilityComponentType: PropTypes.oneOf(AccessibilityComponentTypes),
/**
* Indicates to accessibility services to treat UI component like a
* native one. Merging accessibilityComponentType and accessibilityTraits.
*/
accessibilityRole: PropTypes.oneOf(AccessibilityRoles),
/**
* Indicates to accessibility services whether the user should be notified
* when this view changes. Works for Android API >= 19 only.

View File

@ -33,9 +33,11 @@ RCT_MULTI_ENUM_CONVERTER(UIAccessibilityTraits, (@{
@"header": @(UIAccessibilityTraitHeader),
@"search": @(UIAccessibilityTraitSearchField),
@"image": @(UIAccessibilityTraitImage),
@"tabbar": @(UIAccessibilityTraitTabBar),
@"selected": @(UIAccessibilityTraitSelected),
@"plays": @(UIAccessibilityTraitPlaysSound),
@"key": @(UIAccessibilityTraitKeyboardKey),
@"keyboardkey": @(UIAccessibilityTraitKeyboardKey),
@"text": @(UIAccessibilityTraitStaticText),
@"summary": @(UIAccessibilityTraitSummaryElement),
@"disabled": @(UIAccessibilityTraitNotEnabled),
@ -110,6 +112,7 @@ RCT_REMAP_VIEW_PROPERTY(accessible, reactAccessibilityElement.isAccessibilityEle
RCT_REMAP_VIEW_PROPERTY(accessibilityActions, reactAccessibilityElement.accessibilityActions, NSString)
RCT_REMAP_VIEW_PROPERTY(accessibilityLabel, reactAccessibilityElement.accessibilityLabel, NSString)
RCT_REMAP_VIEW_PROPERTY(accessibilityTraits, reactAccessibilityElement.accessibilityTraits, UIAccessibilityTraits)
RCT_REMAP_VIEW_PROPERTY(accessibilityRole, reactAccessibilityElement.accessibilityTraits, UIAccessibilityTraits)
RCT_REMAP_VIEW_PROPERTY(accessibilityViewIsModal, reactAccessibilityElement.accessibilityViewIsModal, BOOL)
RCT_REMAP_VIEW_PROPERTY(accessibilityElementsHidden, reactAccessibilityElement.accessibilityElementsHidden, BOOL)
RCT_REMAP_VIEW_PROPERTY(accessibilityIgnoresInvertColors, reactAccessibilityElement.shouldAccessibilityIgnoresInvertColors, BOOL)

View File

@ -0,0 +1,121 @@
// Copyright (c) 2004-present, Facebook, Inc.
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package com.facebook.react.uimanager;
import android.annotation.TargetApi;
import android.os.Build;
import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
import javax.annotation.Nullable;
/**
* Utility class that handles the addition of a "role" for accessibility to either a View or
* AccessibilityNodeInfo.
*/
public class AccessibilityRoleUtil {
/**
* These roles are defined by Google's TalkBack screen reader, and this list should be kept up to
* date with their implementation. Details can be seen in their source code here:
*
* <p>https://github.com/google/talkback/blob/master/utils/src/main/java/Role.java
*/
public enum AccessibilityRole {
NONE(null),
BUTTON("android.widget.Button"),
IMAGE("android.widget.ImageView"),
KEYBOARD_KEY("android.inputmethodservice.Keyboard$Key"),
TEXT("android.widget.ViewGroup"),
TAB_BAR("android.widget.TabWidget");
@Nullable private final String mValue;
AccessibilityRole(String type) {
mValue = type;
}
@Nullable
public String getValue() {
return mValue;
}
public static AccessibilityRole fromValue(String value) {
for (AccessibilityRole role : AccessibilityRole.values()) {
if (role.getValue() != null && role.getValue().equals(value)) {
return role;
}
}
return AccessibilityRole.NONE;
}
}
private AccessibilityRoleUtil() {
// No instances
}
public static void setRole(View view, final AccessibilityRole role) {
// if a view already has an accessibility delegate, replacing it could cause problems,
// so leave it alone.
if (!ViewCompat.hasAccessibilityDelegate(view)) {
ViewCompat.setAccessibilityDelegate(
view,
new AccessibilityDelegateCompat() {
@Override
public void onInitializeAccessibilityNodeInfo(
View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
setRole(info, role);
}
});
}
}
public static void setRole(AccessibilityNodeInfoCompat nodeInfo, final AccessibilityRole role) {
nodeInfo.setClassName(role.getValue());
}
/**
* Variables and methods for setting accessibilityRole on view properties.
*/
private static final String NONE = "none";
private static final String BUTTON = "button";
private static final String IMAGE = "image";
private static final String KEYBOARDKEY = "keyboardkey";
private static final String TEXT = "text";
private static final String TABBAR = "tabbar";
public static void updateAccessibilityRole(View view, String role) {
if (role == null) {
view.setAccessibilityDelegate(null);
}
switch (role) {
case NONE:
break;
case BUTTON:
setRole(view, AccessibilityRoleUtil.AccessibilityRole.BUTTON);
break;
case IMAGE:
setRole(view, AccessibilityRoleUtil.AccessibilityRole.IMAGE);
break;
case KEYBOARDKEY:
setRole(view, AccessibilityRoleUtil.AccessibilityRole.KEYBOARD_KEY);
break;
case TEXT:
setRole(view, AccessibilityRoleUtil.AccessibilityRole.TEXT);
break;
case TABBAR:
setRole(view, AccessibilityRoleUtil.AccessibilityRole.TAB_BAR);
break;
default:
view.setAccessibilityDelegate(null);
}
}
}

View File

@ -29,6 +29,7 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
private static final String PROP_ACCESSIBILITY_LABEL = "accessibilityLabel";
private static final String PROP_ACCESSIBILITY_COMPONENT_TYPE = "accessibilityComponentType";
private static final String PROP_ACCESSIBILITY_LIVE_REGION = "accessibilityLiveRegion";
private static final String PROP_ACCESSIBILITY_ROLE = "accessibilityRole";
private static final String PROP_IMPORTANT_FOR_ACCESSIBILITY = "importantForAccessibility";
// DEPRECATED
@ -117,6 +118,11 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
AccessibilityHelper.updateAccessibilityComponentType(view, accessibilityComponentType);
}
@ReactProp(name = PROP_ACCESSIBILITY_ROLE)
public void setAccessibilityRole(T view, String accessibilityRole) {
AccessibilityRoleUtil.updateAccessibilityRole(view, accessibilityRole);
}
@ReactProp(name = PROP_IMPORTANT_FOR_ACCESSIBILITY)
public void setImportantForAccessibility(T view, String importantForAccessibility) {
if (importantForAccessibility == null || importantForAccessibility.equals("auto")) {