mirror of
https://github.com/zhigang1992/react-native-web.git
synced 2026-03-26 01:04:13 +08:00
[fix] 'menuitem' role supports Enter/Space keyboard interaction
The 'menuitem' ARIA role should support Enter/Space keyboard interaction as if it were a button. This is required because the ARIA spec makes it so that the ARIA properties of 'menuitem' children are ignored, i.e., you can't just wrap a button in a 'menuitem' and expect Assistive Technologies to surface the button to users. Fix #1068 Close #1069
This commit is contained in:
committed by
Nicolas Gallagher
parent
1f06229289
commit
505e3faee8
@@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
19
packages/react-native-web/src/modules/AccessibilityUtil/buttonLikeRoles.js
vendored
Normal file
19
packages/react-native-web/src/modules/AccessibilityUtil/buttonLikeRoles.js
vendored
Normal file
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user