alt-text.js 8.5 KB

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