mirror of
https://github.com/zhigang1992/react-native-web.git
synced 2026-03-26 09:14:15 +08:00
[change] a11y and layout: button role and DOM props
Problems: HTML's native <button> tag doesn't support flex styling in all browsers, causing layout bugs. And buttons or links created by "createDOMElement" (without an accessibility role) do not have the correct props. Solution: The "button" role is rendered to a "div[role=button]" that is focusable and responds to the same keyboard events as a native button. A native button can still be rendered using "createDOMElement". Make "createDOMProps" aware of the component context to ensure style resets and other props are correctly applied when an accessibility role is not defined. Additionally: This patch also adds a new "label" role to support accessible forms. It maps to a native label element. Close #241
This commit is contained in:
@@ -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:
|
||||
|
||||
* `<View accessibilityRole='article' />` => `<article role='article' />`.
|
||||
* `<View accessibilityRole='banner' />` => `<header role='banner' />`.
|
||||
* `<View accessibilityRole='button' />` => `<button type='button' role='button' />`.
|
||||
* `<View accessibilityRole='button' />` => `<div role='button' tabIndex='0' />`.
|
||||
* `<Text accessibilityRole='label' />` => `<label />`.
|
||||
* `<Text accessibilityRole='link' href='/' />` => `<a role='link' href='/' />`.
|
||||
* `<View accessibilityRole='main' />` => `<main role='main' />`.
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
<TouchableWithoutFeedback accessibilityRole="button" onPress={this._onPress}>
|
||||
```js
|
||||
<TouchableHighlight accessibilityRole="button" onPress={this._handlePress}>
|
||||
<View style={styles.button}>
|
||||
<Text style={styles.buttonText}>Press me!</Text>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</TouchableHighlight>
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -66,7 +66,7 @@ export default class StyleRegistry {
|
||||
*/
|
||||
resolve(reactNativeStyle, options = emptyObject) {
|
||||
if (!reactNativeStyle) {
|
||||
return undefined;
|
||||
return emptyObject;
|
||||
}
|
||||
|
||||
// fast and cachable
|
||||
|
||||
@@ -34,6 +34,7 @@ exports[`components/Switch disabled when "true" a disabled checkbox is rendered
|
||||
style="height:20px;width:20px;"
|
||||
/>
|
||||
<input
|
||||
aria-disabled="true"
|
||||
class="rn-bottom-1p0dtai rn-cursor-1ei5mc7 rn-height-1pi2tsx rn-left-1d2f490 rn-marginTop-1mnahxq rn-marginRight-61z16t rn-marginBottom-p1pxzi rn-marginLeft-11wrixw rn-opacity-1272l3b rn-paddingTop-wk8lta rn-paddingRight-9aemit rn-paddingBottom-1mdbw0j rn-paddingLeft-gy4na3 rn-position-u8s1d rn-right-zchlnj rn-top-ipm5af rn-width-13qz1uu"
|
||||
disabled=""
|
||||
type="checkbox"
|
||||
|
||||
@@ -192,7 +192,7 @@ describe('components/TextInput', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('modifier keys', done => {
|
||||
test('modifier keys are included', done => {
|
||||
const input = findNativeInput(mount(<TextInput onKeyPress={onKeyPress} />));
|
||||
input.simulate('keyPress', {
|
||||
altKey: true,
|
||||
|
||||
@@ -10,11 +10,6 @@ describe('components/View', () => {
|
||||
const component = shallow(<View />);
|
||||
expect(component.type()).toBe('div');
|
||||
});
|
||||
|
||||
test('is a "span" when inside <View accessibilityRole="button" />', () => {
|
||||
const component = render(<View accessibilityRole="button"><View /></View>);
|
||||
expect(component.find('span').length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('prop "children"', () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
116
src/modules/AccessibilityUtil/__tests__/propsToTabIndex-test.js
Normal file
116
src/modules/AccessibilityUtil/__tests__/propsToTabIndex-test.js
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
13
src/modules/AccessibilityUtil/isDisabled.js
Normal file
13
src/modules/AccessibilityUtil/isDisabled.js
Normal file
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 <a>).
|
||||
// Assume that 'button' is not (uses <div role='button'>) 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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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`] = `
|
||||
<span
|
||||
aria-label="accessibilityLabel"
|
||||
/>
|
||||
`;
|
||||
exports[`modules/createDOMElement it renders different DOM elements 1`] = `<span />`;
|
||||
|
||||
exports[`modules/createDOMElement prop "accessibilityLiveRegion" 1`] = `
|
||||
<span
|
||||
aria-live="off"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`modules/createDOMElement prop "accessibilityRole" button 1`] = `
|
||||
<button
|
||||
class="rn-appearance-30o5oe rn-backgroundColor-wib322 rn-color-homxoj rn-font-1lw9tu2 rn-textAlign-1ttztb7"
|
||||
role="button"
|
||||
type="button"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`modules/createDOMElement prop "accessibilityRole" compatibility with accessibilityComponentType 1`] = `
|
||||
<button
|
||||
class="rn-appearance-30o5oe rn-backgroundColor-wib322 rn-color-homxoj rn-font-1lw9tu2 rn-textAlign-1ttztb7"
|
||||
role="button"
|
||||
type="button"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`modules/createDOMElement prop "accessibilityRole" compatibility with accessibilityComponentType 2`] = `
|
||||
<a
|
||||
class="rn-backgroundColor-wib322 rn-color-homxoj rn-textDecoration-bauka4"
|
||||
role="link"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`modules/createDOMElement prop "accessibilityRole" compatibility with accessibilityTraits 1`] = `
|
||||
<button
|
||||
class="rn-appearance-30o5oe rn-backgroundColor-wib322 rn-color-homxoj rn-font-1lw9tu2 rn-textAlign-1ttztb7"
|
||||
role="button"
|
||||
type="button"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`modules/createDOMElement prop "accessibilityRole" compatibility with accessibilityTraits 2`] = `
|
||||
<a
|
||||
class="rn-backgroundColor-wib322 rn-color-homxoj rn-textDecoration-bauka4"
|
||||
role="link"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`modules/createDOMElement prop "accessibilityRole" headings 1`] = `
|
||||
<h1
|
||||
role="heading"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`modules/createDOMElement prop "accessibilityRole" headings 2`] = `
|
||||
<h3
|
||||
aria-level="3"
|
||||
role="heading"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`modules/createDOMElement prop "accessibilityRole" link and target="_blank" 1`] = `
|
||||
<a
|
||||
class="rn-backgroundColor-wib322 rn-color-homxoj rn-textDecoration-bauka4"
|
||||
rel=" noopener noreferrer"
|
||||
role="link"
|
||||
target="_blank"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`modules/createDOMElement prop "accessibilityRole" roles 1`] = `
|
||||
<header
|
||||
role="banner"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`modules/createDOMElement prop "accessible" 1`] = `
|
||||
<span
|
||||
tabindex="0"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`modules/createDOMElement prop "accessible" 2`] = `<span />`;
|
||||
|
||||
exports[`modules/createDOMElement prop "importantForAccessibility" 1`] = `<span />`;
|
||||
|
||||
exports[`modules/createDOMElement prop "importantForAccessibility" 2`] = `<span />`;
|
||||
|
||||
exports[`modules/createDOMElement prop "importantForAccessibility" 3`] = `
|
||||
<span
|
||||
aria-hidden="true"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`modules/createDOMElement prop "importantForAccessibility" 4`] = `<span />`;
|
||||
|
||||
exports[`modules/createDOMElement prop "testID" 1`] = `
|
||||
<span
|
||||
data-testid="Example.testID"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`modules/createDOMElement renders correct DOM element 1`] = `<span />`;
|
||||
|
||||
exports[`modules/createDOMElement renders correct DOM element 2`] = `<main />`;
|
||||
exports[`modules/createDOMElement it renders different DOM elements 2`] = `<main />`;
|
||||
|
||||
@@ -4,106 +4,14 @@ import createDOMElement from '..';
|
||||
import { shallow, render } from 'enzyme';
|
||||
|
||||
describe('modules/createDOMElement', () => {
|
||||
test('renders correct DOM element', () => {
|
||||
test('it renders different DOM elements', () => {
|
||||
let component = render(createDOMElement('span'));
|
||||
expect(component).toMatchSnapshot();
|
||||
component = render(createDOMElement('main'));
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
const accessibilityLabel = 'accessibilityLabel';
|
||||
const component = render(createDOMElement('span', { accessibilityLabel }));
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('prop "accessibilityLiveRegion"', () => {
|
||||
const component = render(createDOMElement('span', { accessibilityLiveRegion: 'none' }));
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('prop "accessibilityRole"', () => {
|
||||
test('roles', () => {
|
||||
const component = render(createDOMElement('span', { accessibilityRole: 'banner' }));
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('button', () => {
|
||||
const component = render(createDOMElement('span', { accessibilityRole: 'button' }));
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('headings', () => {
|
||||
let component = render(createDOMElement('div', { accessibilityRole: 'heading' }));
|
||||
expect(component).toMatchSnapshot();
|
||||
|
||||
component = render(
|
||||
createDOMElement('div', { accessibilityRole: 'heading', 'aria-level': '3' })
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('link and target="_blank"', () => {
|
||||
const component = render(
|
||||
createDOMElement('span', {
|
||||
accessibilityRole: 'link',
|
||||
target: '_blank'
|
||||
})
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('compatibility with', () => {
|
||||
test('accessibilityComponentType', () => {
|
||||
let component = render(createDOMElement('span', { accessibilityComponentType: 'button' }));
|
||||
expect(component).toMatchSnapshot();
|
||||
|
||||
component = render(
|
||||
createDOMElement('span', {
|
||||
accessibilityComponentType: 'button',
|
||||
accessibilityRole: 'link'
|
||||
})
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('accessibilityTraits', () => {
|
||||
let component = render(createDOMElement('span', { accessibilityTraits: 'button' }));
|
||||
expect(component).toMatchSnapshot();
|
||||
|
||||
component = render(
|
||||
createDOMElement('span', { accessibilityTraits: 'button', accessibilityRole: 'link' })
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('prop "accessible"', () => {
|
||||
let component = render(createDOMElement('span', { accessible: true }));
|
||||
expect(component).toMatchSnapshot();
|
||||
|
||||
component = render(createDOMElement('span', { accessible: false }));
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('prop "importantForAccessibility"', () => {
|
||||
let component = render(createDOMElement('span', { importantForAccessibility: 'auto' }));
|
||||
expect(component).toMatchSnapshot();
|
||||
|
||||
component = render(createDOMElement('span', { importantForAccessibility: 'no' }));
|
||||
expect(component).toMatchSnapshot();
|
||||
|
||||
component = render(
|
||||
createDOMElement('span', { importantForAccessibility: 'no-hide-descendants' })
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
|
||||
component = render(createDOMElement('span', { importantForAccessibility: 'yes' }));
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('onClick', done => {
|
||||
test('it normalizes event.nativeEvent', done => {
|
||||
const onClick = e => {
|
||||
e.nativeEvent.timestamp = 1496876171255;
|
||||
expect(e.nativeEvent).toMatchSnapshot();
|
||||
@@ -119,8 +27,29 @@ describe('modules/createDOMElement', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('prop "testID"', () => {
|
||||
const component = render(createDOMElement('span', { testID: 'Example.testID' }));
|
||||
expect(component).toMatchSnapshot();
|
||||
describe('when ARIA role is "button"', () => {
|
||||
[{ disabled: true }, { disabled: false }].forEach(({ disabled }) => {
|
||||
describe(`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(
|
||||
createDOMElement('span', { accessibilityRole: 'button', disabled, onClick })
|
||||
);
|
||||
component.find('span').simulate('keyPress', {
|
||||
isDefaultPrevented() {},
|
||||
nativeEvent: {
|
||||
preventDefault() {},
|
||||
stopImmediatePropagation() {},
|
||||
stopPropagation() {}
|
||||
},
|
||||
preventDefault() {},
|
||||
which
|
||||
});
|
||||
expect(onClick).toHaveBeenCalledTimes(disabled ? 0 : 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,12 @@ import React from 'react';
|
||||
|
||||
modality();
|
||||
|
||||
/**
|
||||
* Ensure event handlers receive an event of the expected shape. The 'button'
|
||||
* role – for accessibility reasons and functional equivalence to the native
|
||||
* button element – must also support synthetic keyboard activation of onclick,
|
||||
* and remove event handlers when disabled.
|
||||
*/
|
||||
const eventHandlerNames = {
|
||||
onClick: true,
|
||||
onClickCapture: true,
|
||||
@@ -40,27 +46,47 @@ const eventHandlerNames = {
|
||||
onTouchStart: true,
|
||||
onTouchStartCapture: true
|
||||
};
|
||||
const adjustProps = domProps => {
|
||||
const isButtonRole = domProps.role === 'button';
|
||||
const isDisabled = AccessibilityUtil.isDisabled(domProps);
|
||||
|
||||
const wrapEventHandler = handler => e => {
|
||||
e.nativeEvent = normalizeNativeEvent(e.nativeEvent);
|
||||
return handler(e);
|
||||
Object.keys(domProps).forEach(propName => {
|
||||
const prop = domProps[propName];
|
||||
const isEventHandler = typeof prop === 'function' && eventHandlerNames[propName];
|
||||
if (isEventHandler) {
|
||||
if (isButtonRole && isDisabled) {
|
||||
domProps[propName] = undefined;
|
||||
} else {
|
||||
// TODO: move this out of the render path
|
||||
domProps[propName] = e => {
|
||||
e.nativeEvent = normalizeNativeEvent(e.nativeEvent);
|
||||
return prop(e);
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Button role should trigger 'onClick' if SPACE or ENTER keys are pressed
|
||||
if (isButtonRole && !isDisabled) {
|
||||
const { onClick } = domProps;
|
||||
domProps.onKeyPress = function(e) {
|
||||
if (!e.isDefaultPrevented() && (e.which === 13 || e.which === 32)) {
|
||||
e.preventDefault();
|
||||
if (onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const createDOMElement = (component, props) => {
|
||||
// use equivalent platform elements where possible
|
||||
const accessibilityComponent = AccessibilityUtil.propsToAccessibilityComponent(props);
|
||||
const Component = accessibilityComponent || component;
|
||||
const domProps = createDOMProps(props);
|
||||
const domProps = createDOMProps(Component, props);
|
||||
|
||||
// normalize DOM events to match React Native events
|
||||
// TODO: move this out of the render path
|
||||
Object.keys(domProps).forEach(propName => {
|
||||
const prop = domProps[propName];
|
||||
const isEventHandler = typeof prop === 'function' && eventHandlerNames[propName];
|
||||
if (isEventHandler) {
|
||||
domProps[propName] = wrapEventHandler(prop);
|
||||
}
|
||||
});
|
||||
adjustProps(domProps);
|
||||
|
||||
return <Component {...domProps} />;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`modules/createDOMProps includes "rel" values for "a" elements (to securely open external links) 1`] = `" noopener noreferrer"`;
|
||||
|
||||
exports[`modules/createDOMProps includes cursor style for "button" role 1`] = `
|
||||
Object {
|
||||
"className": "rn-cursor-1loqt21",
|
||||
"role": "button",
|
||||
"tabIndex": "0",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`modules/createDOMProps includes reset styles for "a" elements 1`] = `
|
||||
Object {
|
||||
"className": "rn-backgroundColor-wib322 rn-color-homxoj rn-textDecoration-bauka4",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`modules/createDOMProps includes reset styles for "button" elements 1`] = `
|
||||
Object {
|
||||
"className": "rn-appearance-30o5oe rn-backgroundColor-wib322 rn-color-homxoj rn-font-1lw9tu2 rn-textAlign-1ttztb7",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`modules/createDOMProps includes reset styles for "ul" elements 1`] = `
|
||||
Object {
|
||||
"className": "rn-listStyle-1ebb2ja",
|
||||
}
|
||||
`;
|
||||
74
src/modules/createDOMProps/__tests__/index-test.js
Normal file
74
src/modules/createDOMProps/__tests__/index-test.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/* eslint-env jasmine, jest */
|
||||
|
||||
import createDOMProps from '..';
|
||||
|
||||
const createProps = props => createDOMProps(null, props);
|
||||
|
||||
describe('modules/createDOMProps', () => {
|
||||
test('prop "accessibilityLabel" becomes "aria-label"', () => {
|
||||
const accessibilityLabel = 'accessibilityLabel';
|
||||
const props = createProps({ accessibilityLabel });
|
||||
expect(props['aria-label']).toEqual(accessibilityLabel);
|
||||
});
|
||||
|
||||
test('prop "accessibilityLiveRegion" becomes "aria-live"', () => {
|
||||
const accessibilityLiveRegion = 'none';
|
||||
const props = createProps({ accessibilityLiveRegion });
|
||||
expect(props['aria-live']).toEqual('off');
|
||||
});
|
||||
|
||||
describe('prop "accessibilityRole"', () => {
|
||||
test('does not become "role" when value is "label"', () => {
|
||||
const accessibilityRole = 'label';
|
||||
const props = createProps({ accessibilityRole });
|
||||
expect(props.role).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
test('prop "className" is preserved', () => {
|
||||
const className = 'external-class-name';
|
||||
const props = createProps({ className });
|
||||
expect(props.className).toEqual(className);
|
||||
});
|
||||
|
||||
test('prop "importantForAccessibility" becomes "aria-hidden"', () => {
|
||||
const props = createProps({ importantForAccessibility: 'no-hide-descendants' });
|
||||
expect(props['aria-hidden']).toEqual(true);
|
||||
});
|
||||
|
||||
test('prop "testID" becomes "data-testid"', () => {
|
||||
const testID = 'Example.testID';
|
||||
const props = createProps({ testID });
|
||||
expect(props['data-testid']).toEqual(testID);
|
||||
});
|
||||
|
||||
test('includes reset styles for "a" elements', () => {
|
||||
const props = createDOMProps('a');
|
||||
expect(props).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('includes "rel" values for "a" elements (to securely open external links)', () => {
|
||||
const props = createDOMProps('a', { target: '_blank' });
|
||||
expect(props.rel).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('includes reset styles for "button" elements', () => {
|
||||
const props = createDOMProps('button');
|
||||
expect(props).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('includes cursor style for "button" role', () => {
|
||||
const props = createDOMProps('span', { accessibilityRole: 'button' });
|
||||
expect(props).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('includes reset styles for "ul" elements', () => {
|
||||
const props = createDOMProps('ul');
|
||||
expect(props).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('includes tabIndex when needed', () => {
|
||||
const props = createProps({ accessible: true });
|
||||
expect(props.tabIndex).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -14,25 +14,28 @@ import StyleRegistry from '../../apis/StyleSheet/registry';
|
||||
|
||||
const emptyObject = {};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttonReset: {
|
||||
const resetStyles = StyleSheet.create({
|
||||
ariaButton: {
|
||||
cursor: 'pointer'
|
||||
},
|
||||
button: {
|
||||
appearance: 'none',
|
||||
backgroundColor: 'transparent',
|
||||
color: 'inherit',
|
||||
font: 'inherit',
|
||||
textAlign: 'inherit'
|
||||
},
|
||||
linkReset: {
|
||||
link: {
|
||||
backgroundColor: 'transparent',
|
||||
color: 'inherit',
|
||||
textDecorationLine: 'none'
|
||||
},
|
||||
listReset: {
|
||||
list: {
|
||||
listStyle: 'none'
|
||||
}
|
||||
});
|
||||
|
||||
const pointerEventStyles = StyleSheet.create({
|
||||
const pointerEventsStyles = StyleSheet.create({
|
||||
auto: {
|
||||
pointerEvents: 'auto'
|
||||
},
|
||||
@@ -47,24 +50,26 @@ const pointerEventStyles = StyleSheet.create({
|
||||
}
|
||||
});
|
||||
|
||||
const resolver = style => StyleRegistry.resolve(style);
|
||||
const defaultStyleResolver = style => StyleRegistry.resolve(style);
|
||||
|
||||
const createDOMProps = (rnProps, resolveStyle) => {
|
||||
if (!resolveStyle) {
|
||||
resolveStyle = resolver;
|
||||
const createDOMProps = (component, props, styleResolver) => {
|
||||
if (!styleResolver) {
|
||||
styleResolver = defaultStyleResolver;
|
||||
}
|
||||
|
||||
if (!props) {
|
||||
props = emptyObject;
|
||||
}
|
||||
|
||||
const props = rnProps || emptyObject;
|
||||
const {
|
||||
accessibilityLabel,
|
||||
accessibilityLiveRegion,
|
||||
accessible,
|
||||
importantForAccessibility,
|
||||
pointerEvents,
|
||||
style: rnStyle,
|
||||
style: providedStyle,
|
||||
testID,
|
||||
type,
|
||||
/* eslint-disable */
|
||||
accessible,
|
||||
accessibilityComponentType,
|
||||
accessibilityRole,
|
||||
accessibilityTraits,
|
||||
@@ -72,19 +77,24 @@ const createDOMProps = (rnProps, resolveStyle) => {
|
||||
...domProps
|
||||
} = props;
|
||||
|
||||
const isDisabled = AccessibilityUtil.isDisabled(props);
|
||||
const role = AccessibilityUtil.propsToAriaRole(props);
|
||||
const pointerEventStyle = pointerEvents !== undefined && pointerEventStyles[pointerEvents];
|
||||
const tabIndex = AccessibilityUtil.propsToTabIndex(props);
|
||||
const reactNativeStyle = [
|
||||
(role === 'button' && styles.buttonReset) ||
|
||||
(role === 'link' && styles.linkReset) ||
|
||||
(role === 'list' && styles.listReset),
|
||||
rnStyle,
|
||||
pointerEventStyle
|
||||
component === 'a' && resetStyles.link,
|
||||
component === 'button' && resetStyles.button,
|
||||
component === 'ul' && resetStyles.list,
|
||||
role === 'button' && !isDisabled && resetStyles.ariaButton,
|
||||
providedStyle,
|
||||
pointerEvents && pointerEventsStyles[pointerEvents]
|
||||
];
|
||||
const { className, style } = resolveStyle(reactNativeStyle) || emptyObject;
|
||||
const { className, style } = styleResolver(reactNativeStyle);
|
||||
|
||||
if (accessible === true) {
|
||||
domProps.tabIndex = AccessibilityUtil.propsToTabIndex(props);
|
||||
if (isDisabled) {
|
||||
domProps['aria-disabled'] = true;
|
||||
}
|
||||
if (importantForAccessibility === 'no-hide-descendants') {
|
||||
domProps['aria-hidden'] = true;
|
||||
}
|
||||
if (accessibilityLabel && accessibilityLabel.constructor === String) {
|
||||
domProps['aria-label'] = accessibilityLabel;
|
||||
@@ -95,26 +105,21 @@ const createDOMProps = (rnProps, resolveStyle) => {
|
||||
if (className && className.constructor === String) {
|
||||
domProps.className = domProps.className ? `${domProps.className} ${className}` : className;
|
||||
}
|
||||
if (importantForAccessibility === 'no-hide-descendants') {
|
||||
domProps['aria-hidden'] = true;
|
||||
if (component === 'a' && domProps.target === '_blank') {
|
||||
domProps.rel = `${domProps.rel || ''} noopener noreferrer`;
|
||||
}
|
||||
if (role && role.constructor === String) {
|
||||
if (role && role.constructor === String && role !== 'label') {
|
||||
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 (tabIndex) {
|
||||
domProps.tabIndex = tabIndex;
|
||||
}
|
||||
if (testID && testID.constructor === String) {
|
||||
domProps['data-testid'] = testID;
|
||||
}
|
||||
if (type && type.constructor === String) {
|
||||
domProps.type = type;
|
||||
}
|
||||
|
||||
return domProps;
|
||||
};
|
||||
|
||||
@@ -17,7 +17,10 @@ const BaseComponentPropTypes = {
|
||||
accessible: bool,
|
||||
importantForAccessibility: oneOf(['auto', 'no', 'no-hide-descendants', 'yes']),
|
||||
style: oneOfType([array, number, object]),
|
||||
testID: string
|
||||
testID: string,
|
||||
// compatibility with React Native
|
||||
accessibilityComponentType: string,
|
||||
accessibilityTraits: oneOfType([array, string])
|
||||
};
|
||||
|
||||
export default BaseComponentPropTypes;
|
||||
|
||||
Reference in New Issue
Block a user