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
This commit is contained in:
J. Renée Beach 2016-09-27 18:53:14 -07:00 committed by Brandon Dail
parent 72ed5df5a4
commit 59ff7749ed
6 changed files with 277 additions and 0 deletions

View File

@ -0,0 +1,11 @@
---
title: Invalid ARIA Prop Warning
layout: single
permalink: warnings/invalid-aria-prop.html
---
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).
1. If you feel that you are using a valid prop, check the spelling carefully. `aria-labelledby` and `aria-activedescendant` are often misspelled.
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

View File

@ -138,9 +138,11 @@ if (__DEV__) {
var ReactInstrumentation = require('ReactInstrumentation');
var ReactDOMUnknownPropertyHook = require('ReactDOMUnknownPropertyHook');
var ReactDOMNullInputValuePropHook = require('ReactDOMNullInputValuePropHook');
var ReactDOMInvalidARIAHook = require('ReactDOMInvalidARIAHook');
ReactInstrumentation.debugTool.addHook(ReactDOMUnknownPropertyHook);
ReactInstrumentation.debugTool.addHook(ReactDOMNullInputValuePropHook);
ReactInstrumentation.debugTool.addHook(ReactDOMInvalidARIAHook);
}
module.exports = ReactDOM;

View File

@ -0,0 +1,74 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ARIADOMPropertyConfig
*/
'use strict';
var ARIADOMPropertyConfig = {
Properties: {
// Global States and Properties
'aria-current': 0, // state
'aria-details': 0,
'aria-disabled': 0, // state
'aria-hidden': 0, // state
'aria-invalid': 0, // state
'aria-keyshortcuts': 0,
'aria-label': 0,
'aria-roledescription': 0,
// Widget Attributes
'aria-autocomplete': 0,
'aria-checked': 0,
'aria-expanded': 0,
'aria-haspopup': 0,
'aria-level': 0,
'aria-modal': 0,
'aria-multiline': 0,
'aria-multiselectable': 0,
'aria-orientation': 0,
'aria-placeholder': 0,
'aria-pressed': 0,
'aria-readonly': 0,
'aria-required': 0,
'aria-selected': 0,
'aria-sort': 0,
'aria-valuemax': 0,
'aria-valuemin': 0,
'aria-valuenow': 0,
'aria-valuetext': 0,
// Live Region Attributes
'aria-atomic': 0,
'aria-busy': 0,
'aria-live': 0,
'aria-relevant': 0,
// Drag-and-Drop Attributes
'aria-dropeffect': 0,
'aria-grabbed': 0,
// Relationship Attributes
'aria-activedescendant': 0,
'aria-colcount': 0,
'aria-colindex': 0,
'aria-colspan': 0,
'aria-controls': 0,
'aria-describedby': 0,
'aria-errormessage': 0,
'aria-flowto': 0,
'aria-labelledby': 0,
'aria-owns': 0,
'aria-posinset': 0,
'aria-rowcount': 0,
'aria-rowindex': 0,
'aria-rowspan': 0,
'aria-setsize': 0,
},
DOMAttributeNames: {},
DOMPropertyNames: {},
};
module.exports = ARIADOMPropertyConfig;

View File

@ -11,6 +11,7 @@
'use strict';
var ARIADOMPropertyConfig = require('ARIADOMPropertyConfig');
var BeforeInputEventPlugin = require('BeforeInputEventPlugin');
var ChangeEventPlugin = require('ChangeEventPlugin');
var DefaultEventPluginOrder = require('DefaultEventPluginOrder');
@ -73,6 +74,7 @@ function inject() {
ReactDOMTextComponent
);
ReactInjection.DOMProperty.injectDOMPropertyConfig(ARIADOMPropertyConfig);
ReactInjection.DOMProperty.injectDOMPropertyConfig(HTMLDOMPropertyConfig);
ReactInjection.DOMProperty.injectDOMPropertyConfig(SVGDOMPropertyConfig);

View File

