"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); var _jsxAstUtils = require("jsx-ast-utils"); var _schemas = require("../util/schemas"); var _hasAccessibleChild = _interopRequireDefault(require("../util/hasAccessibleChild")); var _isPresentationRole = _interopRequireDefault(require("../util/isPresentationRole")); /** * @fileoverview Enforce all elements that require alternative text have it. * @author Ethan Cohen */ // ---------------------------------------------------------------------------- // Rule Definition // ---------------------------------------------------------------------------- var DEFAULT_ELEMENTS = ['img', 'object', 'area', 'input[type="image"]']; var schema = (0, _schemas.generateObjSchema)({ elements: _schemas.arraySchema, img: _schemas.arraySchema, object: _schemas.arraySchema, area: _schemas.arraySchema, 'input[type="image"]': _schemas.arraySchema }); var ariaLabelHasValue = function ariaLabelHasValue(prop) { var value = (0, _jsxAstUtils.getPropValue)(prop); if (value === undefined) { return false; } if (typeof value === 'string' && value.length === 0) { return false; } return true; }; var ruleByElement = { img(context, node) { var nodeType = (0, _jsxAstUtils.elementType)(node); var altProp = (0, _jsxAstUtils.getProp)(node.attributes, 'alt'); // Missing alt prop error. if (altProp === undefined) { if ((0, _isPresentationRole["default"])(nodeType, node.attributes)) { context.report({ node, message: 'Prefer alt="" over a presentational role. First rule of aria is to not use aria if it can be achieved via native HTML.' }); return; } // Check for `aria-label` to provide text alternative // Don't create an error if the attribute is used correctly. But if it // isn't, suggest that the developer use `alt` instead. var ariaLabelProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-label'); if (ariaLabelProp !== undefined) { if (!ariaLabelHasValue(ariaLabelProp)) { context.report({ node, message: 'The aria-label attribute must have a value. The alt attribute is preferred over aria-label for images.' }); } return; } // Check for `aria-labelledby` to provide text alternative // Don't create an error if the attribute is used correctly. But if it // isn't, suggest that the developer use `alt` instead. var ariaLabelledbyProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-labelledby'); if (ariaLabelledbyProp !== undefined) { if (!ariaLabelHasValue(ariaLabelledbyProp)) { context.report({ node, message: 'The aria-labelledby attribute must have a value. The alt attribute is preferred over aria-labelledby for images.' }); } return; } context.report({ node, message: "".concat(nodeType, " elements must have an alt prop, either with meaningful text, or an empty string for decorative images.") }); return; } // Check if alt prop is undefined. var altValue = (0, _jsxAstUtils.getPropValue)(altProp); var isNullValued = altProp.value === null; // if (altValue && !isNullValued || altValue === '') { return; } // Undefined alt prop error. context.report({ node, message: "Invalid alt value for ".concat(nodeType, ". Use alt=\"\" for presentational images.") }); }, object(context, node) { var ariaLabelProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-label'); var arialLabelledByProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-labelledby'); var hasLabel = ariaLabelHasValue(ariaLabelProp) || ariaLabelHasValue(arialLabelledByProp); var titleProp = (0, _jsxAstUtils.getLiteralPropValue)((0, _jsxAstUtils.getProp)(node.attributes, 'title')); var hasTitleAttr = !!titleProp; if (hasLabel || hasTitleAttr || (0, _hasAccessibleChild["default"])(node.parent)) { return; } context.report({ node, message: 'Embedded elements must have alternative text by providing inner text, aria-label or aria-labelledby props.' }); }, area(context, node) { var ariaLabelProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-label'); var arialLabelledByProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-labelledby'); var hasLabel = ariaLabelHasValue(ariaLabelProp) || ariaLabelHasValue(arialLabelledByProp); if (hasLabel) { return; } var altProp = (0, _jsxAstUtils.getProp)(node.attributes, 'alt'); if (altProp === undefined) { context.report({ node, message: 'Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.' }); return; } var altValue = (0, _jsxAstUtils.getPropValue)(altProp); var isNullValued = altProp.value === null; // if (altValue && !isNullValued || altValue === '') { return; } context.report({ node, message: 'Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.' }); }, 'input[type="image"]': function inputImage(context, node) { // Only test input[type="image"] var nodeType = (0, _jsxAstUtils.elementType)(node); if (nodeType === 'input') { var typePropValue = (0, _jsxAstUtils.getPropValue)((0, _jsxAstUtils.getProp)(node.attributes, 'type')); if (typePropValue !== 'image') { return; } } var ariaLabelProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-label'); var arialLabelledByProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-labelledby'); var hasLabel = ariaLabelHasValue(ariaLabelProp) || ariaLabelHasValue(arialLabelledByProp); if (hasLabel) { return; } var altProp = (0, _jsxAstUtils.getProp)(node.attributes, 'alt'); if (altProp === undefined) { context.report({ node, message: ' elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.' }); return; } var altValue = (0, _jsxAstUtils.getPropValue)(altProp); var isNullValued = altProp.value === null; // if (altValue && !isNullValued || altValue === '') { return; } context.report({ node, message: ' elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.' }); } }; module.exports = { meta: { docs: { url: 'https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules/alt-text.md' }, schema: [schema] }, create: function create(context) { var _ref; var options = context.options[0] || {}; // Elements to validate for alt text. var elementOptions = options.elements || DEFAULT_ELEMENTS; // Get custom components for just the elements that will be tested. var customComponents = elementOptions.map(function (element) { return options[element]; }).reduce(function (components, customComponentsForElement) { return components.concat(customComponentsForElement || []); }, []); var typesToValidate = new Set((_ref = []).concat.apply(_ref, [customComponents].concat((0, _toConsumableArray2["default"])(elementOptions))).map(function (type) { if (type === 'input[type="image"]') { return 'input'; } return type; })); return { JSXOpeningElement: function JSXOpeningElement(node) { var nodeType = (0, _jsxAstUtils.elementType)(node); if (!typesToValidate.has(nodeType)) { return; } var DOMElement = nodeType; if (DOMElement === 'input') { DOMElement = 'input[type="image"]'; } // Map nodeType to the DOM element if we are running this on a custom component. if (elementOptions.indexOf(DOMElement) === -1) { DOMElement = elementOptions.find(function (element) { var customComponentsForElement = options[element] || []; return customComponentsForElement.indexOf(nodeType) > -1; }); } ruleByElement[DOMElement](context, node); } }; } };