Skip to content

Commit 08c5bc1

Browse files
badtantbeefancohen
authored andcommitted
[new] - Add rule anchor-has-content (#70)
Fixes #68
1 parent eaaeddf commit 08c5bc1

File tree

5 files changed

+177
-0
lines changed

5 files changed

+177
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ Then configure the rules you want to use under the rules section.
7070

7171
## Supported Rules
7272

73+
- [anchor-has-content](docs/rules/anchor-has-content.md): Enforce all anchors to contain accessible content.
7374
- [aria-props](docs/rules/aria-props.md): Enforce all `aria-*` props are valid.
7475
- [aria-proptypes](docs/rules/aria-proptypes.md): Enforce ARIA state and property values are valid.
7576
- [aria-role](docs/rules/aria-role.md): Enforce that elements with ARIA roles must use a valid, non-abstract ARIA role.

docs/rules/anchor-has-content.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# anchor-has-content
2+
3+
Enforce that anchors have content and that the content is accessible to screen readers. Accessible means that it is not hidden using the `aria-hidden` prop. Refer to the references to learn about why this is important.
4+
5+
#### References
6+
1. [Deque University](https://dequeuniversity.com/rules/axe/1.1/link-name)
7+
8+
## Rule details
9+
10+
This rule takes one optional argument of type string or array of strings. These strings determine which JSX elements should be checked including `a` and `Link` (react-router) by default. This is a good use case when you have a wrapper component that simply renders a anchor element (like in React):
11+
12+
```js
13+
// Anchor.js
14+
const Anchor = props => {
15+
return (
16+
<a {...props}>{ props.children }</a>
17+
);
18+
}
19+
20+
...
21+
22+
// CreateAccount.js (for example)
23+
...
24+
return (
25+
<Anchor>Create Account</Anchor>
26+
);
27+
```
28+
29+
To tell this plugin to also check your `Anchor` element, specify this in your `.eslintrc` file:
30+
31+
```json
32+
{
33+
"rules": {
34+
"jsx-a11y/anchor-has-content": [ 2, "Anchor" ], // OR
35+
"jsx-a11y/anchor-has-content": [ 2, [ "AnchorOne", "AnchorTwo" ] ]
36+
}
37+
}
38+
```
39+
40+
41+
### Succeed
42+
```jsx
43+
<a>Anchor Content!</a>
44+
<a><TextWrapper /><a>
45+
<a dangerouslySetInnerHTML={{ __html: 'foo' }} />
46+
```
47+
48+
### Fail
49+
```jsx
50+
<a />
51+
<a><TextWrapper aria-hidden />
52+
```

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
module.exports = {
44
rules: {
5+
'anchor-has-content': require('./rules/anchor-has-content'),
56
'aria-props': require('./rules/aria-props'),
67
'aria-proptypes': require('./rules/aria-proptypes'),
78
'aria-role': require('./rules/aria-role'),
@@ -32,6 +33,7 @@ module.exports = {
3233
},
3334
},
3435
rules: {
36+
'jsx-a11y/anchor-has-content': 2,
3537
'jsx-a11y/aria-props': 2,
3638
'jsx-a11y/aria-proptypes': 2,
3739
'jsx-a11y/aria-role': 2,

src/rules/anchor-has-content.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* @fileoverview Enforce anchor elements to contain accessible content.
3+
* @author Lisa Ring & Niklas Holmberg
4+
*/
5+
6+
// ----------------------------------------------------------------------------
7+
// Rule Definition
8+
// ----------------------------------------------------------------------------
9+
10+
import { elementType, hasProp } from 'jsx-ast-utils';
11+
import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';
12+
13+
const errorMessage =
14+
'Anchors must have content and the content must be accessible by a screen reader.';
15+
16+
const anchors = [
17+
'a',
18+
'Link',
19+
];
20+
21+
module.exports = context => ({
22+
JSXOpeningElement: node => {
23+
const typeCheck = anchors.concat(context.options[0]);
24+
const nodeType = elementType(node);
25+
26+
// Only check anchor elements and custom types.
27+
if (typeCheck.indexOf(nodeType) === -1) {
28+
return;
29+
}
30+
const isAccessible = node.parent.children.some(child => {
31+
switch (child.type) {
32+
case 'Literal':
33+
return Boolean(child.value);
34+
case 'JSXElement':
35+
return !isHiddenFromScreenReader(
36+
elementType(child.openingElement),
37+
child.openingElement.attributes
38+
);
39+
case 'JSXExpressionContainer':
40+
if (child.expression.type === 'Identifier') {
41+
return child.expression.name !== 'undefined';
42+
}
43+
return true;
44+
default:
45+
return false;
46+
}
47+
}) || hasProp(node.attributes, 'dangerouslySetInnerHTML');
48+
49+
50+
if (isAccessible) {
51+
return;
52+
}
53+
54+
context.report({
55+
node,
56+
message: errorMessage,
57+
});
58+
},
59+
});
60+
61+
module.exports.schema = [
62+
{
63+
oneOf: [
64+
{ type: 'string' },
65+
{
66+
type: 'array',
67+
items: {
68+
type: 'string',
69+
},
70+
minItems: 1,
71+
uniqueItems: true,
72+
},
73+
],
74+
},
75+
];

tests/src/rules/anchor-has-content.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* @fileoverview Enforce anchor elements to contain accessible content.
3+
* @author Lisa Ring & Niklas Holmberg
4+
*/
5+
6+
// -----------------------------------------------------------------------------
7+
// Requirements
8+
// -----------------------------------------------------------------------------
9+
10+
import rule from '../../../src/rules/anchor-has-content';
11+
import { RuleTester } from 'eslint';
12+
13+
const parserOptions = {
14+
ecmaVersion: 6,
15+
ecmaFeatures: {
16+
jsx: true,
17+
},
18+
};
19+
20+
// -----------------------------------------------------------------------------
21+
// Tests
22+
// -----------------------------------------------------------------------------
23+
24+
const ruleTester = new RuleTester();
25+
26+
const expectedError = {
27+
message: 'Anchors must have content and the content must be accessible by a screen reader.',
28+
type: 'JSXOpeningElement',
29+
};
30+
31+
ruleTester.run('anchor-has-content', rule, {
32+
valid: [
33+
{ code: '<div />;', parserOptions },
34+
{ code: '<a>Foo</a>', parserOptions },
35+
{ code: '<Link>Foo</Link>', parserOptions },
36+
{ code: '<a><Bar /></a>', parserOptions },
37+
{ code: '<a>{foo}</a>', parserOptions },
38+
{ code: '<a>{foo.bar}</a>', parserOptions },
39+
{ code: '<a dangerouslySetInnerHTML={{ __html: "foo" }} />', parserOptions },
40+
],
41+
invalid: [
42+
{ code: '<a />', errors: [expectedError], parserOptions },
43+
{ code: '<Link />', errors: [expectedError], parserOptions },
44+
{ code: '<a><Bar aria-hidden /></a>', errors: [expectedError], parserOptions },
45+
{ code: '<a>{undefined}</a>', errors: [expectedError], parserOptions },
46+
],
47+
});

0 commit comments

Comments
 (0)