alt-text.js 8.5 KB

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