[change] improve button accessibility and styling

1. If no 'accessibilityRole' is set, fallback to looking for 'button'
role in the equivalent native props. This helps improve accessibility of
button-like components authored without the web platform in mind.

2. Ensure button context is properly inherited.

3. Add 'appearance:none' to DOM button elements to enable better styling
support in Safari

Fix #378
This commit is contained in:
Nicolas Gallagher
2017-03-20 14:46:09 -07:00
parent ec2db3e2a3
commit 458c534200
8 changed files with 131 additions and 20 deletions
@@ -50,5 +50,6 @@ input::-webkit-inner-spin-button,input::-webkit-outer-spin-button,input::-webkit
.rn-paddingLeft-gy4na3{padding-left:0px}
.rn-textAlign-1ttztb7{text-align:inherit}
.rn-textDecoration-bauka4{text-decoration:none}
.rn-appearance-30o5oe{-moz-appearance:none;-webkit-appearance:none;appearance:none}
</style>"
`;
@@ -20,8 +20,9 @@ exports[`components/View rendered element is a "div" by default 1`] = `
exports[`components/View rendered element is a "span" when inside <View accessibilityRole="button" /> 1`] = `
<button
className="rn-alignItems-1oszu61 rn-backgroundColor-wib322 rn-borderTopStyle-1efd50x rn-borderRightStyle-14skgim rn-borderBottomStyle-rull8r rn-borderLeftStyle-mm0ijv rn-borderTopWidth-13yce4e rn-borderRightWidth-fnigne rn-borderBottomWidth-ndvcnb rn-borderLeftWidth-gxnn5r rn-boxSizing-deolkf rn-color-homxoj rn-display-6koalj rn-flexShrink-1qe8dj5 rn-flexBasis-1mlwlqe rn-flexDirection-eqz5dr rn-font-1lw9tu2 rn-listStyle-1ebb2ja rn-marginTop-1mnahxq rn-marginRight-61z16t rn-marginBottom-p1pxzi rn-marginLeft-11wrixw rn-minHeight-ifefl9 rn-minWidth-bcqeeo rn-paddingTop-wk8lta rn-paddingRight-9aemit rn-paddingBottom-1mdbw0j rn-paddingLeft-gy4na3 rn-position-bnwqim rn-textAlign-1ttztb7 rn-textDecoration-bauka4"
className="rn-alignItems-1oszu61 rn-appearance-30o5oe rn-backgroundColor-wib322 rn-borderTopStyle-1efd50x rn-borderRightStyle-14skgim rn-borderBottomStyle-rull8r rn-borderLeftStyle-mm0ijv rn-borderTopWidth-13yce4e rn-borderRightWidth-fnigne rn-borderBottomWidth-ndvcnb rn-borderLeftWidth-gxnn5r rn-boxSizing-deolkf rn-color-homxoj rn-display-6koalj rn-flexShrink-1qe8dj5 rn-flexBasis-1mlwlqe rn-flexDirection-eqz5dr rn-font-1lw9tu2 rn-listStyle-1ebb2ja rn-marginTop-1mnahxq rn-marginRight-61z16t rn-marginBottom-p1pxzi rn-marginLeft-11wrixw rn-minHeight-ifefl9 rn-minWidth-bcqeeo rn-paddingTop-wk8lta rn-paddingRight-9aemit rn-paddingBottom-1mdbw0j rn-paddingLeft-gy4na3 rn-position-bnwqim rn-textAlign-1ttztb7 rn-textDecoration-bauka4"
role="button"
style={Object {}}
type="button">
<span
className="rn-alignItems-1oszu61 rn-backgroundColor-wib322 rn-borderTopStyle-1efd50x rn-borderRightStyle-14skgim rn-borderBottomStyle-rull8r rn-borderLeftStyle-mm0ijv rn-borderTopWidth-13yce4e rn-borderRightWidth-fnigne rn-borderBottomWidth-ndvcnb rn-borderLeftWidth-gxnn5r rn-boxSizing-deolkf rn-color-homxoj rn-display-6koalj rn-flexShrink-1qe8dj5 rn-flexBasis-1mlwlqe rn-flexDirection-eqz5dr rn-font-1lw9tu2 rn-listStyle-1ebb2ja rn-marginTop-1mnahxq rn-marginRight-61z16t rn-marginBottom-p1pxzi rn-marginLeft-11wrixw rn-minHeight-ifefl9 rn-minWidth-bcqeeo rn-paddingTop-wk8lta rn-paddingRight-9aemit rn-paddingBottom-1mdbw0j rn-paddingLeft-gy4na3 rn-position-bnwqim rn-textAlign-1ttztb7 rn-textDecoration-bauka4" />
+15 -6
View File
@@ -1,6 +1,7 @@
import applyLayout from '../../modules/applyLayout';
import applyNativeMethods from '../../modules/applyNativeMethods';
import createDOMElement from '../../modules/createDOMElement';
import getAccessibilityRole from '../../modules/getAccessibilityRole';
import StyleSheet from '../../apis/StyleSheet';
import ViewPropTypes from './ViewPropTypes';
import { Component, PropTypes } from 'react';
@@ -25,7 +26,7 @@ class View extends Component {
};
getChildContext() {
const isInAButtonView = this.props.accessibilityRole === 'button' ||
const isInAButtonView = getAccessibilityRole(this.props) === 'button' ||
this.context.isInAButtonView;
return isInAButtonView ? { isInAButtonView } : emptyObject;
}
@@ -35,8 +36,6 @@ class View extends Component {
pointerEvents,
style,
/* eslint-disable */
accessibilityComponentType,
accessibilityTraits,
collapsable,
hitSlop,
onAccessibilityTap,
@@ -47,10 +46,17 @@ class View extends Component {
...otherProps
} = this.props;
const component = this.context.isInAButtonView ? 'span' : 'div';
const { isInAButtonView } = this.context;
const isButton = getAccessibilityRole(this.props) === 'button';
otherProps.style = [styles.initial, style, pointerEvents && pointerEventStyles[pointerEvents]];
otherProps.style = [
styles.initial,
isButton && styles.buttonOnly,
style,
pointerEvents && pointerEventStyles[pointerEvents]
];
const component = isInAButtonView ? 'span' : 'div';
return createDOMElement(component, otherProps);
}
}
@@ -68,7 +74,7 @@ const styles = StyleSheet.create({
margin: 0,
padding: 0,
position: 'relative',
// button and anchor reset
// button and anchor resets
backgroundColor: 'transparent',
color: 'inherit',
font: 'inherit',
@@ -79,6 +85,9 @@ const styles = StyleSheet.create({
// fix flexbox bugs
minHeight: 0,
minWidth: 0
},
buttonOnly: {
appearance: 'none'
}
});
@@ -14,6 +14,28 @@ exports[`modules/createDOMElement prop "accessibilityRole" button 1`] = `
type="button" />
`;
exports[`modules/createDOMElement prop "accessibilityRole" compatibility with accessibilityComponentType 1`] = `
<button
role="button"
type="button" />
`;
exports[`modules/createDOMElement prop "accessibilityRole" compatibility with accessibilityComponentType 2`] = `
<a
role="link" />
`;
exports[`modules/createDOMElement prop "accessibilityRole" compatibility with accessibilityTraits 1`] = `
<button
role="button"
type="button" />
`;
exports[`modules/createDOMElement prop "accessibilityRole" compatibility with accessibilityTraits 2`] = `
<a
role="link" />
`;
exports[`modules/createDOMElement prop "accessibilityRole" link and target="_blank" 1`] = `
<a
rel=" noopener noreferrer"
@@ -43,6 +43,35 @@ describe('modules/createDOMElement', () => {
);
expect(component.toJSON()).toMatchSnapshot();
});
describe('compatibility with', () => {
test('accessibilityComponentType', () => {
let component = renderer.create(
createDOMElement('span', { accessibilityComponentType: 'button' })
);
expect(component.toJSON()).toMatchSnapshot();
component = renderer.create(
createDOMElement('span', {
accessibilityComponentType: 'button',
accessibilityRole: 'link'
})
);
expect(component.toJSON()).toMatchSnapshot();
});
test('accessibilityTraits', () => {
let component = renderer.create(
createDOMElement('span', { accessibilityTraits: 'button' })
);
expect(component.toJSON()).toMatchSnapshot();
component = renderer.create(
createDOMElement('span', { accessibilityTraits: 'button', accessibilityRole: 'link' })
);
expect(component.toJSON()).toMatchSnapshot();
});
});
});
test('prop "accessible"', () => {
+19 -13
View File
@@ -1,5 +1,6 @@
import '../injectResponderEventPlugin';
import getAccessibilityRole from '../getAccessibilityRole';
import normalizeNativeEvent from '../normalizeNativeEvent';
import React from 'react';
import StyleRegistry from '../../apis/StyleSheet/registry';
@@ -54,16 +55,21 @@ const createDOMElement = (component, rnProps) => {
const {
accessibilityLabel,
accessibilityLiveRegion,
accessibilityRole,
accessible = true,
style: rnStyle,
testID,
type,
/* eslint-disable */
accessibilityComponentType,
accessibilityRole,
accessibilityTraits,
/* eslint-enable */
...domProps
} = rnProps || emptyObject;
// use equivalent platform elements where possible
const accessibilityComponent = accessibilityRole && roleComponents[accessibilityRole];
const role = getAccessibilityRole(rnProps || emptyObject);
const accessibilityComponent = role && roleComponents[role];
const Component = accessibilityComponent || component;
// convert React Native styles to DOM styles
@@ -89,23 +95,23 @@ const createDOMElement = (component, rnProps) => {
if (accessibilityLiveRegion) {
domProps['aria-live'] = accessibilityLiveRegion;
}
if (testID) {
domProps['data-testid'] = testID;
}
if (accessibilityRole) {
domProps.role = accessibilityRole;
if (accessibilityRole === 'button') {
domProps.type = 'button';
} else if (accessibilityRole === 'link' && domProps.target === '_blank') {
domProps.rel = `${domProps.rel || ''} noopener noreferrer`;
}
}
if (className && className !== '') {
domProps.className = domProps.className ? `${domProps.className} ${className}` : className;
}
if (role) {
domProps.role = role;
if (role === 'button') {
domProps.type = 'button';
} else if (role === 'link' && domProps.target === '_blank') {
domProps.rel = `${domProps.rel || ''} noopener noreferrer`;
}
}
if (style) {
domProps.style = style;
}
if (testID) {
domProps['data-testid'] = testID;
}
if (type) {
domProps.type = type;
}
@@ -0,0 +1,28 @@
/* eslint-env jasmine, jest */
import getAccessibilityRole from '..';
describe('modules/getAccessibilityRole', () => {
test('returns undefined when missing accessibility props', () => {
expect(getAccessibilityRole({})).toBeUndefined();
});
test('returns value of "accessibilityRole" when defined', () => {
expect(getAccessibilityRole({ accessibilityRole: 'banner' })).toEqual('banner');
});
test('returns "button" when iOS/Android accessibility prop equals "button"', () => {
expect(getAccessibilityRole({ accessibilityComponentType: 'button' })).toEqual('button');
expect(getAccessibilityRole({ accessibilityTraits: 'button' })).toEqual('button');
});
test('prioritizes "accessibilityRole" when defined', () => {
expect(
getAccessibilityRole({
accessibilityComponentType: 'button',
accessibilityRole: 'link',
accessibilityTraits: 'button'
})
).toEqual('link');
});
});
+15
View File
@@ -0,0 +1,15 @@
const getAccessibilityRole = (
{
accessibilityComponentType,
accessibilityRole,
accessibilityTraits
}
) => {
if (accessibilityRole) {
return accessibilityRole;
} else if (accessibilityComponentType === 'button' || accessibilityTraits === 'button') {
return 'button';
}
};
module.exports = getAccessibilityRole;