@ -0,0 +1,69 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/
'use strict';
describe('ReactDOMInvalidARIAHook', () => {
var React;
var ReactTestUtils;
var mountComponent;
beforeEach(() => {
jest.resetModuleRegistry();
React = require('React');
ReactTestUtils = require('ReactTestUtils');
mountComponent = function(props) {
ReactTestUtils.renderIntoDocument(<div {...props} />);
};
});
describe('aria-* props', () => {
it('should allow valid aria-* props', () => {
spyOn(console, 'error');
mountComponent({'aria-label': 'Bumble bees'});
expect(console.error.calls.count()).toBe(0);
});
it('should warn for one invalid aria-* prop', () => {
spyOn(console, 'error');
mountComponent({'aria-badprop': 'maybe'});
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0]).toContain(
'Warning: Invalid aria prop `aria-badprop` on <div> tag. ' +
'For details, see https://fb.me/invalid-aria-prop'
);
});
it('should warn for many invalid aria-* props', () => {
spyOn(console, 'error');
mountComponent(
{
'aria-badprop': 'Very tall trees',
'aria-malprop': 'Turbulent seas',
}
);
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0]).toContain(
'Warning: Invalid aria props `aria-badprop`, `aria-malprop` on <div> ' +
'tag. For details, see https://fb.me/invalid-aria-prop'
);
});
it('should warn for an improperly cased aria-* prop', () => {
spyOn(console, 'error');
// The valid attribute name is aria-haspopup.
mountComponent({'aria-hasPopup': 'true'});
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0]).toContain(
'Warning: Unknown ARIA attribute aria-hasPopup. ' +
'Did you mean aria-haspopup?'
);
});
});
});

View File

@ -0,0 +1,119 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactDOMInvalidARIAHook
*/
'use strict';
var DOMProperty = require('DOMProperty');
var ReactComponentTreeHook = require('ReactComponentTreeHook');
var warning = require('warning');
var warnedProperties = {};
var rARIA = new RegExp('^(aria)-[' + DOMProperty.ATTRIBUTE_NAME_CHAR + ']*$');
function validateProperty(tagName, name, debugID) {
if (
warnedProperties.hasOwnProperty(name)
&& warnedProperties[name]
) {
return true;
}
if (rARIA.test(name)) {
var lowerCasedName = name.toLowerCase();
var standardName =
DOMProperty.getPossibleStandardName.hasOwnProperty(lowerCasedName) ?
DOMProperty.getPossibleStandardName[lowerCasedName] :
null;
// If this is an aria-* attribute, but is not listed in the known DOM
// DOM properties, then it is an invalid aria-* attribute.
if (standardName == null) {
warnedProperties[name] = true;
return false;
}
// aria-* attributes should be lowercase; suggest the lowercase version.
if (name !== standardName) {
warning(
false,
'Unknown ARIA attribute %s. Did you mean %s?%s',
name,
standardName,
ReactComponentTreeHook.getStackAddendumByID(debugID)
);
warnedProperties[name] = true;
return true;
}
}
return true;
}
function warnInvalidARIAProps(debugID, element) {
const invalidProps = [];
for (var key in element.props) {
var isValid = validateProperty(element.type, key, debugID);
if (!isValid) {
invalidProps.push(key);
}
}
const unknownPropString = invalidProps
.map(prop => '`' + prop + '`')
.join(', ');
if (invalidProps.length === 1) {
warning(
false,
'Invalid aria prop %s on <%s> tag. ' +
'For details, see https://fb.me/invalid-aria-prop%s',
unknownPropString,
element.type,
ReactComponentTreeHook.getStackAddendumByID(debugID)
);
} else if (invalidProps.length > 1) {
warning(
false,
'Invalid aria props %s on <%s> tag. ' +
'For details, see https://fb.me/invalid-aria-prop%s',
unknownPropString,
element.type,
ReactComponentTreeHook.getStackAddendumByID(debugID)
);
}
}
function handleElement(debugID, element) {
if (element == null || typeof element.type !== 'string') {
return;
}
if (element.type.indexOf('-') >= 0 || element.props.is) {
return;
}
warnInvalidARIAProps(debugID, element);
}
var ReactDOMInvalidARIAHook = {
onBeforeMountComponent(debugID, element) {
if (__DEV__) {
handleElement(debugID, element);
}
},
onBeforeUpdateComponent(debugID, element) {
if (__DEV__) {
handleElement(debugID, element);
}
},
};
module.exports = ReactDOMInvalidARIAHook;