Repository: ember-template-lint/ember-template-lint
Pull request #630 (2019-01-24 10:34:08)

feat: [a11y] img-redundant-alt
ref: https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/img-redundant-alt.md)


Patch:
diff --git a/docs/rule/require-valid-alt-text.md b/docs/rule/require-valid-alt-text.md
index 2da0cec0..c76e5cce 100644
--- a/docs/rule/require-valid-alt-text.md
+++ b/docs/rule/require-valid-alt-text.md
@@ -2,6 +2,8 @@
 
 Enforce that all elements that require alternative text have meaningful information to relay back to the end user. This is a critical component of accessibility for screenreader users in order for them to understand the content's purpose on the page. By default, this rule checks for alternative text on the following elements: `<img>`, `<area>`, `<input type="image">`, and `<object>`.
 
+Enforce `img` alt attribute does not contain the word image, picture, or photo. Screenreaders already announce `img` elements as an image. There is no need to use words such as *image*, *photo*, and/or *picture*. The rule will first check if `aria-hidden` is true to determine whether to enforce the rule. If the image is hidden, then rule will always succeed.
+
 This rule **forbids** the following:
 
 ### `<img>`
@@ -12,16 +14,24 @@ The content of an `alt` attribute is used to calculate the machine-readable labe
 
 If it's not a meaningful image, it should have an empty alt attribute value and have the role of presentation or none.
 
+`img` alt attribute does not contain the word image, picture, or photo. Screenreaders already announce `img` elements as an image. There is no need to use words such as *image*, *photo*, and/or *picture*.
+
+
 This rule **forbids** the following:
 
 ```hbs
 <img src="rwjblue.png">
+<img src="foo" alt="Photo of foo being weird." />
+<img src="bar" alt="Image of me at a bar!" />
+<img src="baz" alt="Picture of baz fixing a bug." />
 ```
 
 This rule **allows** the following:
 
 ```hbs
 <img src="rwjblue.png" alt="A man standing in front of a room of people, giving a presentation about Ember.">
+<img src="bar" aria-hidden alt="Picture of me taking a photo of an image" /> // Will pass because it is hidden.
+<img src="baz" alt="Baz taking a {{photo}}" /> // This is valid since photo is a variable name.
 ```
 
 ### `<object>`
diff --git a/lib/rules/lint-require-valid-alt-text.js b/lib/rules/lint-require-valid-alt-text.js
index ce769b92..71f6292b 100644
--- a/lib/rules/lint-require-valid-alt-text.js
+++ b/lib/rules/lint-require-valid-alt-text.js
@@ -7,6 +7,11 @@ function hasAccessibleChild(node) {
   return AstNodeInfo.hasChildren(node);
 }
 
