diff --git a/packages/react-native-web/src/exports/createElement/__tests__/index-test.js b/packages/react-native-web/src/exports/createElement/__tests__/index-test.js index a4d5db0d..4d4b9556 100644 --- a/packages/react-native-web/src/exports/createElement/__tests__/index-test.js +++ b/packages/react-native-web/src/exports/createElement/__tests__/index-test.js @@ -36,24 +36,38 @@ describe('modules/createElement', () => { expect(component.find('div').length).toBe(1); }); - [{ disabled: true }, { disabled: false }].forEach(({ disabled }) => { - describe(`value is "button" and disabled is "${disabled}"`, () => { - [{ name: 'Enter', which: 13 }, { name: 'Space', which: 32 }].forEach(({ name, which }) => { - test(`"onClick" is ${disabled ? 'not ' : ''}called when "${name}" is pressed`, () => { - const onClick = jest.fn(); - const component = shallow( - createElement('span', { accessibilityRole: 'button', disabled, onClick }) - ); - component.find('span').simulate('keyPress', { - isDefaultPrevented() {}, - nativeEvent: {}, - preventDefault() {}, - which - }); - expect(onClick).toHaveBeenCalledTimes(disabled ? 0 : 1); + const testRole = ({ accessibilityRole, disabled }) => { + [{ key: 'Enter', which: 13 }, { key: 'Space', which: 32 }].forEach(({ key, which }) => { + test(`"onClick" is ${disabled ? 'not ' : ''}called when "${key}" key is pressed`, () => { + const onClick = jest.fn(); + const component = shallow( + createElement('span', { accessibilityRole, disabled, onClick }) + ); + component.find('span').simulate('keyPress', { + isDefaultPrevented() {}, + nativeEvent: {}, + preventDefault() {}, + which }); + expect(onClick).toHaveBeenCalledTimes(disabled ? 0 : 1); }); }); + }; + + describe('value is "button" and disabled is "true"', () => { + testRole({ accessibilityRole: 'button', disabled: true }); + }); + + describe('value is "button" and disabled is "false"', () => { + testRole({ accessibilityRole: 'button', disabled: false }); + }); + + describe('value is "menuitem" and disabled is "true"', () => { + testRole({ accessibilityRole: 'menuitem', disabled: true }); + }); + + describe('value is "menuitem" and disabled is "false"', () => { + testRole({ accessibilityRole: 'menuitem', disabled: false }); }); }); }); diff --git a/packages/react-native-web/src/exports/createElement/index.js b/packages/react-native-web/src/exports/createElement/index.js index 10d3f460..db3c914b 100644 --- a/packages/react-native-web/src/exports/createElement/index.js +++ b/packages/react-native-web/src/exports/createElement/index.js @@ -48,7 +48,7 @@ const eventHandlerNames = { const adjustProps = domProps => { const { onClick, onResponderRelease, role } = domProps; - const isButtonRole = role === 'button'; + const isButtonLikeRole = AccessibilityUtil.buttonLikeRoles[role]; const isDisabled = AccessibilityUtil.isDisabled(domProps); const isLinkRole = role === 'link'; @@ -56,7 +56,7 @@ const adjustProps = domProps => { const prop = domProps[propName]; const isEventHandler = typeof prop === 'function' && eventHandlerNames[propName]; if (isEventHandler) { - if (isButtonRole && isDisabled) { + if (isButtonLikeRole && isDisabled) { domProps[propName] = undefined; } else { // TODO: move this out of the render path @@ -80,8 +80,8 @@ const adjustProps = domProps => { }; } - // Button role should trigger 'onClick' if SPACE or ENTER keys are pressed. - if (isButtonRole && !isDisabled) { + // Button-like roles should trigger 'onClick' if SPACE or ENTER keys are pressed. + if (isButtonLikeRole && !isDisabled) { domProps.onKeyPress = function(e) { if (!e.isDefaultPrevented() && (e.which === 13 || e.which === 32)) { e.preventDefault(); diff --git a/packages/react-native-web/src/modules/AccessibilityUtil/buttonLikeRoles.js b/packages/react-native-web/src/modules/AccessibilityUtil/buttonLikeRoles.js new file mode 100644 index 00000000..61a42c41 --- /dev/null +++ b/packages/react-native-web/src/modules/AccessibilityUtil/buttonLikeRoles.js @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2017-present, Nicolas Gallagher. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +const buttonLikeRoles: { [string]: boolean } = { + // ARIA button behaves like native 'button' element + button: true, + // ARIA menuitem responds to Enter/Space like a button. Spec requires AT to + // ignore ARIA roles of any children. + // https://www.w3.org/WAI/GL/wiki/Using_ARIA_menus + menuitem: true +}; + +export default buttonLikeRoles; diff --git a/packages/react-native-web/src/modules/AccessibilityUtil/index.js b/packages/react-native-web/src/modules/AccessibilityUtil/index.js index 9c4a0e12..e6f7cb12 100644 --- a/packages/react-native-web/src/modules/AccessibilityUtil/index.js +++ b/packages/react-native-web/src/modules/AccessibilityUtil/index.js @@ -7,11 +7,13 @@ * @flow */ +import buttonLikeRoles from './buttonLikeRoles'; import isDisabled from './isDisabled'; import propsToAccessibilityComponent from './propsToAccessibilityComponent'; import propsToAriaRole from './propsToAriaRole'; const AccessibilityUtil = { + buttonLikeRoles, isDisabled, propsToAccessibilityComponent, propsToAriaRole diff --git a/packages/react-native-web/src/modules/createDOMProps/__tests__/index-test.js b/packages/react-native-web/src/modules/createDOMProps/__tests__/index-test.js index e33cf95a..921231ed 100644 --- a/packages/react-native-web/src/modules/createDOMProps/__tests__/index-test.js +++ b/packages/react-native-web/src/modules/createDOMProps/__tests__/index-test.js @@ -65,9 +65,7 @@ describe('modules/createDOMProps', () => { }); }); - describe('"accessibilityRole" of "button"', () => { - const accessibilityRole = 'button'; - + const testFocusableRole = accessibilityRole => { test('default case', () => { expect(createProps({ accessibilityRole })).toEqual( expect.objectContaining({ 'data-focusable': true, tabIndex: '0' }) @@ -118,6 +116,14 @@ describe('modules/createDOMProps', () => { }) ).not.toEqual(expect.objectContaining({ 'data-focusable': true, tabIndex: '0' })); }); + }; + + describe('"accessibilityRole" of "button"', () => { + testFocusableRole('button'); + }); + + describe('"accessibilityRole" of "menuitem"', () => { + testFocusableRole('menuitem'); }); describe('with unfocusable accessibilityRole', () => { diff --git a/packages/react-native-web/src/modules/createDOMProps/index.js b/packages/react-native-web/src/modules/createDOMProps/index.js index d5a76905..9e5a4558 100644 --- a/packages/react-native-web/src/modules/createDOMProps/index.js +++ b/packages/react-native-web/src/modules/createDOMProps/index.js @@ -131,7 +131,7 @@ const createDOMProps = (component, props, styleResolver) => { } else { domProps['data-focusable'] = true; } - } else if (role === 'button' || role === 'textbox') { + } else if (AccessibilityUtil.buttonLikeRoles[role] || role === 'textbox') { if (accessible !== false && focusable) { domProps['data-focusable'] = true; domProps.tabIndex = '0';