From 20718e6b8ce4f5c3a8393067d5e8eb0da910751c Mon Sep 17 00:00:00 2001 From: Gabriel Donadel Dall'Agnol Date: Mon, 24 Oct 2022 08:38:47 -0700 Subject: [PATCH] feat: Add role prop to Text component (#34976) Summary: As pointed out by necolas on https://github.com/facebook/react-native/issues/34424#issuecomment-1261482517 we forgot we add the `role` prop mapping to the `Text` component. This PR adds a new `role` prop to `Text`, mapping the web `role` values to the already existing `accessibilityRole` prop and moves the `roleToAccessibilityRoleMapping` to a common file that can be imported by both the `Text` and `View` components as requested on https://github.com/facebook/react-native/issues/34424. This PR also updates the RNTester AcessebilityExample to include a test using this new prop. ## Changelog [General] [Added] - Add role prop to Text component Pull Request resolved: https://github.com/facebook/react-native/pull/34976 Test Plan: 1. Open the RNTester app and navigate to the Accessibility Example page 2. Test the `role` prop through the `Text with role = heading` section Reviewed By: yungsters Differential Revision: D40596039 Pulled By: jacdebug fbshipit-source-id: f72f02e8bd32169423ea517ad18b598b52257b17 --- Libraries/Components/View/View.js | 71 +------- .../Components/View/ViewAccessibility.d.ts | 71 ++++++++ Libraries/Text/Text.js | 9 ++ Libraries/Text/TextProps.js | 6 + Libraries/Utilities/AcessibilityMapping.js | 152 ++++++++++++++++++ .../Accessibility/AccessibilityExample.js | 4 + 6 files changed, 244 insertions(+), 69 deletions(-) create mode 100644 Libraries/Utilities/AcessibilityMapping.js diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index 129f50d36e2d95..8ef1f814a312fb 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -12,6 +12,7 @@ import type {ViewProps} from './ViewPropTypes'; import flattenStyle from '../../StyleSheet/flattenStyle'; import TextAncestor from '../../Text/TextAncestor'; +import {getAccessibilityRoleFromRole} from '../../Utilities/AcessibilityMapping'; import ViewNativeComponent from './ViewNativeComponent'; import * as React from 'react'; @@ -80,74 +81,6 @@ const View: React.AbstractComponent< text: ariaValueText ?? accessibilityValue?.text, }; - // Map role values to AccessibilityRole values - const roleToAccessibilityRoleMapping = { - alert: 'alert', - alertdialog: undefined, - application: undefined, - article: undefined, - banner: undefined, - button: 'button', - cell: undefined, - checkbox: 'checkbox', - columnheader: undefined, - combobox: 'combobox', - complementary: undefined, - contentinfo: undefined, - definition: undefined, - dialog: undefined, - directory: undefined, - document: undefined, - feed: undefined, - figure: undefined, - form: undefined, - grid: 'grid', - group: undefined, - heading: 'header', - img: 'image', - link: 'link', - list: 'list', - listitem: undefined, - log: undefined, - main: undefined, - marquee: undefined, - math: undefined, - menu: 'menu', - menubar: 'menubar', - menuitem: 'menuitem', - meter: undefined, - navigation: undefined, - none: 'none', - note: undefined, - presentation: 'none', - progressbar: 'progressbar', - radio: 'radio', - radiogroup: 'radiogroup', - region: undefined, - row: undefined, - rowgroup: undefined, - rowheader: undefined, - scrollbar: 'scrollbar', - searchbox: 'search', - separator: undefined, - slider: 'adjustable', - spinbutton: 'spinbutton', - status: undefined, - summary: 'summary', - switch: 'switch', - tab: 'tab', - table: undefined, - tablist: 'tablist', - tabpanel: undefined, - term: undefined, - timer: 'timer', - toolbar: 'toolbar', - tooltip: undefined, - tree: undefined, - treegrid: undefined, - treeitem: undefined, - }; - const flattenedStyle = flattenStyle(style); const newPointerEvents = flattenedStyle?.pointerEvents || pointerEvents; @@ -162,7 +95,7 @@ const View: React.AbstractComponent< focusable={tabIndex !== undefined ? !tabIndex : focusable} accessibilityState={_accessibilityState} accessibilityRole={ - role ? roleToAccessibilityRoleMapping[role] : accessibilityRole + role ? getAccessibilityRoleFromRole(role) : accessibilityRole } accessibilityElementsHidden={ ariaHidden ?? accessibilityElementsHidden diff --git a/Libraries/Components/View/ViewAccessibility.d.ts b/Libraries/Components/View/ViewAccessibility.d.ts index a9ff8baa374686..8f530a78c19848 100644 --- a/Libraries/Components/View/ViewAccessibility.d.ts +++ b/Libraries/Components/View/ViewAccessibility.d.ts @@ -101,6 +101,11 @@ export interface AccessibilityProps 'aria-live'?: ('polite' | 'assertive' | 'off') | undefined; 'aria-modal'?: boolean | undefined; + + /** + * Indicates to accessibility services to treat UI component like a specific role. + */ + role?: Role; } export type AccessibilityActionInfo = Readonly<{ @@ -286,3 +291,69 @@ export interface AccessibilityPropsIOS { */ accessibilityIgnoresInvertColors?: boolean | undefined; } + +export type Role = + | 'alert' + | 'alertdialog' + | 'application' + | 'article' + | 'banner' + | 'button' + | 'cell' + | 'checkbox' + | 'columnheader' + | 'combobox' + | 'complementary' + | 'contentinfo' + | 'definition' + | 'dialog' + | 'directory' + | 'document' + | 'feed' + | 'figure' + | 'form' + | 'grid' + | 'group' + | 'heading' + | 'img' + | 'link' + | 'list' + | 'listitem' + | 'log' + | 'main' + | 'marquee' + | 'math' + | 'menu' + | 'menubar' + | 'menuitem' + | 'meter' + | 'navigation' + | 'none' + | 'note' + | 'presentation' + | 'progressbar' + | 'radio' + | 'radiogroup' + | 'region' + | 'row' + | 'rowgroup' + | 'rowheader' + | 'scrollbar' + | 'searchbox' + | 'separator' + | 'slider' + | 'spinbutton' + | 'status' + | 'summary' + | 'switch' + | 'tab' + | 'table' + | 'tablist' + | 'tabpanel' + | 'term' + | 'timer' + | 'toolbar' + | 'tooltip' + | 'tree' + | 'treegrid' + | 'treeitem'; diff --git a/Libraries/Text/Text.js b/Libraries/Text/Text.js index ac427bf8c736ae..2470a3c2627bc6 100644 --- a/Libraries/Text/Text.js +++ b/Libraries/Text/Text.js @@ -15,6 +15,7 @@ import usePressability from '../Pressability/usePressability'; import flattenStyle from '../StyleSheet/flattenStyle'; import processColor from '../StyleSheet/processColor'; import StyleSheet from '../StyleSheet/StyleSheet'; +import {getAccessibilityRoleFromRole} from '../Utilities/AcessibilityMapping'; import Platform from '../Utilities/Platform'; import TextAncestor from './TextAncestor'; import {NativeText, NativeVirtualText} from './TextNativeComponent'; @@ -34,6 +35,7 @@ const Text: React.AbstractComponent< const { accessible, accessibilityLabel, + accessibilityRole, allowFontScaling, 'aria-busy': ariaBusy, 'aria-checked': ariaChecked, @@ -55,6 +57,7 @@ const Text: React.AbstractComponent< onResponderTerminationRequest, onStartShouldSetResponder, pressRetentionOffset, + role, suppressHighlighting, ...restProps } = props; @@ -223,6 +226,9 @@ const Text: React.AbstractComponent< accessibilityState={_accessibilityState} {...eventHandlersForText} accessibilityLabel={ariaLabel ?? accessibilityLabel} + accessibilityRole={ + role ? getAccessibilityRoleFromRole(role) : accessibilityRole + } isHighlighted={isHighlighted} isPressable={isPressable} selectable={_selectable} @@ -246,6 +252,9 @@ const Text: React.AbstractComponent< } accessibilityLabel={ariaLabel ?? accessibilityLabel} accessibilityState={nativeTextAccessibilityState} + accessibilityRole={ + role ? getAccessibilityRoleFromRole(role) : accessibilityRole + } allowFontScaling={allowFontScaling !== false} ellipsizeMode={ellipsizeMode ?? 'tail'} isHighlighted={isHighlighted} diff --git a/Libraries/Text/TextProps.js b/Libraries/Text/TextProps.js index 3a927ae48681e4..4ef9e7cec3ce39 100644 --- a/Libraries/Text/TextProps.js +++ b/Libraries/Text/TextProps.js @@ -15,6 +15,7 @@ import type { AccessibilityActionInfo, AccessibilityRole, AccessibilityState, + Role, } from '../Components/View/ViewAccessibility'; import type {TextStyleProp} from '../StyleSheet/StyleSheet'; import type { @@ -176,6 +177,11 @@ export type TextProps = $ReadOnly<{| */ pressRetentionOffset?: ?PressRetentionOffset, + /** + * Indicates to accessibility services to treat UI component like a specific role. + */ + role?: ?Role, + /** * Lets the user select text. * diff --git a/Libraries/Utilities/AcessibilityMapping.js b/Libraries/Utilities/AcessibilityMapping.js new file mode 100644 index 00000000000000..8db68118821dd8 --- /dev/null +++ b/Libraries/Utilities/AcessibilityMapping.js @@ -0,0 +1,152 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type { + AccessibilityRole, + Role, +} from '../Components/View/ViewAccessibility'; + +// Map role values to AccessibilityRole values +export function getAccessibilityRoleFromRole(role: Role): ?AccessibilityRole { + switch (role) { + case 'alert': + return 'alert'; + case 'alertdialog': + return undefined; + case 'application': + return undefined; + case 'article': + return undefined; + case 'banner': + return undefined; + case 'button': + return 'button'; + case 'cell': + return undefined; + case 'checkbox': + return 'checkbox'; + case 'columnheader': + return undefined; + case 'combobox': + return 'combobox'; + case 'complementary': + return undefined; + case 'contentinfo': + return undefined; + case 'definition': + return undefined; + case 'dialog': + return undefined; + case 'directory': + return undefined; + case 'document': + return undefined; + case 'feed': + return undefined; + case 'figure': + return undefined; + case 'form': + return undefined; + case 'grid': + return 'grid'; + case 'group': + return undefined; + case 'heading': + return 'header'; + case 'img': + return 'image'; + case 'link': + return 'link'; + case 'list': + return 'list'; + case 'listitem': + return undefined; + case 'log': + return undefined; + case 'main': + return undefined; + case 'marquee': + return undefined; + case 'math': + return undefined; + case 'menu': + return 'menu'; + case 'menubar': + return 'menubar'; + case 'menuitem': + return 'menuitem'; + case 'meter': + return undefined; + case 'navigation': + return undefined; + case 'none': + return 'none'; + case 'note': + return undefined; + case 'presentation': + return 'none'; + case 'progressbar': + return 'progressbar'; + case 'radio': + return 'radio'; + case 'radiogroup': + return 'radiogroup'; + case 'region': + return undefined; + case 'row': + return undefined; + case 'rowgroup': + return undefined; + case 'rowheader': + return undefined; + case 'scrollbar': + return 'scrollbar'; + case 'searchbox': + return 'search'; + case 'separator': + return undefined; + case 'slider': + return 'adjustable'; + case 'spinbutton': + return 'spinbutton'; + case 'status': + return undefined; + case 'summary': + return 'summary'; + case 'switch': + return 'switch'; + case 'tab': + return 'tab'; + case 'table': + return undefined; + case 'tablist': + return 'tablist'; + case 'tabpanel': + return undefined; + case 'term': + return undefined; + case 'timer': + return 'timer'; + case 'toolbar': + return 'toolbar'; + case 'tooltip': + return undefined; + case 'tree': + return undefined; + case 'treegrid': + return undefined; + case 'treeitem': + return undefined; + } + + return undefined; +} diff --git a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js index 9efdbfdb2da011..c552f75699656a 100644 --- a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js +++ b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js @@ -157,6 +157,10 @@ class AccessibilityExample extends React.Component<{}> { This is a title. + + This is a title. + + Alert.alert('Link has been clicked!')}