mirror of
https://github.com/zoriya/react-native-web.git
synced 2026-05-28 16:45:17 +00:00
[add] support for accessibilityRole and accessibilityStates
React Native 0.57 introduced 'accessibilityRole' and 'accessibilityStates' as cross-platform accessibility APIs to replace 'accessibilityComponentType' and 'accessibilityTraits' for Android and iOS. React Native for Web has supported the 'accessibilityRole' for a while. This patch maps some of the values defined in React Native to web equivalents, and continues to allow a larger selection of roles for web apps. It also adds support for 'accessibilityStates', mapping values to ARIA states with boolean values and expanding support beyond 'disabled' and 'selected'. Fix #1112 Close #1113
This commit is contained in:
+13
-1
@@ -11,7 +11,7 @@
|
||||
import EdgeInsetsPropType, { type EdgeInsetsProp } from '../EdgeInsetsPropType';
|
||||
import StyleSheetPropType from '../../modules/StyleSheetPropType';
|
||||
import ViewStylePropTypes from './ViewStylePropTypes';
|
||||
import { any, array, bool, func, object, oneOf, oneOfType, string } from 'prop-types';
|
||||
import { any, array, arrayOf, bool, func, object, oneOf, oneOfType, string } from 'prop-types';
|
||||
|
||||
const stylePropType = StyleSheetPropType(ViewStylePropTypes);
|
||||
|
||||
@@ -33,6 +33,7 @@ export type ViewProps = {
|
||||
accessibilityLabel?: string,
|
||||
accessibilityLiveRegion?: 'none' | 'polite' | 'assertive',
|
||||
accessibilityRole?: string,
|
||||
accessibilityStates?: Array<string>,
|
||||
accessibilityTraits?: string | Array<string>,
|
||||
accessible?: boolean,
|
||||
children?: any,
|
||||
@@ -83,6 +84,17 @@ const ViewPropTypes = {
|
||||
accessibilityLabel: string,
|
||||
accessibilityLiveRegion: oneOf(['assertive', 'none', 'polite']),
|
||||
accessibilityRole: string,
|
||||
accessibilityStates: arrayOf(oneOf([
|
||||
'disabled',
|
||||
'selected',
|
||||
/* web-only */
|
||||
'busy',
|
||||
'checked',
|
||||
'expanded',
|
||||
'grabbed',
|
||||
'invalid',
|
||||
'pressed'
|
||||
])),
|
||||
accessibilityTraits: oneOfType([array, string]),
|
||||
accessible: bool,
|
||||
children: any,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
const whitelist = {
|
||||
const supportedProps = {
|
||||
accessibilityComponentType: true,
|
||||
accessibilityLabel: true,
|
||||
accessibilityLiveRegion: true,
|
||||
accessibilityRole: true,
|
||||
accessibilityStates: true,
|
||||
accessibilityTraits: true,
|
||||
accessible: true,
|
||||
children: true,
|
||||
@@ -67,7 +68,7 @@ const filterSupportedProps = props => {
|
||||
const safeProps = {};
|
||||
for (const prop in props) {
|
||||
if (props.hasOwnProperty(prop)) {
|
||||
if (whitelist[prop] || prop.indexOf('aria-') === 0 || prop.indexOf('data-') === 0) {
|
||||
if (supportedProps[prop] || prop.indexOf('aria-') === 0 || prop.indexOf('data-') === 0) {
|
||||
safeProps[prop] = props[prop];
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -25,4 +25,11 @@ describe('modules/AccessibilityUtil/propsToAriaRole', () => {
|
||||
})
|
||||
).toEqual('link');
|
||||
});
|
||||
|
||||
test('when "accessibilityRole" is a native-only value', () => {
|
||||
expect(propsToAriaRole({ accessibilityRole: 'none' })).toEqual('presentation');
|
||||
expect(propsToAriaRole({ accessibilityRole: 'imagebutton' })).toEqual(undefined);
|
||||
// not really native-only, but used to allow Web to render <label> around TextInput
|
||||
expect(propsToAriaRole({ accessibilityRole: 'label' })).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
const isDisabled = (props: Object) => props.disabled || props['aria-disabled'];
|
||||
const isDisabled = (props: Object) =>
|
||||
props.disabled ||
|
||||
(Array.isArray(props.accessibilityStates) && props.accessibilityStates.indexOf('disabled') > -1);
|
||||
|
||||
export default isDisabled;
|
||||
|
||||
+5
-1
@@ -15,7 +15,6 @@ const roleComponents = {
|
||||
complementary: 'aside',
|
||||
contentinfo: 'footer',
|
||||
form: 'form',
|
||||
label: 'label',
|
||||
link: 'a',
|
||||
list: 'ul',
|
||||
listitem: 'li',
|
||||
@@ -27,6 +26,11 @@ const roleComponents = {
|
||||
const emptyObject = {};
|
||||
|
||||
const propsToAccessibilityComponent = (props: Object = emptyObject) => {
|
||||
// special-case for "label" role which doesn't map to an ARIA role
|
||||
if (props.accessibilityRole === 'label') {
|
||||
return 'label';
|
||||
}
|
||||
|
||||
const role = propsToAriaRole(props);
|
||||
if (role) {
|
||||
if (role === 'heading') {
|
||||
|
||||
+20
-1
@@ -23,6 +23,21 @@ const accessibilityTraitsToRole = {
|
||||
summary: 'region'
|
||||
};
|
||||
|
||||
const accessibilityRoleToWebRole = {
|
||||
adjustable: 'slider',
|
||||
button: 'button',
|
||||
header: 'heading',
|
||||
image: 'img',
|
||||
imagebutton: null,
|
||||
keyboardkey: null,
|
||||
label: null,
|
||||
link: 'link',
|
||||
none: 'presentation',
|
||||
search: 'search',
|
||||
summary: 'region',
|
||||
text: null
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides compatibility with React Native's "accessibilityTraits" (iOS) and
|
||||
* "accessibilityComponentType" (Android), converting them to equivalent ARIA
|
||||
@@ -34,7 +49,11 @@ const propsToAriaRole = ({
|
||||
accessibilityTraits
|
||||
}: Object) => {
|
||||
if (accessibilityRole) {
|
||||
return accessibilityRole;
|
||||
const inferredRole = accessibilityRoleToWebRole[accessibilityRole];
|
||||
if (inferredRole !== null) {
|
||||
// ignore roles that don't map to web
|
||||
return inferredRole || accessibilityRole;
|
||||
}
|
||||
}
|
||||
if (accessibilityTraits) {
|
||||
const trait = Array.isArray(accessibilityTraits) ? accessibilityTraits[0] : accessibilityTraits;
|
||||
|
||||
+11
-18
@@ -35,18 +35,12 @@ describe('modules/createDOMProps', () => {
|
||||
expect(createProps({ accessibilityRole, disabled: true })).toEqual(
|
||||
expect.objectContaining({ 'aria-disabled': true, disabled: true, tabIndex: '-1' })
|
||||
);
|
||||
expect(createProps({ accessibilityRole, 'aria-disabled': true })).toEqual(
|
||||
expect.objectContaining({ 'aria-disabled': true, disabled: true, tabIndex: '-1' })
|
||||
);
|
||||
});
|
||||
|
||||
test('when "disabled" is false', () => {
|
||||
expect(createProps({ accessibilityRole, disabled: false })).toEqual(
|
||||
expect.objectContaining({ 'data-focusable': true })
|
||||
);
|
||||
expect(createProps({ accessibilityRole, 'aria-disabled': false })).toEqual(
|
||||
expect.objectContaining({ 'data-focusable': true })
|
||||
);
|
||||
});
|
||||
|
||||
test('when "importantForAccessibility" is "no"', () => {
|
||||
@@ -88,18 +82,12 @@ describe('modules/createDOMProps', () => {
|
||||
expect(createProps({ accessibilityRole, disabled: true })).toEqual(
|
||||
expect.objectContaining({ 'aria-disabled': true, disabled: true })
|
||||
);
|
||||
expect(createProps({ accessibilityRole, 'aria-disabled': true })).toEqual(
|
||||
expect.objectContaining({ 'aria-disabled': true, disabled: true })
|
||||
);
|
||||
});
|
||||
|
||||
test('when "disabled" is false', () => {
|
||||
expect(createProps({ accessibilityRole, disabled: false })).toEqual(
|
||||
expect.objectContaining({ 'data-focusable': true, tabIndex: '0' })
|
||||
);
|
||||
expect(createProps({ accessibilityRole, 'aria-disabled': false })).toEqual(
|
||||
expect.objectContaining({ 'data-focusable': true, tabIndex: '0' })
|
||||
);
|
||||
});
|
||||
|
||||
test('when "importantForAccessibility" is "no"', () => {
|
||||
@@ -164,12 +152,17 @@ describe('modules/createDOMProps', () => {
|
||||
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 "accessibilityRole" becomes "role"', () => {
|
||||
const accessibilityRole = 'button';
|
||||
const props = createProps({ accessibilityRole });
|
||||
expect(props.role).toEqual('button');
|
||||
});
|
||||
|
||||
test('prop "accessibilityStates" becomes ARIA states', () => {
|
||||
const accessibilityStates = ['disabled', 'selected'];
|
||||
const props = createProps({ accessibilityStates });
|
||||
expect(props['aria-disabled']).toEqual(true);
|
||||
expect(props['aria-selected']).toEqual(true);
|
||||
});
|
||||
|
||||
test('prop "className" is preserved', () => {
|
||||
|
||||
@@ -76,6 +76,7 @@ const createDOMProps = (component, props, styleResolver) => {
|
||||
const {
|
||||
accessibilityLabel,
|
||||
accessibilityLiveRegion,
|
||||
accessibilityStates,
|
||||
importantForAccessibility,
|
||||
nativeID,
|
||||
placeholderTextColor,
|
||||
@@ -104,7 +105,12 @@ const createDOMProps = (component, props, styleResolver) => {
|
||||
if (accessibilityLiveRegion && accessibilityLiveRegion.constructor === String) {
|
||||
domProps['aria-live'] = accessibilityLiveRegion === 'none' ? 'off' : accessibilityLiveRegion;
|
||||
}
|
||||
if (role && role.constructor === String && role !== 'label') {
|
||||
if (Array.isArray(accessibilityStates)) {
|
||||
for (let i = 0; i < accessibilityStates.length; i += 1) {
|
||||
domProps[`aria-${accessibilityStates[i]}`] = true;
|
||||
}
|
||||
}
|
||||
if (role && role.constructor === String) {
|
||||
domProps.role = role;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user