diff --git a/Libraries/Components/View/ViewAccessibility.js b/Libraries/Components/View/ViewAccessibility.js index 4a9ebc172a13c9..a12528fad5fc76 100644 --- a/Libraries/Components/View/ViewAccessibility.js +++ b/Libraries/Components/View/ViewAccessibility.js @@ -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', + ], }; diff --git a/Libraries/Components/View/ViewPropTypes.js b/Libraries/Components/View/ViewPropTypes.js index 5cfcd45f24b9a6..73a39cf255fcbf 100644 --- a/Libraries/Components/View/ViewPropTypes.js +++ b/Libraries/Components/View/ViewPropTypes.js @@ -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, + 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. diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index ede084d1a5fb8b..4443f525a6abf3 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -33,9 +33,11 @@ @implementation RCTConvert(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 @@ - (RCTShadowView *)shadowView 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) diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/AccessibilityRoleUtil.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/AccessibilityRoleUtil.java new file mode 100644 index 00000000000000..f063925a23b8f2 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/AccessibilityRoleUtil.java @@ -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: + * + *

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); + } + } +} 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 0fb71f1aa1a685..5b4c312ea5819f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -29,6 +29,7 @@ public abstract class BaseViewManager