+const REDUNDANT_WORDS = ['image', 'photo', 'picture'];
+
+const errorMessage =
+  'Redundant alt attribute. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom words) in the alt attribute.';
+
 module.exports = class A11yLintAltText extends Rule {
   logNode({ node, message }) {
     return this.log({
@@ -34,7 +39,6 @@ module.exports = class A11yLintAltText extends Rule {
 
         if (isImg) {
           const hasAltAttribute = AstNodeInfo.hasAttribute(node, 'alt');
-          const altValue = AstNodeInfo.elementAttributeValue(node, 'alt');
           const roleValue = AstNodeInfo.elementAttributeValue(node, 'role');
 
           if (!hasAltAttribute) {
@@ -43,15 +47,50 @@ module.exports = class A11yLintAltText extends Rule {
               node,
             });
           } else {
-            if (altValue === '') {
-              if (['presentation', 'none'].includes(roleValue)) {
+            const altAttribute = AstNodeInfo.findAttribute(node, 'alt');
+            if (altAttribute.value) {
+              let normalizedAltValue = '';
+              if (altAttribute.value.type === 'TextNode') {
+                normalizedAltValue = altAttribute.value.chars.trim().toLowerCase();
+              } else if (altAttribute.value.type === 'ConcatStatement') {
+                normalizedAltValue = altAttribute.value.parts
+                  .filter(part => part.type === 'TextNode')
+                  .map(part => part.chars)
+                  .join(' ')
+                  .trim()
+                  .toLowerCase();
+                if (normalizedAltValue === '') {
+                  normalizedAltValue = null;
+                }
+              } else {
+                normalizedAltValue = null;
+              }
+
+              if (normalizedAltValue === '') {
+                if (['presentation', 'none'].includes(roleValue)) {
+                  return;
+                }
+                this.logNode({
+                  message:
+                    'if the `alt` attribute is present and the value is an empty string, `role="presentation"` or `role="none"` must be present',
+                  node,
+                });
                 return;
               }
-              this.logNode({
-                message:
-                  'if the `alt` attribute is present and the value is an empty string, `role="presentation"` or `role="none"` must be present',
-                node,
-              });
+
+              if (normalizedAltValue !== null) {
+                const existingWords = REDUNDANT_WORDS.filter(
+                  word => normalizedAltValue.indexOf(word) !== -1
+                );
+                if (existingWords.length) {
+                  this.log({
+                    message: errorMessage,
+                    line: node.loc && node.loc.start.line,
+                    column: node.loc && node.loc.start.column,
+                    source: this.sourceForNode(node),
+                  });
+                }
+              }
             }
           }
         } else if (isInput) {


Test patch:
diff --git a/test/unit/rules/lint-require-valid-alt-text-test.js b/test/unit/rules/lint-require-valid-alt-text-test.js
index d1932f4a..31e9c370 100644
--- a/test/unit/rules/lint-require-valid-alt-text-test.js
+++ b/test/unit/rules/lint-require-valid-alt-text-test.js
@@ -17,6 +17,11 @@ generateRuleTests({
     '<img alt="" role="presentation" src="zoey.jpg">',
     '<img alt="a stylized graphic of a female hamster" src="zoey.jpg">',
 
+    '<img alt="some-alt-name">',
+    '<img alt="name {{picture}}">',
+    '<img aria-hidden="true">',
+    '<img alt="{{picture}}">',
+
     '<input type="image" alt="some-alt">',
     '<input type="image" aria-labelledby="some-alt">',
     '<input type="image" aria-label="some-alt">',
@@ -131,5 +136,65 @@ generateRuleTests({
         column: 0,
       },
     },
+    {
+      template: '<img alt="picture">',
+
+      result: {
+        message:
+          'Redundant alt attribute. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom words) in the alt attribute.',
+        moduleId: 'layout.hbs',
+        source: '<img alt="picture">',
+        line: 1,
+        column: 0,
+      },
+    },
+    {
+      template: '<img alt="photo">',
+
+      result: {
+        message:
+          'Redundant alt attribute. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom words) in the alt attribute.',
+        moduleId: 'layout.hbs',
+        source: '<img alt="photo">',
+        line: 1,
+        column: 0,
+      },
+    },
+    {
+      template: '<img alt="image">',
+
+      result: {
+        message:
+          'Redundant alt attribute. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom words) in the alt attribute.',
+        moduleId: 'layout.hbs',
+        source: '<img alt="image">',
+        line: 1,
+        column: 0,
+      },
+    },
+    {
+      template: '<img alt="  IMAGE ">',
+
+      result: {
+        message:
+          'Redundant alt attribute. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom words) in the alt attribute.',
+        moduleId: 'layout.hbs',
+        source: '<img alt="  IMAGE ">',
+        line: 1,
+        column: 0,
+      },
+    },
+    {
+      template: '<img alt="  IMAGE {{picture}} {{word}} ">',
+
+      result: {
+        message:
+          'Redundant alt attribute. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom words) in the alt attribute.',
+        moduleId: 'layout.hbs',
+        source: '<img alt="  IMAGE {{picture}} {{word}} ">',
+        line: 1,
+        column: 0,
+      },
+    },
   ],
 });