diff --git a/package-lock.json b/package-lock.json index 8f0b7469f887..ba966f731985 100644 --- a/package-lock.json +++ b/package-lock.json @@ -824,9 +824,9 @@ } }, "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, "end-of-stream": { @@ -3371,6 +3371,14 @@ "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" + }, + "dependencies": { + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + } } }, "string_decoder": { diff --git a/package.json b/package.json index 6000e9d1c47c..f88a7e66be23 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "code-red": "0.1.1", "codecov": "^3.5.0", "css-tree": "1.0.0-alpha22", + "emoji-regex": "^8.0.0", "eslint": "^6.3.0", "eslint-plugin-import": "^2.18.2", "eslint-plugin-svelte3": "^2.7.3", diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index e8108858c53b..3681e30e2363 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -1,3 +1,4 @@ +import emojiRegex from 'emoji-regex'; import { is_void } from '../../utils/names'; import Node from './shared/Node'; import Attribute from './Attribute'; @@ -274,6 +275,7 @@ export default class Element extends Node { this.validate_attributes(); this.validate_bindings(); this.validate_content(); + this.validate_emoji(); this.validate_event_handlers(); } @@ -671,6 +673,36 @@ export default class Element extends Node { } } + validate_emoji() { + if (this.children.length === 1) { + const child = this.children[0]; + if (child.type === 'Text' && emojiRegex().test(child.data)) { + + const isHidden = this.attributes.find( + (attribute: Attribute) => attribute.name === 'aria-hidden' + ); + if (isHidden && (isHidden.is_true || isHidden.get_static_value() === "true")) { + return; // emoji is decorative + } + + const hasLabel = this.attributes.find( + (attribute: Attribute) => attribute.name === 'aria-label' || attribute.name === 'aria-labelledby' + ); + const role = this.attributes.find( + (attribute: Attribute) => attribute.name === 'role' + ); + const isSpan = this.name === 'span'; + + if (!hasLabel || role.get_static_value() !== 'img' || isSpan === false) { + this.component.warn(this, { + code: `a11y-accessible-emoji`, + message: `A11y: Emojis should be wrapped in , have role="img", and have an accessible description with aria-label or aria-labelledby. ` + }); + } + } + } + } + validate_event_handlers() { const { component } = this; diff --git a/test/validator/samples/a11y-accessible-emoji/input.svelte b/test/validator/samples/a11y-accessible-emoji/input.svelte new file mode 100644 index 000000000000..df378d3f0417 --- /dev/null +++ b/test/validator/samples/a11y-accessible-emoji/input.svelte @@ -0,0 +1,18 @@ +
+ +No emoji here! +๐Ÿผ + +๐Ÿผ + + + +๐Ÿผ + + +๐Ÿผ +foo๐Ÿผbar +foo ๐Ÿผ bar +๐Ÿผ +๐Ÿผ +๐Ÿผ diff --git a/test/validator/samples/a11y-accessible-emoji/warnings.json b/test/validator/samples/a11y-accessible-emoji/warnings.json new file mode 100644 index 000000000000..9ef494cf3975 --- /dev/null +++ b/test/validator/samples/a11y-accessible-emoji/warnings.json @@ -0,0 +1,92 @@ +[ + { + "code": "a11y-accessible-emoji", + "message": "A11y: Emojis should be wrapped in , have role=\"img\", and have an accessible description with aria-label or aria-labelledby. ", + "start": { + "line": 13, + "column": 0, + "character": 424 + }, + "end": { + "line": 13, + "column": 15, + "character": 439 + }, + "pos": 424 + }, + { + "code": "a11y-accessible-emoji", + "message": "A11y: Emojis should be wrapped in , have role=\"img\", and have an accessible description with aria-label or aria-labelledby. ", + "start": { + "line": 14, + "column": 0, + "character": 440 + }, + "end": { + "line": 14, + "column": 21, + "character": 461 + }, + "pos": 440 + }, + { + "code": "a11y-accessible-emoji", + "message": "A11y: Emojis should be wrapped in , have role=\"img\", and have an accessible description with aria-label or aria-labelledby. ", + "start": { + "line": 15, + "column": 0, + "character": 462 + }, + "end": { + "line": 15, + "column": 23, + "character": 485 + }, + "pos": 462 + }, + { + "code": "a11y-accessible-emoji", + "message": "A11y: Emojis should be wrapped in , have role=\"img\", and have an accessible description with aria-label or aria-labelledby. ", + "start": { + "line": 16, + "column": 0, + "character": 486 + }, + "end": { + "line": 16, + "column": 44, + "character": 530 + }, + "pos": 486 + }, + { + "code": "a11y-accessible-emoji", + "message": "A11y: Emojis should be wrapped in , have role=\"img\", and have an accessible description with aria-label or aria-labelledby. ", + "start": { + "line": 17, + "column": 0, + "character": 531 + }, + "end": { + "line": 17, + "column": 42, + "character": 573 + }, + "pos": 531 + }, + { + "code": "a11y-accessible-emoji", + "message": "A11y: Emojis should be wrapped in , have role=\"img\", and have an accessible description with aria-label or aria-labelledby. ", + "start": { + "line": 18, + "column": 0, + "character": 574 + }, + "end": { + "line": 18, + "column": 35, + "character": 609 + }, + "pos": 574 + } +]