Skip to content

Commit 84a9771

Browse files
sjarvaljharb
authored andcommitted
[Fix] no-unknown-property: properly recognize valid data- and aria- attributes
1 parent d6d84cc commit 84a9771

File tree

4 files changed

+64
-1
lines changed

4 files changed

+64
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
88
### Fixed
99
* [`jsx-key`]: avoid a crash with optional chaining ([#3371][] @ljharb)
1010
* [`jsx-sort-props`]: avoid a crash with spread props ([#3376][] @ljharb)
11+
* [`no-unknown-property`]: properly recognize valid data- and aria- attributes ([#3377][] @sjarva)
1112

1213
### Changed
1314
* [Docs] [`jsx-sort-props`]: replace ref string with ref variable ([#3375][] @Luccasoli)

docs/rules/no-unknown-property.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
🔧 This rule is automatically fixable using the `--fix` [flag](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) on the command line.
66

7-
In JSX all DOM properties and attributes should be camelCased to be consistent with standard JavaScript style. This can be a possible source of error if you are used to writing plain HTML.
7+
In JSX most DOM properties and attributes should be camelCased to be consistent with standard JavaScript style. This can be a possible source of error if you are used to writing plain HTML.
8+
Only `data-*` and `aria-*` attributes are usings hyphens and lowercase letters in JSX.
89

910
## Rule Details
1011

@@ -22,6 +23,14 @@ Examples of **correct** code for this rule:
2223
var React = require('react');
2324

2425
var Hello = <div className="hello">Hello World</div>;
26+
27+
// aria-* attributes
28+
var IconButton = <button aria-label="Close" onClick={this.close}>{closeIcon}</button>;
29+
30+
31+
// data-* attributes
32+
var Data = <div data-index={12}>Some data</div>;
33+
2534
```
2635

2736
## Rule Options

lib/rules/no-unknown-property.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,23 @@ const DOM_PROPERTY_NAMES = [
137137

138138
const DOM_PROPERTIES_IGNORE_CASE = ['charset'];
139139

140+
const ARIA_PROPERTIES = [
141+
// See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes
142+
// Global attributes
143+
'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current',
144+
'aria-describedby', 'aria-description', 'aria-details',
145+
'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-flowto', 'aria-grabbed', 'aria-haspopup',
146+
'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live',
147+
'aria-owns', 'aria-relevant', 'aria-roledescription',
148+
// Widget attributes
149+
'aria-autocomplete', 'aria-checked', 'aria-expanded', 'aria-level', 'aria-modal', 'aria-multiline', 'aria-multiselectable',
150+
'aria-orientation', 'aria-placeholder', 'aria-pressed', 'aria-readonly', 'aria-required', 'aria-selected',
151+
'aria-sort', 'aria-valuemax', 'aria-valuemin', 'aria-valuenow', 'aria-valuetext',
152+
// Relationship attributes
153+
'aria-activedescendant', 'aria-colcount', 'aria-colindex', 'aria-colindextext', 'aria-colspan',
154+
'aria-posinset', 'aria-rowcount', 'aria-rowindex', 'aria-rowindextext', 'aria-rowspan', 'aria-setsize',
155+
];
156+
140157
function getDOMPropertyNames(context) {
141158
// this was removed in React v16.1+, see https://github.com/facebook/react/pull/10823
142159
if (!testReactVersion(context, '>= 16.1.0')) {
@@ -176,6 +193,31 @@ function isValidHTMLTagInJSX(childNode) {
176193
return false;
177194
}
178195

196+
/**
197+
* Checks if an attribute name is a valid `data-*` attribute:
198+
* if the name starts with "data-" and has some lowcase (a to z) words, separated but hyphens (-)
199+
* (which is also called "kebab case" or "dash case"), then the attribute is valid data attribute.
200+
*
201+
* @param {String} name - Attribute name to be tested
202+
* @returns {boolean} Result
203+
*/
204+
function isValidDataAttribute(name) {
205+
const dataAttrConvention = /^data(-[a-z]*)*$/;
206+
return !!dataAttrConvention.test(name);
207+
}
208+
209+
/**
210+
* Checks if an attribute name is a standard aria attribute by compering it to a list
211+
* of standard aria property names
212+
*
213+
* @param {String} name - Attribute name to be tested
214+
* @returns {Boolean} Result
215+
*/
216+
217+
function isValidAriaAttribute(name) {
218+
return ARIA_PROPERTIES.some((element) => element === name);
219+
}
220+
179221
/**
180222
* Checks if the attribute name is included in the attributes that are excluded
181223
* from the camel casing.
@@ -291,6 +333,10 @@ module.exports = {
291333
return;
292334
}
293335

336+
if (isValidDataAttribute(name)) { return; }
337+
338+
if (isValidAriaAttribute(name)) { return; }
339+
294340
if (isCaseIgnoredAttribute(name)) { return; }
295341

296342
const tagName = getTagName(node);

tests/lib/rules/no-unknown-property.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,20 @@ ruleTester.run('no-unknown-property', rule, {
4343
{ code: '<App clip-path="bar" />;' },
4444
{ code: '<div className="bar"></div>;' },
4545
{ code: '<div onMouseDown={this._onMouseDown}></div>;' },
46+
// data attributes should work
4647
{ code: '<div data-foo="bar"></div>;' },
48+
{ code: '<div data-foo-bar="baz"></div>;' },
49+
{ code: '<div data-parent="parent"></div>;' },
50+
{ code: '<div data-index-number="1234"></div>;' },
4751
{ code: '<div class="foo" is="my-elem"></div>;' },
4852
{ code: '<div {...this.props} class="foo" is="my-elem"></div>;' },
4953
{ code: '<atom-panel class="foo"></atom-panel>;' }, {
5054
code: '<div class="bar"></div>;',
5155
options: [{ ignore: ['class'] }],
5256
},
57+
// aria attributes should work
58+
{ code: '<button aria-haspopup="true">Click me to open pop up</button>;' },
59+
{ code: '<button aria-label="Close" onClick={someThing.close} />;' },
5360
{ code: '<script crossOrigin />' },
5461
{ code: '<audio crossOrigin />' },
5562
{ code: '<div hasOwnProperty="should not be allowed tag" />' },

0 commit comments

Comments
 (0)