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:
parent
72ed5df5a4
commit
59ff7749ed
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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?'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
Loading…
Reference in New Issue