Skip to content

Commit bd33c70

Browse files
authored
Merge pull request #136 from evcohen/ethanc/accessible-emoji
[new] - Add accessible-emoji rule.
2 parents 76df6bf + 4ed6fd9 commit bd33c70

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: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 an accessible description with aria-label or aria-labelledby.',
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 role="img" aria-labelledby="id1">🐼</span>', parserOptions },
39+
{ code: '<span role="img" aria-labelledby="id1">&#9731;</span>', parserOptions },
40+
{ code: '<span role="img" aria-labelledby="id1" aria-label="Snowman">&#9731;</span>', parserOptions },
41+
{ code: '<span>{props.emoji}</span>', parserOptions },
42+
],
43+
invalid: [
44+
{ code: '<span>🐼</span>', errors: [expectedError], parserOptions },
45+
{ code: '<span>foo🐼bar</span>', errors: [expectedError], parserOptions },
46+
{ code: '<span>foo 🐼 bar</span>', errors: [expectedError], parserOptions },
47+
{ code: '<i role="img" aria-label="Panda face">🐼</i>', errors: [expectedError], parserOptions },
48+
{ code: '<i role="img" aria-labelledby="id1">🐼</i>', errors: [expectedError], parserOptions },
49+
{ code: '<Foo>🐼</Foo>', errors: [expectedError], parserOptions },
50+
],
51+
});

docs/rules/accessible-emoji.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
<span role="img" aria-labelledby="panda1">🐼</span>
17+
```
18+
19+
### Fail
20+
```jsx
21+
<span>🐼</span>
22+
<i role="img" aria-label="Panda">🐼</i>
23+
```

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: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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 an accessible description with aria-label or aria-labelledby.';
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(
28+
child => child.type === 'Literal',
29+
);
30+
31+
if (literalChildValue && emojiRegex().test(literalChildValue.value)) {
32+
const rolePropValue = getLiteralPropValue(getProp(node.attributes, 'role'));
33+
const ariaLabelProp = getProp(node.attributes, 'aria-label');
34+
const arialLabelledByProp = getProp(node.attributes, 'aria-labelledby');
35+
const hasLabel = ariaLabelProp !== undefined || arialLabelledByProp !== undefined;
36+
const isSpan = elementType(node) === 'span';
37+
38+
if (hasLabel === false || rolePropValue !== 'img' || isSpan === false) {
39+
context.report({
40+
node,
41+
message: errorMessage,
42+
});
43+
}
44+
}
45+
},
46+
}),
47+
};

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)