[change] accessibilityRelationship and accessibilityState props

Adds the accessibilityState and accessibilityRelationship object props that map
to ARIA props.

Removes the accessibilityStates array prop that is not compatible with web
accessibility services.

Ref #1172
This commit is contained in:
Nicolas Gallagher
2019-07-23 16:46:24 -07:00
parent ae94551ac5
commit d57fb6eb01
7 changed files with 187 additions and 39 deletions
@@ -10,7 +10,7 @@ describe('CheckBox', () => {
describe('disabled', () => {
test('when "false" a default checkbox is rendered', () => {
const component = shallow(<CheckBox />);
expect(component.find(checkboxSelector).prop('disabled')).toBe(false);
expect(component.find(checkboxSelector).prop('disabled')).toBe(undefined);
});
test('when "true" a disabled checkbox is rendered', () => {
@@ -15,7 +15,7 @@ describe('components/Switch', () => {
describe('disabled', () => {
test('when "false" a default checkbox is rendered', () => {
const component = shallow(<Switch />);
expect(component.find(checkboxSelector).prop('disabled')).toBe(false);
expect(component.find(checkboxSelector).prop('disabled')).toBe(undefined);
});
test('when "true" a disabled checkbox is rendered', () => {
@@ -10,11 +10,12 @@
import StyleSheetPropType from '../../modules/StyleSheetPropType';
import TextStylePropTypes from './TextStylePropTypes';
import { any, bool, func, number, oneOf, string } from 'prop-types';
import { any, bool, func, number, object, oneOf, string } from 'prop-types';
const TextPropTypes = {
accessibilityLabel: string,
accessibilityLiveRegion: oneOf(['assertive', 'none', 'polite']),
accessibilityRelationship: object,
accessibilityRole: oneOf([
'button',
'header',
@@ -26,6 +27,7 @@ const TextPropTypes = {
'text'
]),
accessible: bool,
accessibilityState: object,
children: any,
importantForAccessibility: oneOf(['auto', 'no', 'no-hide-descendants', 'yes']),
maxFontSizeMultiplier: number,
+26 -15
View File
@@ -11,7 +11,7 @@
import EdgeInsetsPropType, { type EdgeInsetsProp } from '../EdgeInsetsPropType';
import StyleSheetPropType from '../../modules/StyleSheetPropType';
import ViewStylePropTypes from './ViewStylePropTypes';
import { any, arrayOf, bool, func, object, oneOf, string } from 'prop-types';
import { any, bool, func, object, oneOf, string } from 'prop-types';
import { type StyleObj } from '../StyleSheet/StyleSheetTypes';
const stylePropType = StyleSheetPropType(ViewStylePropTypes);
@@ -33,8 +33,30 @@ export type ViewProps = {
accessibilityComponentType?: string,
accessibilityLabel?: string,
accessibilityLiveRegion?: 'none' | 'polite' | 'assertive',
accessibilityRelationship?: {
activedescendant?: ?string,
controls?: ?string,
describedby?: ?string,
details?: ?string,
haspopup?: ?string,
labelledby?: ?string,
owns?: ?string
},
accessibilityRole?: string,
accessibilityStates?: Array<string>,
accessibilityState?: {
busy?: ?boolean,
checked?: ?boolean | 'mixed',
disabled?: ?boolean,
expanded?: ?boolean,
grabbed?: ?boolean,
hidden?: ?boolean,
invalid?: ?boolean,
modal?: ?boolean,
pressed?: ?boolean,
readonly?: ?boolean,
required?: ?boolean,
selected?: ?boolean
},
accessible?: boolean,
children?: any,
className?: string,
@@ -90,20 +112,9 @@ const ViewPropTypes = {
accessibilityComponentType: string,
accessibilityLabel: string,
accessibilityLiveRegion: oneOf(['assertive', 'none', 'polite']),
accessibilityRelationship: object,
accessibilityRole: string,
accessibilityStates: arrayOf(
oneOf([
'disabled',
'selected',
/* web-only */
'busy',
'checked',
'expanded',
'grabbed',
'invalid',
'pressed'
])
),
accessibilityState: object,
accessible: bool,
children: any,
hitSlop: EdgeInsetsPropType,
@@ -13,3 +13,53 @@ exports[`modules/createDOMProps includes base reset style for browser-styled ele
exports[`modules/createDOMProps includes cursor style for pressable roles 1`] = `"css-cursor-18t94o4"`;
exports[`modules/createDOMProps includes cursor style for pressable roles 2`] = `"css-cursor-18t94o4"`;
exports[`modules/createDOMProps prop "accessibilityRelationship" values are "id" string 1`] = `
Object {
"aria-activedescendant": "id",
"aria-controls": "id",
"aria-describedby": "id",
"aria-details": "id",
"aria-haspopup": "id",
"aria-labelledby": "id",
"aria-owns": "id",
}
`;
exports[`modules/createDOMProps prop "accessibilityRelationship" values are "undefined" 1`] = `Object {}`;
exports[`modules/createDOMProps prop "accessibilityState" values are "false" 1`] = `
Object {
"aria-busy": false,
"aria-checked": false,
"aria-expanded": false,
"aria-grabbed": false,
"aria-invalid": false,
"aria-modal": false,
"aria-pressed": false,
"aria-readonly": false,
"aria-required": false,
"aria-selected": false,
}
`;
exports[`modules/createDOMProps prop "accessibilityState" values are "true" 1`] = `
Object {
"aria-busy": true,
"aria-checked": true,
"aria-disabled": true,
"aria-expanded": true,
"aria-grabbed": true,
"aria-hidden": true,
"aria-invalid": true,
"aria-modal": true,
"aria-pressed": true,
"aria-readonly": true,
"aria-required": true,
"aria-selected": true,
"disabled": true,
"hidden": true,
}
`;
exports[`modules/createDOMProps prop "accessibilityState" values are "undefined" 1`] = `Object {}`;
@@ -158,11 +158,67 @@ describe('modules/createDOMProps', () => {
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);
describe('prop "accessibilityState"', () => {
function createAccessibilityState(value) {
return {
busy: value,
checked: value,
disabled: value,
expanded: value,
grabbed: value,
hidden: value,
invalid: value,
modal: value,
pressed: value,
readonly: value,
required: value,
selected: value
};
}
test('values are "undefined"', () => {
const accessibilityState = createAccessibilityState(undefined);
const props = createProps({ accessibilityState });
expect(props).toMatchSnapshot();
});
test('values are "false"', () => {
const accessibilityState = createAccessibilityState(false);
const props = createProps({ accessibilityState });
expect(props).toMatchSnapshot();
});
test('values are "true"', () => {
const accessibilityState = createAccessibilityState(true);
const props = createProps({ accessibilityState });
expect(props).toMatchSnapshot();
});
});
describe('prop "accessibilityRelationship"', () => {
function createAccessibilityRelationship(value) {
return {
activedescendant: value,
controls: value,
describedby: value,
details: value,
haspopup: value,
labelledby: value,
owns: value
};
}
test('values are "undefined"', () => {
const accessibilityRelationship = createAccessibilityRelationship(undefined);
const props = createProps({ accessibilityRelationship });
expect(props).toMatchSnapshot();
});
test('values are "id" string', () => {
const accessibilityRelationship = createAccessibilityRelationship('id');
const props = createProps({ accessibilityRelationship });
expect(props).toMatchSnapshot();
});
});
test('prop "className" is preserved', () => {
+45 -16
View File
@@ -63,9 +63,11 @@ const createDOMProps = (component, props, styleResolver) => {
const {
accessibilityLabel,
accessibilityLiveRegion,
accessibilityStates,
accessibilityRelationship,
accessibilityState,
classList,
className: deprecatedClassName,
disabled: providedDisabled,
importantForAccessibility,
nativeID,
placeholderTextColor,
@@ -79,32 +81,59 @@ const createDOMProps = (component, props, styleResolver) => {
...domProps
} = props;
const disabled = AccessibilityUtil.isDisabled(props);
const disabled =
(accessibilityState != null && accessibilityState.disabled === true) || providedDisabled;
const role = AccessibilityUtil.propsToAriaRole(props);
// GENERAL ACCESSIBILITY
if (importantForAccessibility === 'no-hide-descendants') {
domProps['aria-hidden'] = true;
}
if (accessibilityLabel && accessibilityLabel.constructor === String) {
// accessibilityLabel
if (accessibilityLabel != null) {
domProps['aria-label'] = accessibilityLabel;
}
if (accessibilityLiveRegion && accessibilityLiveRegion.constructor === String) {
// accessibilityLiveRegion
if (accessibilityLiveRegion != null) {
domProps['aria-live'] = accessibilityLiveRegion === 'none' ? 'off' : accessibilityLiveRegion;
}
if (Array.isArray(accessibilityStates)) {
for (let i = 0; i < accessibilityStates.length; i += 1) {
domProps[`aria-${accessibilityStates[i]}`] = true;
// accessibilityRelationship
if (accessibilityRelationship != null) {
for (const prop in accessibilityRelationship) {
const value = accessibilityRelationship[prop];
if (value != null) {
domProps[`aria-${prop}`] = value;
}
}
}
if (role && role.constructor === String) {
// accessibilityRole
if (role != null) {
domProps.role = role;
}
// DISABLED
if (disabled) {
domProps['aria-disabled'] = disabled;
domProps.disabled = disabled;
// accessibilityState
if (accessibilityState != null) {
for (const prop in accessibilityState) {
const value = accessibilityState[prop];
if (value != null) {
if (prop === 'disabled' || prop === 'hidden') {
if (value === true) {
domProps[`aria-${prop}`] = value;
// also set prop directly to pick up host component behaviour
domProps[prop] = value;
}
} else {
domProps[`aria-${prop}`] = value;
}
}
}
}
// legacy fallbacks
if (importantForAccessibility === 'no-hide-descendants') {
domProps['aria-hidden'] = true;
}
if (disabled === true) {
domProps['aria-disabled'] = true;
domProps.disabled = true;
}
// FOCUS