alt-text.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. "use strict";
  2. var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
  3. var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
  4. var _jsxAstUtils = require("jsx-ast-utils");
  5. var _schemas = require("../util/schemas");
  6. var _hasAccessibleChild = _interopRequireDefault(require("../util/hasAccessibleChild"));
  7. var _isPresentationRole = _interopRequireDefault(require("../util/isPresentationRole"));
  8. /**
  9. * @fileoverview Enforce all elements that require alternative text have it.
  10. * @author Ethan Cohen
  11. */
  12. // ----------------------------------------------------------------------------
  13. // Rule Definition
  14. // ----------------------------------------------------------------------------
  15. var DEFAULT_ELEMENTS = ['img', 'object', 'area', 'input[type="image"]'];
  16. var schema = (0, _schemas.generateObjSchema)({
  17. elements: _schemas.arraySchema,
  18. img: _schemas.arraySchema,
  19. object: _schemas.arraySchema,
  20. area: _schemas.arraySchema,
  21. 'input[type="image"]': _schemas.arraySchema
  22. });
  23. var ariaLabelHasValue = function ariaLabelHasValue(prop) {
  24. var value = (0, _jsxAstUtils.getPropValue)(prop);
  25. if (value === undefined) {
  26. return false;
  27. }
  28. if (typeof value === 'string' && value.length === 0) {
  29. return false;
  30. }
  31. return true;
  32. };
  33. var ruleByElement = {
  34. img(context, node) {
  35. var nodeType = (0, _jsxAstUtils.elementType)(node);
  36. var altProp = (0, _jsxAstUtils.getProp)(node.attributes, 'alt'); // Missing alt prop error.
  37. if (altProp === undefined) {
  38. if ((0, _isPresentationRole["default"])(nodeType, node.attributes)) {
  39. context.report({
  40. node,
  41. message: 'Prefer alt="" over a presentational role. First rule of aria is to not use aria if it can be achieved via native HTML.'
  42. });
  43. return;
  44. } // Check for `aria-label` to provide text alternative
  45. // Don't create an error if the attribute is used correctly. But if it
  46. // isn't, suggest that the developer use `alt` instead.
  47. var ariaLabelProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-label');
  48. if (ariaLabelProp !== undefined) {
  49. if (!ariaLabelHasValue(ariaLabelProp)) {
  50. context.report({
  51. node,
  52. message: 'The aria-label attribute must have a value. The alt attribute is preferred over aria-label for images.'
  53. });
  54. }
  55. return;
  56. } // Check for `aria-labelledby` to provide text alternative
  57. // Don't create an error if the attribute is used correctly. But if it
  58. // isn't, suggest that the developer use `alt` instead.
  59. var ariaLabelledbyProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-labelledby');
  60. if (ariaLabelledbyProp !== undefined) {
  61. if (!ariaLabelHasValue(ariaLabelledbyProp)) {
  62. context.report({
  63. node,
  64. message: 'The aria-labelledby attribute must have a value. The alt attribute is preferred over aria-labelledby for images.'
  65. });
  66. }
  67. return;
  68. }
  69. context.report({
  70. node,
  71. message: "".concat(nodeType, " elements must have an alt prop, either with meaningful text, or an empty string for decorative images.")
  72. });
  73. return;
  74. } // Check if alt prop is undefined.
  75. var altValue = (0, _jsxAstUtils.getPropValue)(altProp);
  76. var isNullValued = altProp.value === null; // <img alt />
  77. if (altValue && !isNullValued || altValue === '') {
  78. return;
  79. } // Undefined alt prop error.
  80. context.report({
  81. node,
  82. message: "Invalid alt value for ".concat(nodeType, ". Use alt=\"\" for presentational images.")
  83. });
  84. },
  85. object(context, node) {
  86. var ariaLabelProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-label');
  87. var arialLabelledByProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-labelledby');
  88. var hasLabel = ariaLabelHasValue(ariaLabelProp) || ariaLabelHasValue(arialLabelledByProp);
  89. var titleProp = (0, _jsxAstUtils.getLiteralPropValue)((0, _jsxAstUtils.getProp)(node.attributes, 'title'));
  90. var hasTitleAttr = !!titleProp;
  91. if (hasLabel || hasTitleAttr || (0, _hasAccessibleChild["default"])(node.parent)) {
  92. return;
  93. }
  94. context.report({
  95. node,
  96. message: 'Embedded <object> elements must have alternative text by providing inner text, aria-label or aria-labelledby props.'
  97. });
  98. },
  99. area(context, node) {
  100. var ariaLabelProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-label');
  101. var arialLabelledByProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-labelledby');
  102. var hasLabel = ariaLabelHasValue(ariaLabelProp) || ariaLabelHasValue(arialLabelledByProp);
  103. if (hasLabel) {
  104. return;
  105. }
  106. var altProp = (0, _jsxAstUtils.getProp)(node.attributes, 'alt');
  107. if (altProp === undefined) {
  108. context.report({
  109. node,
  110. message: 'Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.'
  111. });
  112. return;
  113. }
  114. var altValue = (0, _jsxAstUtils.getPropValue)(altProp);
  115. var isNullValued = altProp.value === null; // <area alt />
  116. if (altValue && !isNullValued || altValue === '') {
  117. return;
  118. }
  119. context.report({
  120. node,
  121. message: 'Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.'
  122. });
  123. },
  124. 'input[type="image"]': function inputImage(context, node) {
  125. // Only test input[type="image"]
  126. var nodeType = (0, _jsxAstUtils.elementType)(node);
  127. if (nodeType === 'input') {
  128. var typePropValue = (0, _jsxAstUtils.getPropValue)((0, _jsxAstUtils.getProp)(node.attributes, 'type'));
  129. if (typePropValue !== 'image') {
  130. return;
  131. }
  132. }
  133. var ariaLabelProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-label');
  134. var arialLabelledByProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-labelledby');
  135. var hasLabel = ariaLabelHasValue(ariaLabelProp) || ariaLabelHasValue(arialLabelledByProp);
  136. if (hasLabel) {
  137. return;
  138. }
  139. var altProp = (0, _jsxAstUtils.getProp)(node.attributes, 'alt');
  140. if (altProp === undefined) {
  141. context.report({
  142. node,
  143. message: '<input> elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.'
  144. });
  145. return;
  146. }
  147. var altValue = (0, _jsxAstUtils.getPropValue)(altProp);
  148. var isNullValued = altProp.value === null; // <area alt />
  149. if (altValue && !isNullValued || altValue === '') {
  150. return;
  151. }
  152. context.report({
  153. node,
  154. message: '<input> elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.'
  155. });
  156. }
  157. };
  158. module.exports = {
  159. meta: {
  160. docs: {
  161. url: 'https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules/alt-text.md'
  162. },
  163. schema: [schema]
  164. },
  165. create: function create(context) {
  166. var _ref;
  167. var options = context.options[0] || {}; // Elements to validate for alt text.
  168. var elementOptions = options.elements || DEFAULT_ELEMENTS; // Get custom components for just the elements that will be tested.
  169. var customComponents = elementOptions.map(function (element) {
  170. return options[element];
  171. }).reduce(function (components, customComponentsForElement) {
  172. return components.concat(customComponentsForElement || []);
  173. }, []);
  174. var typesToValidate = new Set((_ref = []).concat.apply(_ref, [customComponents].concat((0, _toConsumableArray2["default"])(elementOptions))).map(function (type) {
  175. if (type === 'input[type="image"]') {
  176. return 'input';
  177. }
  178. return type;
  179. }));
  180. return {
  181. JSXOpeningElement: function JSXOpeningElement(node) {
  182. var nodeType = (0, _jsxAstUtils.elementType)(node);
  183. if (!typesToValidate.has(nodeType)) {
  184. return;
  185. }
  186. var DOMElement = nodeType;
  187. if (DOMElement === 'input') {
  188. DOMElement = 'input[type="image"]';
  189. } // Map nodeType to the DOM element if we are running this on a custom component.
  190. if (elementOptions.indexOf(DOMElement) === -1) {
  191. DOMElement = elementOptions.find(function (element) {
  192. var customComponentsForElement = options[element] || [];
  193. return customComponentsForElement.indexOf(nodeType) > -1;
  194. });
  195. }
  196. ruleByElement[DOMElement](context, node);
  197. }
  198. };
  199. }
  200. };