`. -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
) but must be treated as such. + if (role === 'link') { if (props.accessible === false || !focusable) { return '-1'; } + } else if (role === 'button') { + if (props.accessible !== false && focusable) { + return '0'; + } } else { if (props.accessible === true && focusable) { return '0'; diff --git a/src/modules/NativeMethodsMixin/index.js b/src/modules/NativeMethodsMixin/index.js index b5e41f79..243b21d9 100644 --- a/src/modules/NativeMethodsMixin/index.js +++ b/src/modules/NativeMethodsMixin/index.js @@ -120,7 +120,7 @@ const NativeMethodsMixin = { const domStyleProps = { classList, style }; // Next DOM state - const domProps = createDOMProps(i18nStyle(nativeProps), style => + const domProps = createDOMProps(null, i18nStyle(nativeProps), style => StyleRegistry.resolveStateful(style, domStyleProps, { i18n: false }) ); UIManager.updateView(node, domProps, this); diff --git a/src/modules/createDOMElement/__tests__/__snapshots__/index-test.js.snap b/src/modules/createDOMElement/__tests__/__snapshots__/index-test.js.snap index 75c2d6f7..2959a1e2 100644 --- a/src/modules/createDOMElement/__tests__/__snapshots__/index-test.js.snap +++ b/src/modules/createDOMElement/__tests__/__snapshots__/index-test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`modules/createDOMElement onClick 1`] = ` +exports[`modules/createDOMElement it normalizes event.nativeEvent 1`] = ` Object { "_normalized": true, "changedTouches": Array [], @@ -15,110 +15,6 @@ Object { } `; -exports[`modules/createDOMElement prop "accessibilityLabel" 1`] = ` - -`; +exports[`modules/createDOMElement it renders different DOM elements 1`] = ``; -exports[`modules/createDOMElement prop "accessibilityLiveRegion" 1`] = ` - -`; - -exports[`modules/createDOMElement prop "accessibilityRole" button 1`] = ` -