From 1aeac1c62528004d994200664368dc85fba1795d Mon Sep 17 00:00:00 2001 From: Marc Mulcahy Date: Thu, 25 Apr 2019 06:09:35 -0700 Subject: [PATCH] Additional Accessibility Roles and States (#24095) Summary: Assistive technologies use the accessibility role of a component to tell the disabled user what the component is, and provide hints about how to use it. Many important roles do not have analog AccessibilityTraits on iOS. This PR adds many critical roles, such as editabletext, checkbox, menu, and switch to name a few. Accessibility states are used to convey the current state of a component. This PR adds several critical states such as checked, unchecked, on and off. [general] [change] - Adds critical accessibility roles and states. Pull Request resolved: https://github.com/facebook/react-native/pull/24095 Differential Revision: D15079245 Pulled By: cpojer fbshipit-source-id: 941b30eb8f5d565597e5ea3a04687d9809cbe372 --- .../Components/View/ViewAccessibility.js | 29 +- .../DeprecatedViewAccessibility.js | 27 +- RNTester/js/AccessibilityExample.js | 278 ++++++++++++++++++ React/Views/RCTView.h | 4 + React/Views/RCTView.m | 63 ++++ React/Views/RCTViewManager.m | 58 +++- .../uimanager/AccessibilityDelegateUtil.java | 260 ++++++++++------ .../react/uimanager/BaseViewManager.java | 48 ++- .../main/res/views/uimanager/values/ids.xml | 6 + .../res/views/uimanager/values/strings.xml | 90 +++++- 10 files changed, 743 insertions(+), 120 deletions(-) diff --git a/Libraries/Components/View/ViewAccessibility.js b/Libraries/Components/View/ViewAccessibility.js index 42f85f827a7bdc..316ca494b71cc3 100644 --- a/Libraries/Components/View/ViewAccessibility.js +++ b/Libraries/Components/View/ViewAccessibility.js @@ -22,7 +22,32 @@ export type AccessibilityRole = | 'adjustable' | 'imagebutton' | 'header' - | 'summary'; + | 'summary' + | 'alert' + | 'checkbox' + | 'combobox' + | 'menu' + | 'menubar' + | 'menuitem' + | 'progressbar' + | 'radio' + | 'radiogroup' + | 'scrollbar' + | 'spinbutton' + | 'switch' + | 'tab' + | 'tablist' + | 'timer' + | 'toolbar'; // This must be kept in sync with the AccessibilityStatesMask in RCTViewManager.m -export type AccessibilityStates = $ReadOnlyArray<'disabled' | 'selected'>; +export type AccessibilityStates = $ReadOnlyArray< + | 'disabled' + | 'selected' + | 'checked' + | 'unchecked' + | 'busy' + | 'expanded' + | 'collapsed' + | 'hasPopup', +>; diff --git a/Libraries/DeprecatedPropTypes/DeprecatedViewAccessibility.js b/Libraries/DeprecatedPropTypes/DeprecatedViewAccessibility.js index 5b5c8a8164757b..4b52b7330d4efd 100644 --- a/Libraries/DeprecatedPropTypes/DeprecatedViewAccessibility.js +++ b/Libraries/DeprecatedPropTypes/DeprecatedViewAccessibility.js @@ -24,7 +24,32 @@ module.exports = { 'imagebutton', 'header', 'summary', + 'alert', + 'checkbox', + 'combobox', + 'menu', + 'menubar', + 'menuitem', + 'progressbar', + 'radio', + 'radiogroup', + 'scrollbar', + 'spinbutton', + 'switch', + 'tab', + 'tablist', + 'timer', + 'toolbar', ], // This must be kept in sync with the AccessibilityStatesMask in RCTViewManager.m - DeprecatedAccessibilityStates: ['selected', 'disabled'], + DeprecatedAccessibilityStates: [ + 'selected', + 'disabled', + 'checked', + 'unchecked', + 'busy', + 'expanded', + 'collapsed', + 'hasPopup', + ], }; diff --git a/RNTester/js/AccessibilityExample.js b/RNTester/js/AccessibilityExample.js index 1653ffec432cee..8bb29c3e28695f 100644 --- a/RNTester/js/AccessibilityExample.js +++ b/RNTester/js/AccessibilityExample.js @@ -12,10 +12,14 @@ const React = require('react'); const { AccessibilityInfo, + Button, Text, View, TouchableOpacity, Alert, + UIManager, + findNodeHandle, + Platform, } = require('react-native'); const RNTesterBlock = require('./RNTesterBlock'); @@ -138,6 +142,274 @@ class AccessibilityExample extends React.Component { } } +class CheckboxExample extends React.Component { + state = { + checkboxState: 'checked', + }; + + _onCheckboxPress = () => { + const checkboxState = + this.state.checkboxState === 'checked' ? 'unchecked' : 'checked'; + + this.setState({ + checkboxState: checkboxState, + }); + + if (Platform.OS === 'android') { + UIManager.sendAccessibilityEvent( + findNodeHandle(this), + UIManager.AccessibilityEventTypes.typeViewClicked, + ); + } + }; + + render() { + return ( + + Checkbox example + + ); + } +} + +class SwitchExample extends React.Component { + state = { + switchState: 'checked', + }; + + _onSwitchToggle = () => { + const switchState = + this.state.switchState === 'checked' ? 'unchecked' : 'checked'; + + this.setState({ + switchState: switchState, + }); + + if (Platform.OS === 'android') { + UIManager.sendAccessibilityEvent( + findNodeHandle(this), + UIManager.AccessibilityEventTypes.typeViewClicked, + ); + } + }; + + render() { + return ( + + Switch example + + ); + } +} + +class SelectionExample extends React.Component { + constructor(props) { + super(props); + this.selectableElement = React.createRef(); + } + + state = { + isSelected: true, + isEnabled: false, + }; + + render() { + let accessibilityStates = []; + let accessibilityHint = 'click me to select'; + if (this.state.isSelected) { + accessibilityStates.push('selected'); + accessibilityHint = 'click me to unselect'; + } + if (!this.state.isEnabled) { + accessibilityStates.push('disabled'); + accessibilityHint = 'use the button on the right to enable selection'; + } + let buttonTitle = this.state.isEnabled + ? 'Disable selection' + : 'Enable selection'; + + return ( + + { + this.setState({ + isSelected: !this.state.isSelected, + }); + + if (Platform.OS === 'android') { + UIManager.sendAccessibilityEvent( + findNodeHandle(this.selectableElement.current), + UIManager.AccessibilityEventTypes.typeViewClicked, + ); + } + }} + accessibilityLabel="element 19" + accessibilityStates={accessibilityStates} + accessibilityHint={accessibilityHint}> + Selectable element example + +