@samitouri / QOS-React / commits / 59ff7749ed

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;