forked from jsx-eslint/eslint-plugin-jsx-a11y
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request jsx-eslint#833 from JoshuaKGoldberg/prefer-tag-ove…
…r-role New rule: prefer-tag-over-role
- Loading branch information
Showing
3 changed files
with
183 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { RuleTester } from 'eslint'; | ||
import parserOptionsMapper from '../../__util__/parserOptionsMapper'; | ||
import rule from '../../../src/rules/prefer-tag-over-role'; | ||
|
||
const ruleTester = new RuleTester(); | ||
|
||
const expectedError = (role, tag) => ({ | ||
message: `Use ${tag} instead of the "${role}" role to ensure accessibility across all devices.`, | ||
type: 'JSXOpeningElement', | ||
}); | ||
|
||
ruleTester.run('element-role', rule, { | ||
valid: [ | ||
{ code: '<div />;' }, | ||
{ code: '<div role="unknown" />;' }, | ||
{ code: '<div role="also unknown" />;' }, | ||
{ code: '<other />' }, | ||
{ code: '<img role="img" />' }, | ||
{ code: '<input role="checkbox" />' }, | ||
].map(parserOptionsMapper), | ||
invalid: [ | ||
{ | ||
code: '<div role="checkbox" />', | ||
errors: [expectedError('checkbox', '<input type="checkbox">')], | ||
}, | ||
{ | ||
code: '<div role="button checkbox" />', | ||
errors: [expectedError('checkbox', '<input type="checkbox">')], | ||
}, | ||
{ | ||
code: '<div role="heading" />', | ||
errors: [ | ||
expectedError('heading', '<h1>, <h2>, <h3>, <h4>, <h5>, or <h6>'), | ||
], | ||
}, | ||
{ | ||
code: '<div role="link" />', | ||
errors: [ | ||
expectedError( | ||
'link', | ||
'<a href=...>, <area href=...>, or <link href=...>', | ||
), | ||
], | ||
}, | ||
{ | ||
code: '<div role="rowgroup" />', | ||
errors: [expectedError('rowgroup', '<tbody>, <tfoot>, or <thead>')], | ||
}, | ||
{ | ||
code: '<span role="checkbox" />', | ||
errors: [expectedError('checkbox', '<input type="checkbox">')], | ||
}, | ||
{ | ||
code: '<other role="checkbox" />', | ||
errors: [expectedError('checkbox', '<input type="checkbox">')], | ||
}, | ||
{ | ||
code: '<other role="checkbox" />', | ||
errors: [expectedError('checkbox', '<input type="checkbox">')], | ||
}, | ||
{ | ||
code: '<div role="banner" />', | ||
errors: [expectedError('banner', '<header>')], | ||
}, | ||
].map(parserOptionsMapper), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# prefer-tag-over-role | ||
|
||
Enforces using semantic DOM elements over the ARIA `role` property. | ||
|
||
## Rule details | ||
|
||
This rule takes no arguments. | ||
|
||
### Succeed | ||
|
||
```jsx | ||
<div>...</div> | ||
<header>...</header> | ||
<img alt="" src="image.jpg" /> | ||
``` | ||
|
||
### Fail | ||
|
||
```jsx | ||
<div role="checkbox"> | ||
<div role="img"> | ||
``` | ||
|
||
## Accessibility guidelines | ||
|
||
- [WAI-ARIA Roles model](https://www.w3.org/TR/wai-aria-1.0/roles) | ||
|
||
### Resources | ||
|
||
- [MDN WAI-ARIA Roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { roleElements } from 'aria-query'; | ||
import { getProp, getPropValue } from 'jsx-ast-utils'; | ||
|
||
import getElementType from '../util/getElementType'; | ||
import { generateObjSchema } from '../util/schemas'; | ||
|
||
const errorMessage = 'Use {{tag}} instead of the "{{role}}" role to ensure accessibility across all devices.'; | ||
|
||
const schema = generateObjSchema(); | ||
|
||
const formatTag = (tag) => { | ||
if (!tag.attributes) { | ||
return `<${tag.name}>`; | ||
} | ||
|
||
const [attribute] = tag.attributes; | ||
const value = attribute.value ? `"${attribute.value}"` : '...'; | ||
|
||
return `<${tag.name} ${attribute.name}=${value}>`; | ||
}; | ||
|
||
const getLastPropValue = (rawProp) => { | ||
const propValue = getPropValue(rawProp); | ||
if (!propValue) { | ||
return propValue; | ||
} | ||
|
||
const lastSpaceIndex = propValue.lastIndexOf(' '); | ||
|
||
return lastSpaceIndex === -1 | ||
? propValue | ||
: propValue.substring(lastSpaceIndex + 1); | ||
}; | ||
|
||
export default { | ||
meta: { | ||
docs: { | ||
url: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/prefer-tag-over-role.md', | ||
}, | ||
schema: [schema], | ||
}, | ||
|
||
create: (context) => { | ||
const elementType = getElementType(context); | ||
|
||
return { | ||
JSXOpeningElement: (node) => { | ||
const role = getLastPropValue(getProp(node.attributes, 'role')); | ||
if (!role) { | ||
return; | ||
} | ||
|
||
const matchedTagsSet = roleElements.get(role); | ||
if (!matchedTagsSet) { | ||
return; | ||
} | ||
|
||
const matchedTags = Array.from(matchedTagsSet); | ||
if ( | ||
matchedTags.some( | ||
(matchedTag) => matchedTag.name === elementType(node), | ||
) | ||
) { | ||
return; | ||
} | ||
|
||
context.report({ | ||
data: { | ||
tag: | ||
matchedTags.length === 1 | ||
? formatTag(matchedTags[0]) | ||
: [ | ||
matchedTags | ||
.slice(0, matchedTags.length - 1) | ||
.map(formatTag) | ||
.join(', '), | ||
formatTag(matchedTags[matchedTags.length - 1]), | ||
].join(', or '), | ||
role, | ||
}, | ||
node, | ||
message: errorMessage, | ||
}); | ||
}, | ||
}; | ||
}, | ||
}; |