diff --git a/docs/guides/accessibility.md b/docs/guides/accessibility.md
index f26399ea..65cd5e82 100644
--- a/docs/guides/accessibility.md
+++ b/docs/guides/accessibility.md
@@ -40,35 +40,40 @@ using `aria-label`.
In some cases, we also want to alert the end user of the type of selected
component (i.e., that it is a “button”). To provide more context to screen
readers, you should specify the `accessibilityRole` property. (Note that React
-Native for Web provides a compatibility mapping of equivalent
+Native for Web also provides a compatibility mapping of equivalent
`accessibilityTraits` and `accessibilityComponentType` values to
`accessibilityRole`).
The `accessibilityRole` prop is used to infer an [analogous HTML
-element][html-aria-url] to use in addition to the resulting ARIA `role`, where
-possible. While this may contradict some ARIA recommendations, it also helps
-avoid certain HTML5 conformance errors and accessibility anti-patterns (e.g.,
-giving a `heading` role to a `button` element).
+element][html-aria-url] and ARIA `role`, where possible. In most cases, both
+the element and ARIA `role` are rendered. While this may contradict some ARIA
+recommendations, it also helps avoid certain HTML5 conformance errors and
+accessibility anti-patterns (e.g., giving a `heading` role to a `button`
+element) and browser bugs.
For example:
* `` => ``.
* `` => ``.
-* `` => ``.
+* `` => `
`.
+* `` => ``.
* `` => ``.
* `` => ``.
-In the example below, the `TouchableWithoutFeedback` is announced by screen
-readers as a native Button.
+In the example below, the `TouchableHighlight` is announced by screen
+readers as a button.
-```
-
+```js
+Press me!
-
+
```
+Note: The `button` role is not implemented using the native `button` element
+due to browsers limiting the use of flexbox layout on its children.
+
Note: Avoid changing `accessibilityRole` values over time or after user
actions. Generally, accessibility APIs do not provide a means of notifying
assistive technologies of a `role` value change.
diff --git a/src/apis/AppRegistry/__tests__/__snapshots__/renderApplication-test.js.snap b/src/apis/AppRegistry/__tests__/__snapshots__/renderApplication-test.js.snap
index e9e68268..ee0f15ff 100644
--- a/src/apis/AppRegistry/__tests__/__snapshots__/renderApplication-test.js.snap
+++ b/src/apis/AppRegistry/__tests__/__snapshots__/renderApplication-test.js.snap
@@ -30,6 +30,7 @@ exports[`apis/AppRegistry/renderApplication getApplication 3`] = `
.rn-position-bnwqim{position:relative}
.rn-right-zchlnj{right:0px}
.rn-top-ipm5af{top:0px}
+.rn-cursor-1loqt21{cursor:pointer}
.rn-appearance-30o5oe{-moz-appearance:none;-webkit-appearance:none;appearance:none}
.rn-backgroundColor-wib322{background-color:transparent}
.rn-color-homxoj{color:inherit}
diff --git a/src/apis/StyleSheet/StyleRegistry.js b/src/apis/StyleSheet/StyleRegistry.js
index 3a3721e5..d6a0a298 100644
--- a/src/apis/StyleSheet/StyleRegistry.js
+++ b/src/apis/StyleSheet/StyleRegistry.js
@@ -66,7 +66,7 @@ export default class StyleRegistry {
*/
resolve(reactNativeStyle, options = emptyObject) {
if (!reactNativeStyle) {
- return undefined;
+ return emptyObject;
}
// fast and cachable
diff --git a/src/components/Switch/__tests__/__snapshots__/index-test.js.snap b/src/components/Switch/__tests__/__snapshots__/index-test.js.snap
index 01df8163..91a3e45e 100644
--- a/src/components/Switch/__tests__/__snapshots__/index-test.js.snap
+++ b/src/components/Switch/__tests__/__snapshots__/index-test.js.snap
@@ -34,6 +34,7 @@ exports[`components/Switch disabled when "true" a disabled checkbox is rendered
style="height:20px;width:20px;"
/>
{
}
});
- test('modifier keys', done => {
+ test('modifier keys are included', done => {
const input = findNativeInput(mount());
input.simulate('keyPress', {
altKey: true,
diff --git a/src/components/View/__tests__/index-test.js b/src/components/View/__tests__/index-test.js
index fcd91003..4a604364 100644
--- a/src/components/View/__tests__/index-test.js
+++ b/src/components/View/__tests__/index-test.js
@@ -10,11 +10,6 @@ describe('components/View', () => {
const component = shallow();
expect(component.type()).toBe('div');
});
-
- test('is a "span" when inside ', () => {
- const component = render();
- expect(component.find('span').length).toEqual(1);
- });
});
test('prop "children"', () => {
diff --git a/src/components/View/index.js b/src/components/View/index.js
index 7523bd5f..2b0eb4d9 100644
--- a/src/components/View/index.js
+++ b/src/components/View/index.js
@@ -7,7 +7,6 @@
* @flow
*/
-import AccessibilityUtil from '../../modules/AccessibilityUtil';
import applyLayout from '../../modules/applyLayout';
import applyNativeMethods from '../../modules/applyNativeMethods';
import { bool } from 'prop-types';
@@ -16,8 +15,6 @@ import StyleSheet from '../../apis/StyleSheet';
import ViewPropTypes from './ViewPropTypes';
import React, { Component } from 'react';
-const emptyObject = {};
-
const calculateHitSlopStyle = hitSlop => {
const hitStyle = {};
for (const prop in hitSlop) {
@@ -32,23 +29,12 @@ const calculateHitSlopStyle = hitSlop => {
class View extends Component {
static displayName = 'View';
- static childContextTypes = {
- isInAButtonView: bool
- };
-
static contextTypes = {
- isInAButtonView: bool,
isInAParentText: bool
};
static propTypes = ViewPropTypes;
- getChildContext() {
- const isInAButtonView =
- AccessibilityUtil.propsToAriaRole(this.props) === 'button' || this.context.isInAButtonView;
- return isInAButtonView ? { isInAButtonView } : emptyObject;
- }
-
render() {
const {
hitSlop,
@@ -63,7 +49,7 @@ class View extends Component {
...otherProps
} = this.props;
- const { isInAButtonView, isInAParentText } = this.context;
+ const { isInAParentText } = this.context;
otherProps.style = [styles.initial, isInAParentText && styles.inline, style];
@@ -76,7 +62,7 @@ class View extends Component {
}
// avoid HTML validation errors
- const component = isInAButtonView ? 'span' : 'div';
+ const component = 'div';
return createDOMElement(component, otherProps);
}
diff --git a/src/modules/AccessibilityUtil/__tests__/propsToAccessibilityComponent.js b/src/modules/AccessibilityUtil/__tests__/propsToAccessibilityComponent.js
new file mode 100644
index 00000000..f7386ce9
--- /dev/null
+++ b/src/modules/AccessibilityUtil/__tests__/propsToAccessibilityComponent.js
@@ -0,0 +1,27 @@
+/* eslint-env jasmine, jest */
+
+import propsToAccessibilityComponent from '../propsToAccessibilityComponent';
+
+describe('modules/AccessibilityUtil/propsToAccessibilityComponent', () => {
+ test('when missing accessibility props"', () => {
+ expect(propsToAccessibilityComponent({})).toBeUndefined();
+ });
+
+ test('when "accessibilityRole" is "button"', () => {
+ expect(propsToAccessibilityComponent({ accessibilityRole: 'button' })).toBeUndefined();
+ });
+
+ test('when "accessibilityRole" is "heading"', () => {
+ expect(propsToAccessibilityComponent({ accessibilityRole: 'heading' })).toEqual('h1');
+ });
+
+ test('when "accessibilityRole" is "heading" and "aria-level" is set', () => {
+ expect(
+ propsToAccessibilityComponent({ accessibilityRole: 'heading', 'aria-level': 3 })
+ ).toEqual('h3');
+ });
+
+ test('when "accessibilityRole" is "label"', () => {
+ expect(propsToAccessibilityComponent({ accessibilityRole: 'label' })).toEqual('label');
+ });
+});
diff --git a/src/modules/AccessibilityUtil/__tests__/propsToAriaRole-test.js b/src/modules/AccessibilityUtil/__tests__/propsToAriaRole-test.js
index 46a2fe33..cab19565 100644
--- a/src/modules/AccessibilityUtil/__tests__/propsToAriaRole-test.js
+++ b/src/modules/AccessibilityUtil/__tests__/propsToAriaRole-test.js
@@ -3,15 +3,15 @@
import propsToAriaRole from '../propsToAriaRole';
describe('modules/AccessibilityUtil/propsToAriaRole', () => {
- test('returns undefined when missing accessibility props', () => {
+ test('when missing accessibility props', () => {
expect(propsToAriaRole({})).toBeUndefined();
});
- test('returns value of "accessibilityRole" when defined', () => {
+ test('when "accessibilityRole" is defined', () => {
expect(propsToAriaRole({ accessibilityRole: 'banner' })).toEqual('banner');
});
- test('returns "button" when iOS/Android accessibility prop equals "button"', () => {
+ test('when iOS/Android accessibility prop equals "button"', () => {
expect(propsToAriaRole({ accessibilityComponentType: 'button' })).toEqual('button');
expect(propsToAriaRole({ accessibilityTraits: 'button' })).toEqual('button');
});
diff --git a/src/modules/AccessibilityUtil/__tests__/propsToTabIndex-test.js b/src/modules/AccessibilityUtil/__tests__/propsToTabIndex-test.js
new file mode 100644
index 00000000..6e9d0631
--- /dev/null
+++ b/src/modules/AccessibilityUtil/__tests__/propsToTabIndex-test.js
@@ -0,0 +1,116 @@
+/* eslint-env jasmine, jest */
+
+import propsToTabIndex from '../propsToTabIndex';
+
+describe('modules/AccessibilityUtil/propsToTabIndex', () => {
+ test('with no accessibility props', () => {
+ expect(propsToTabIndex({})).toBeUndefined();
+ });
+
+ describe('"accessibilityRole" of "link"', () => {
+ const accessibilityRole = 'link';
+
+ test('default case', () => {
+ expect(propsToTabIndex({ accessibilityRole })).toBeUndefined();
+ });
+
+ test('when "accessible" is true', () => {
+ expect(propsToTabIndex({ accessibilityRole, accessible: true })).toBeUndefined();
+ });
+
+ test('when "accessible" is false', () => {
+ expect(propsToTabIndex({ accessibilityRole, accessible: false })).toEqual('-1');
+ });
+
+ test('when "disabled" is true', () => {
+ expect(propsToTabIndex({ accessibilityRole, disabled: true })).toEqual('-1');
+ expect(propsToTabIndex({ accessibilityRole, 'aria-disabled': true })).toEqual('-1');
+ });
+
+ test('when "disabled" is false', () => {
+ expect(propsToTabIndex({ accessibilityRole, disabled: false })).toBeUndefined();
+ expect(propsToTabIndex({ accessibilityRole, 'aria-disabled': false })).toBeUndefined();
+ });
+
+ test('when "importantForAccessibility" is "no"', () => {
+ expect(propsToTabIndex({ accessibilityRole, importantForAccessibility: 'no' })).toEqual('-1');
+ });
+
+ test('when "importantForAccessibility" is "no-hide-descendants"', () => {
+ expect(
+ propsToTabIndex({
+ accessibilityRole,
+ importantForAccessibility: 'no-hide-descendants'
+ })
+ ).toEqual('-1');
+ });
+ });
+
+ describe('"accessibilityRole" of "button"', () => {
+ const accessibilityRole = 'button';
+
+ test('default case', () => {
+ expect(propsToTabIndex({ accessibilityRole })).toEqual('0');
+ });
+
+ test('when "accessible" is true', () => {
+ expect(propsToTabIndex({ accessibilityRole, accessible: true })).toEqual('0');
+ });
+
+ test('when "accessible" is false', () => {
+ expect(propsToTabIndex({ accessibilityRole, accessible: false })).toBeUndefined();
+ });
+
+ test('when "disabled" is true', () => {
+ expect(propsToTabIndex({ accessibilityRole, disabled: true })).toBeUndefined();
+ expect(propsToTabIndex({ accessibilityRole, 'aria-disabled': true })).toBeUndefined();
+ });
+
+ test('when "disabled" is false', () => {
+ expect(propsToTabIndex({ accessibilityRole, disabled: false })).toEqual('0');
+ expect(propsToTabIndex({ accessibilityRole, 'aria-disabled': false })).toEqual('0');
+ });
+
+ test('when "importantForAccessibility" is "no"', () => {
+ expect(
+ propsToTabIndex({ accessibilityRole, importantForAccessibility: 'no' })
+ ).toBeUndefined();
+ });
+
+ test('when "importantForAccessibility" is "no-hide-descendants"', () => {
+ expect(
+ propsToTabIndex({
+ accessibilityRole,
+ importantForAccessibility: 'no-hide-descendants'
+ })
+ ).toBeUndefined();
+ });
+ });
+
+ describe('with unfocusable accessibilityRole', () => {
+ test('default case', () => {
+ expect(propsToTabIndex({})).toBeUndefined();
+ });
+
+ test('when "accessible" is true', () => {
+ expect(propsToTabIndex({ accessible: true })).toEqual('0');
+ });
+
+ test('when "accessible" is false', () => {
+ expect(propsToTabIndex({ accessible: false })).toBeUndefined();
+ });
+
+ test('when "importantForAccessibility" is "no"', () => {
+ expect(propsToTabIndex({ importantForAccessibility: 'no' })).toBeUndefined();
+ expect(
+ propsToTabIndex({ accessible: true, importantForAccessibility: 'no' })
+ ).toBeUndefined();
+ });
+
+ test('when "importantForAccessibility" is "no-hide-descendants"', () => {
+ expect(
+ propsToTabIndex({ accessible: true, importantForAccessibility: 'no-hide-descendants' })
+ ).toBeUndefined();
+ });
+ });
+});
diff --git a/src/modules/AccessibilityUtil/__tests__/propsToTabIndex.js b/src/modules/AccessibilityUtil/__tests__/propsToTabIndex.js
deleted file mode 100644
index 32bdf20e..00000000
--- a/src/modules/AccessibilityUtil/__tests__/propsToTabIndex.js
+++ /dev/null
@@ -1,74 +0,0 @@
-/* eslint-env jasmine, jest */
-
-import propsToTabIndex from '../propsToTabIndex';
-
-describe('modules/AccessibilityUtil/propsToTabIndex', () => {
- test('returns undefined when missing accessibility props', () => {
- expect(propsToTabIndex({})).toBeUndefined();
- });
-
- describe('with focusable accessibilityRole', () => {
- test('returns "undefined" by default', () => {
- expect(propsToTabIndex({ accessibilityRole: 'button' })).toBeUndefined();
- expect(propsToTabIndex({ accessibilityRole: 'link' })).toBeUndefined();
- });
-
- test('returns "undefined" when "accessible" is true', () => {
- expect(propsToTabIndex({ accessibilityRole: 'button', accessible: true })).toBeUndefined();
- });
-
- test('returns "-1" when "accessible" is false', () => {
- expect(propsToTabIndex({ accessibilityRole: 'button', accessible: false })).toEqual('-1');
- });
-
- test('returns "-1" when "disabled" is true', () => {
- expect(propsToTabIndex({ accessibilityRole: 'button', disabled: true })).toEqual('-1');
- });
-
- test('returns "undefined" when "disabled" is false', () => {
- expect(propsToTabIndex({ accessibilityRole: 'button', disabled: false })).toBeUndefined();
- });
-
- test('returns "-1" when "importantForAccessibility" is "no"', () => {
- expect(
- propsToTabIndex({ accessibilityRole: 'button', importantForAccessibility: 'no' })
- ).toEqual('-1');
- });
-
- test('returns "-1" when "importantForAccessibility" is "no-hide-descendants"', () => {
- expect(
- propsToTabIndex({
- accessibilityRole: 'button',
- importantForAccessibility: 'no-hide-descendants'
- })
- ).toEqual('-1');
- });
- });
-
- describe('with unfocusable accessibilityRole', () => {
- test('returns "undefined" by default', () => {
- expect(propsToTabIndex({})).toBeUndefined();
- });
-
- test('returns "0" when "accessible" is true', () => {
- expect(propsToTabIndex({ accessible: true })).toEqual('0');
- });
-
- test('returns "undefined" when "accessible" is false', () => {
- expect(propsToTabIndex({ accessible: false })).toBeUndefined();
- });
-
- test('returns "undefined" when "importantForAccessibility" is "no"', () => {
- expect(propsToTabIndex({ importantForAccessibility: 'no' })).toBeUndefined();
- expect(
- propsToTabIndex({ accessible: true, importantForAccessibility: 'no' })
- ).toBeUndefined();
- });
-
- test('returns "undefined" when "importantForAccessibility" is "no-hide-descendants"', () => {
- expect(
- propsToTabIndex({ accessible: true, importantForAccessibility: 'no-hide-descendants' })
- ).toBeUndefined();
- });
- });
-});
diff --git a/src/modules/AccessibilityUtil/index.js b/src/modules/AccessibilityUtil/index.js
index 8b4f095d..3fcf3ff8 100644
--- a/src/modules/AccessibilityUtil/index.js
+++ b/src/modules/AccessibilityUtil/index.js
@@ -8,11 +8,13 @@
* @flow
*/
+import isDisabled from './isDisabled';
import propsToAccessibilityComponent from './propsToAccessibilityComponent';
import propsToAriaRole from './propsToAriaRole';
import propsToTabIndex from './propsToTabIndex';
const AccessibilityUtil = {
+ isDisabled,
propsToAccessibilityComponent,
propsToAriaRole,
propsToTabIndex
diff --git a/src/modules/AccessibilityUtil/isDisabled.js b/src/modules/AccessibilityUtil/isDisabled.js
new file mode 100644
index 00000000..306fb2fd
--- /dev/null
+++ b/src/modules/AccessibilityUtil/isDisabled.js
@@ -0,0 +1,13 @@
+/**
+ * Copyright (c) 2017-present, Nicolas Gallagher.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+const isDisabled = (props: Object) => props.disabled || props['aria-disabled'];
+
+export default isDisabled;
diff --git a/src/modules/AccessibilityUtil/propsToAccessibilityComponent.js b/src/modules/AccessibilityUtil/propsToAccessibilityComponent.js
index b94dc3d6..c6fb31c4 100644
--- a/src/modules/AccessibilityUtil/propsToAccessibilityComponent.js
+++ b/src/modules/AccessibilityUtil/propsToAccessibilityComponent.js
@@ -5,7 +5,7 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*
- * @noflow
+ * @flow
*/
import propsToAriaRole from './propsToAriaRole';
@@ -13,10 +13,10 @@ import propsToAriaRole from './propsToAriaRole';
const roleComponents = {
article: 'article',
banner: 'header',
- button: 'button',
complementary: 'aside',
contentinfo: 'footer',
form: 'form',
+ label: 'label',
link: 'a',
list: 'ul',
listitem: 'li',
@@ -27,13 +27,15 @@ const roleComponents = {
const emptyObject = {};
-const propsToAccessibilityComponent = (props = emptyObject) => {
+const propsToAccessibilityComponent = (props: Object = emptyObject) => {
const role = propsToAriaRole(props);
- if (role === 'heading') {
- const level = props['aria-level'] || 1;
- return `h${level}`;
+ if (role) {
+ if (role === 'heading') {
+ const level = props['aria-level'] || 1;
+ return `h${level}`;
+ }
+ return roleComponents[role];
}
- return roleComponents[role];
};
export default propsToAccessibilityComponent;
diff --git a/src/modules/AccessibilityUtil/propsToAriaRole.js b/src/modules/AccessibilityUtil/propsToAriaRole.js
index b05e49ec..2e1c7605 100644
--- a/src/modules/AccessibilityUtil/propsToAriaRole.js
+++ b/src/modules/AccessibilityUtil/propsToAriaRole.js
@@ -5,7 +5,7 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*
- * @noflow
+ * @flow
*/
const accessibilityComponentTypeToRole = {
@@ -24,11 +24,16 @@ const accessibilityTraitsToRole = {
summary: 'region'
};
+/**
+ * Provides compatibility with React Native's "accessibilityTraits" (iOS) and
+ * "accessibilityComponentType" (Android), converting them to equivalent ARIA
+ * roles.
+ */
const propsToAriaRole = ({
accessibilityComponentType,
accessibilityRole,
accessibilityTraits
-}) => {
+}: Object) => {
if (accessibilityRole) {
return accessibilityRole;
}
diff --git a/src/modules/AccessibilityUtil/propsToTabIndex.js b/src/modules/AccessibilityUtil/propsToTabIndex.js
index a3369890..9edfe5e9 100644
--- a/src/modules/AccessibilityUtil/propsToTabIndex.js
+++ b/src/modules/AccessibilityUtil/propsToTabIndex.js
@@ -5,23 +5,29 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*
- * @noflow
+ * @flow
*/
+import isDisabled from './isDisabled';
import propsToAriaRole from './propsToAriaRole';
-const propsToTabIndex = props => {
- const ariaRole = propsToAriaRole(props);
+const propsToTabIndex = (props: Object) => {
+ const role = propsToAriaRole(props);
const focusable =
- props.disabled !== true &&
+ !isDisabled(props) &&
props.importantForAccessibility !== 'no' &&
props.importantForAccessibility !== 'no-hide-descendants';
- const focusableRole = ariaRole === 'button' || ariaRole === 'link';
- if (focusableRole) {
+ // Assume that 'link' is focusable by default (uses ).
+ // Assume that 'button' is not (uses