React dom invalid aria hook (#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
J. Renée Beach committed
Sep 27, 2016 at 18:53 UTC
59ff7749eda0cd858d5ee568315bcba1be75a1ca
6 files changed
+277
docs/warnings/invalid-aria-prop.md
new
+11
@@ -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
\ No newline at end of file
src/renderers/dom/ReactDOM.js
+2
@@ -138,9 +138,11 @@ if (__DEV__) {
138
var ReactInstrumentation = require('ReactInstrumentation');
139
var ReactDOMUnknownPropertyHook = require('ReactDOMUnknownPropertyHook');
140
var ReactDOMNullInputValuePropHook = require('ReactDOMNullInputValuePropHook');
141
+ var ReactDOMInvalidARIAHook = require('ReactDOMInvalidARIAHook');
142
143
ReactInstrumentation.debugTool.addHook(ReactDOMUnknownPropertyHook);
144
ReactInstrumentation.debugTool.addHook(ReactDOMNullInputValuePropHook);
145
+ ReactInstrumentation.debugTool.addHook(ReactDOMInvalidARIAHook);
146
}
147
148
module.exports = ReactDOM;
src/renderers/dom/shared/ARIADOMPropertyConfig.js
new
+74
@@ -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
+2
@@ -11,6 +11,7 @@
11
12
'use strict';
13
14
+var ARIADOMPropertyConfig = require('ARIADOMPropertyConfig');
15
var BeforeInputEventPlugin = require('BeforeInputEventPlugin');
16
var ChangeEventPlugin = require('ChangeEventPlugin');
17
var DefaultEventPluginOrder = require('DefaultEventPluginOrder');
@@ -73,6 +74,7 @@ function inject() {
74
ReactDOMTextComponent
75
);
76
77
+ ReactInjection.DOMProperty.injectDOMPropertyConfig(ARIADOMPropertyConfig);
78
ReactInjection.DOMProperty.injectDOMPropertyConfig(HTMLDOMPropertyConfig);
79
ReactInjection.DOMProperty.injectDOMPropertyConfig(SVGDOMPropertyConfig);
80
src/renderers/dom/shared/__tests__/ReactDOMInvalidARIAHook-test.js
new
+69
@@ -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
+});
src/renderers/dom/shared/hooks/ReactDOMInvalidARIAHook.js
new
+119
@@ -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;