Skip to content

Commit 59ff774

Browse files
jessebeachaweary
authored andcommitted
React dom invalid aria hook (facebook#7744)
* Add a hook that throws a runtime warning for invalid WAI ARIA attributes and values. * Resolved linting errors. * Added a test case for many props. * Added a test case for ARIA attribute proper casing. * Added a warning for uppercased attributes to ReactDOMInvalidARIAHook
1 parent 72ed5df commit 59ff774

File tree

6 files changed

+277
-0
lines changed

6 files changed

+277
-0
lines changed

docs/warnings/invalid-aria-prop.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
title: Invalid ARIA Prop Warning
3+
layout: single
4+
permalink: warnings/invalid-aria-prop.html
5+
---
6+
7+
The invalid-aria-prop warning will fire if you attempt to render a DOM element with an aria-* prop that does not exist in the Web Accessibility Initiative (WAI) Accessible Rich Internet Application (ARIA) [specification](https://www.w3.org/TR/wai-aria-1.1/#states_and_properties).
8+
9+
1. If you feel that you are using a valid prop, check the spelling carefully. `aria-labelledby` and `aria-activedescendant` are often misspelled.
10+
11+
2. React does not yet recognize the attribute you specified. This will likely be fixed in a future version of React. However, React currently strips all unknown attributes, so specifying them in your React app will not cause them to be rendered

src/renderers/dom/ReactDOM.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,11 @@ if (__DEV__) {
138138
var ReactInstrumentation = require('ReactInstrumentation');
139139
var ReactDOMUnknownPropertyHook = require('ReactDOMUnknownPropertyHook');
140140
var ReactDOMNullInputValuePropHook = require('ReactDOMNullInputValuePropHook');
141+
var ReactDOMInvalidARIAHook = require('ReactDOMInvalidARIAHook');
141142

142143
ReactInstrumentation.debugTool.addHook(ReactDOMUnknownPropertyHook);
143144
ReactInstrumentation.debugTool.addHook(ReactDOMNullInputValuePropHook);
145+
ReactInstrumentation.debugTool.addHook(ReactDOMInvalidARIAHook);
144146
}
145147

146148
module.exports = ReactDOM;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* Copyright 2013-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @providesModule ARIADOMPropertyConfig
10+
*/
11+
12+
'use strict';
13+
14+
var ARIADOMPropertyConfig = {
15+
Properties: {
16+
// Global States and Properties
17+
'aria-current': 0, // state
18+
'aria-details': 0,
19+
'aria-disabled': 0, // state
20+
'aria-hidden': 0, // state
21+
'aria-invalid': 0, // state
22+
'aria-keyshortcuts': 0,
23+
'aria-label': 0,
24+
'aria-roledescription': 0,
25+
// Widget Attributes
26+
'aria-autocomplete': 0,
27+
'aria-checked': 0,
28+
'aria-expanded': 0,
29+
'aria-haspopup': 0,
30+
'aria-level': 0,
31+
'aria-modal': 0,
32+
'aria-multiline': 0,
33+
'aria-multiselectable': 0,
34+
'aria-orientation': 0,
35+
'aria-placeholder': 0,
36+
'aria-pressed': 0,
37+
'aria-readonly': 0,
38+
'aria-required': 0,
39+
'aria-selected': 0,
40+
'aria-sort': 0,
41+
'aria-valuemax': 0,
42+
'aria-valuemin': 0,
43+
'aria-valuenow': 0,
44+
'aria-valuetext': 0,
45+
// Live Region Attributes
46+
'aria-atomic': 0,
47+
'aria-busy': 0,
48+
'aria-live': 0,
49+
'aria-relevant': 0,
50+
// Drag-and-Drop Attributes
51+
'aria-dropeffect': 0,
52+
'aria-grabbed': 0,
53+
// Relationship Attributes
54+
'aria-activedescendant': 0,
55+
'aria-colcount': 0,
56+
'aria-colindex': 0,
57+
'aria-colspan': 0,
58+
'aria-controls': 0,
59+
'aria-describedby': 0,
60+
'aria-errormessage': 0,
61+
'aria-flowto': 0,
62+
'aria-labelledby': 0,
63+
'aria-owns': 0,
64+
'aria-posinset': 0,
65+
'aria-rowcount': 0,
66+
'aria-rowindex': 0,
67+
'aria-rowspan': 0,
68+
'aria-setsize': 0,
69+
},
70+
DOMAttributeNames: {},
71+
DOMPropertyNames: {},
72+
};
73+
74+
module.exports = ARIADOMPropertyConfig;

src/renderers/dom/shared/ReactDefaultInjection.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
'use strict';
1313

14+
var ARIADOMPropertyConfig = require('ARIADOMPropertyConfig');
1415
var BeforeInputEventPlugin = require('BeforeInputEventPlugin');
1516
var ChangeEventPlugin = require('ChangeEventPlugin');
1617
var DefaultEventPluginOrder = require('DefaultEventPluginOrder');
@@ -73,6 +74,7 @@ function inject() {
7374
ReactDOMTextComponent
7475
);
7576

77+
ReactInjection.DOMProperty.injectDOMPropertyConfig(ARIADOMPropertyConfig);
7678
ReactInjection.DOMProperty.injectDOMPropertyConfig(HTMLDOMPropertyConfig);
7779
ReactInjection.DOMProperty.injectDOMPropertyConfig(SVGDOMPropertyConfig);
7880

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Copyright 2013-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @emails react-core
10+
*/
11+
12+
'use strict';
13+
14+
describe('ReactDOMInvalidARIAHook', () => {
15+
var React;
16+
var ReactTestUtils;
17+
var mountComponent;
18+
19+
beforeEach(() => {
20+
jest.resetModuleRegistry();
21+
React = require('React');
22+
ReactTestUtils = require('ReactTestUtils');
23+
24+
mountComponent = function(props) {
25+
ReactTestUtils.renderIntoDocument(<div {...props} />);
26+
};
27+
});
28+
29+
describe('aria-* props', () => {
30+
it('should allow valid aria-* props', () => {
31+
spyOn(console, 'error');
32+
mountComponent({'aria-label': 'Bumble bees'});
33+
expect(console.error.calls.count()).toBe(0);
34+
});
35+
it('should warn for one invalid aria-* prop', () => {
36+
spyOn(console, 'error');
37+
mountComponent({'aria-badprop': 'maybe'});
38+
expect(console.error.calls.count()).toBe(1);
39+
expect(console.error.calls.argsFor(0)[0]).toContain(
40+
'Warning: Invalid aria prop `aria-badprop` on <div> tag. ' +
41+
'For details, see https://fb.me/invalid-aria-prop'
42+
);
43+
});
44+
it('should warn for many invalid aria-* props', () => {
45+
spyOn(console, 'error');
46+
mountComponent(
47+
{
48+
'aria-badprop': 'Very tall trees',
49+
'aria-malprop': 'Turbulent seas',
50+
}
51+
);
52+
expect(console.error.calls.count()).toBe(1);
53+
expect(console.error.calls.argsFor(0)[0]).toContain(
54+
'Warning: Invalid aria props `aria-badprop`, `aria-malprop` on <div> ' +
55+
'tag. For details, see https://fb.me/invalid-aria-prop'
56+
);
57+
});
58+
it('should warn for an improperly cased aria-* prop', () => {
59+
spyOn(console, 'error');
60+
// The valid attribute name is aria-haspopup.
61+
mountComponent({'aria-hasPopup': 'true'});
62+
expect(console.error.calls.count()).toBe(1);
63+
expect(console.error.calls.argsFor(0)[0]).toContain(
64+
'Warning: Unknown ARIA attribute aria-hasPopup. ' +
65+
'Did you mean aria-haspopup?'
66+
);
67+
});
68+
});
69+
});
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* Copyright 2013-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @providesModule ReactDOMInvalidARIAHook
10+
*/
11+
12+
'use strict';
13+
14+
var DOMProperty = require('DOMProperty');
15+
var ReactComponentTreeHook = require('ReactComponentTreeHook');
16+
17+
var warning = require('warning');
18+
19+
var warnedProperties = {};
20+
var rARIA = new RegExp('^(aria)-[' + DOMProperty.ATTRIBUTE_NAME_CHAR + ']*$');
21+
22+
function validateProperty(tagName, name, debugID) {
23+
if (
24+
warnedProperties.hasOwnProperty(name)
25+
&& warnedProperties[name]
26+
) {
27+
return true;
28+
}
29+
30+
if (rARIA.test(name)) {
31+
var lowerCasedName = name.toLowerCase();
32+
var standardName =
33+
DOMProperty.getPossibleStandardName.hasOwnProperty(lowerCasedName) ?
34+
DOMProperty.getPossibleStandardName[lowerCasedName] :
35+
null;
36+
37+
// If this is an aria-* attribute, but is not listed in the known DOM
38+
// DOM properties, then it is an invalid aria-* attribute.
39+
if (standardName == null) {
40+
warnedProperties[name] = true;
41+
return false;
42+
}
43+
// aria-* attributes should be lowercase; suggest the lowercase version.
44+
if (name !== standardName) {
45+
warning(
46+
false,
47+
'Unknown ARIA attribute %s. Did you mean %s?%s',
48+
name,
49+
standardName,
50+
ReactComponentTreeHook.getStackAddendumByID(debugID)
51+
);
52+
warnedProperties[name] = true;
53+
return true;
54+
}
55+
}
56+
57+
return true;
58+
}
59+
60+
function warnInvalidARIAProps(debugID, element) {
61+
const invalidProps = [];
62+
63+
for (var key in element.props) {
64+
var isValid = validateProperty(element.type, key, debugID);
65+
if (!isValid) {
66+
invalidProps.push(key);
67+
}
68+
}
69+
70+
const unknownPropString = invalidProps
71+
.map(prop => '`' + prop + '`')
72+
.join(', ');
73+
74+
if (invalidProps.length === 1) {
75+
warning(
76+
false,
77+
'Invalid aria prop %s on <%s> tag. ' +
78+
'For details, see https://fb.me/invalid-aria-prop%s',
79+
unknownPropString,
80+
element.type,
81+
ReactComponentTreeHook.getStackAddendumByID(debugID)
82+
);
83+
} else if (invalidProps.length > 1) {
84+
warning(
85+
false,
86+
'Invalid aria props %s on <%s> tag. ' +
87+
'For details, see https://fb.me/invalid-aria-prop%s',
88+
unknownPropString,
89+
element.type,
90+
ReactComponentTreeHook.getStackAddendumByID(debugID)
91+
);
92+
}
93+
}
94+
95+
function handleElement(debugID, element) {
96+
if (element == null || typeof element.type !== 'string') {
97+
return;
98+
}
99+
if (element.type.indexOf('-') >= 0 || element.props.is) {
100+
return;
101+
}
102+
103+
warnInvalidARIAProps(debugID, element);
104+
}
105+
106+
var ReactDOMInvalidARIAHook = {
107+
onBeforeMountComponent(debugID, element) {
108+
if (__DEV__) {
109+
handleElement(debugID, element);
110+
}
111+
},
112+
onBeforeUpdateComponent(debugID, element) {
113+
if (__DEV__) {
114+
handleElement(debugID, element);
115+
}
116+
},
117+
};
118+
119+
module.exports = ReactDOMInvalidARIAHook;

0 commit comments

Comments
 (0)