Skip to content

Commit d31ebb5

Browse files
Ethan Cohenjessebeach
authored andcommitted
[new] - Add accessible-emoji rule.
1 parent 76df6bf commit d31ebb5

File tree

7 files changed

+129
-0
lines changed

7 files changed

+129
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ You can also enable all the recommended rules at once. Add `plugin:jsx-a11y/reco
8282

8383
## Supported Rules
8484

85+
- [accessible-emoji](docs/rules/accessible-emoji): Enforce emojis are wrapped in <span> and provide screenreader access.
8586
- [anchor-has-content](docs/rules/anchor-has-content.md): Enforce all anchors to contain accessible content.
8687
- [aria-props](docs/rules/aria-props.md): Enforce all `aria-*` props are valid.
8788
- [aria-proptypes](docs/rules/aria-proptypes.md): Enforce ARIA state and property values are valid.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* @fileoverview Enforce <marquee> elements are not used.
3+
* @author Ethan Cohen
4+
*/
5+
6+
// -----------------------------------------------------------------------------
7+
// Requirements
8+
// -----------------------------------------------------------------------------
9+
10+
import { RuleTester } from 'eslint';
11+
import rule from '../../../src/rules/accessible-emoji';
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: 'Emojis should be wrapped in <span>, have role="img", and have aria-label="Description of emoji".',
28+
type: 'JSXOpeningElement',
29+
};
30+
31+
ruleTester.run('accessible-emoji', rule, {
32+
valid: [
33+
{ code: '<div />;', parserOptions },
34+
{ code: '<span />', parserOptions },
35+
{ code: '<span>No emoji here!</span>', parserOptions },
36+
{ code: '<span role="img" aria-label="Panda face">🐼</span>', parserOptions },
37+
{ code: '<span role="img" aria-label="Snowman">&#9731;</span>', parserOptions },
38+
{ code: '<span>{props.emoji}</span>', parserOptions },
39+
],
40+
invalid: [
41+
{ code: '<span>🐼</span>', errors: [expectedError], parserOptions },
42+
{ code: '<span>foo🐼bar</span>', errors: [expectedError], parserOptions },
43+
{ code: '<span>foo 🐼 bar</span>', errors: [expectedError], parserOptions },
44+
{ code: '<i role="img" aria-label="Panda face">🐼</i>', errors: [expectedError], parserOptions },
45+
{ code: '<Foo>🐼</Foo>', errors: [expectedError], parserOptions },
46+
],
47+
});

docs/rules/accessible-emoji.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# accessible-emoji
2+
3+
Emojis have become a common way of communicating content to the end user. To a person using a screenreader, however, he/she may not be aware that this content is there at all. By wrapping the emoji in a `<span>`, giving it the `role="img"`, and providing a useful description in `aria-label`, the screenreader will treat the emoji as an image in the accessibility tree with an accessible name for the end user.
4+
5+
#### Resources
6+
1. [Lèonie Watson](http://tink.uk/accessible-emoji/)
7+
8+
## Rule details
9+
10+
This rule takes no arguments.
11+
12+
### Succeed
13+
```jsx
14+
<span role="img" aria-label="Snowman">&#9731;</span>
15+
<span role="img" aria-label="Panda">🐼</span>
16+
```
17+
18+
### Fail
19+
```jsx
20+
<span>🐼</span>
21+
<i role="img" aria-label="Panda">🐼</i>
22+
```

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"license": "MIT",
4747
"dependencies": {
4848
"damerau-levenshtein": "^1.0.0",
49+
"emoji-regex": "^6.1.0",
4950
"jsx-ast-utils": "^1.0.0",
5051
"object-assign": "^4.0.1"
5152
},

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+
'accessible-emoji': require('./rules/accessible-emoji'),
56
'anchor-has-content': require('./rules/anchor-has-content'),
67
'aria-props': require('./rules/aria-props'),
78
'aria-proptypes': require('./rules/aria-proptypes'),
@@ -36,6 +37,7 @@ module.exports = {
3637
},
3738
},
3839
rules: {
40+
'jsx-a11y/accessible-emoji': 'error',
3941
'jsx-a11y/anchor-has-content': 'error',
4042
'jsx-a11y/aria-props': 'error',
4143
'jsx-a11y/aria-proptypes': 'error',

src/rules/accessible-emoji.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @fileoverview Enforce emojis are wrapped in <span> and provide screenreader access.
3+
* @author Ethan Cohen
4+
*/
5+
6+
// ----------------------------------------------------------------------------
7+
// Rule Definition
8+
// ----------------------------------------------------------------------------
9+
10+
import emojiRegex from 'emoji-regex';
11+
import { getProp, getLiteralPropValue, elementType } from 'jsx-ast-utils';
12+
import { generateObjSchema } from '../util/schemas';
13+
14+
const errorMessage =
15+
'Emojis should be wrapped in <span>, have role="img", and have aria-label="Description of emoji".';
16+
17+
const schema = generateObjSchema();
18+
19+
module.exports = {
20+
meta: {
21+
docs: {},
22+
schema: [schema],
23+
},
24+
25+
create: context => ({
26+
JSXOpeningElement: (node) => {
27+
const literalChildValue = node.parent.children.find((child) => {
28+
if (child.type === 'Literal') {
29+
return child.value;
30+
}
31+
return false;
32+
});
33+
34+
if (literalChildValue) {
35+
const containsEmoji = emojiRegex().test(literalChildValue.value);
36+
37+
if (containsEmoji) {
38+
const roleProp = getProp(node.attributes, 'role');
39+
const rolePropValue = getLiteralPropValue(roleProp);
40+
const ariaLabelProp = getProp(node.attributes, 'aria-label');
41+
const isSpan = elementType(node) === 'span';
42+
if (ariaLabelProp === 'undefined' || rolePropValue !== 'img' || isSpan === false) {
43+
context.report({
44+
node,
45+
message: errorMessage,
46+
});
47+
}
48+
}
49+
}
50+
},
51+
}),
52+
};

yarn.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,6 +1038,10 @@ ecc-jsbn@~0.1.1:
10381038
dependencies:
10391039
jsbn "~0.1.0"
10401040

1041+
emoji-regex@^6.1.0:
1042+
version "6.1.0"
1043+
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.1.0.tgz#d14ef743a7dfa6eaf436882bd1920a4aed84dd94"
1044+
10411045
"errno@>=0.1.1 <0.2.0-0":
10421046
version "0.1.4"
10431047
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"

0 commit comments

Comments
 (0)