123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- "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; // <img alt />
- 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 <object> 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; // <area alt />
- 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: '<input> 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; // <area alt />
- if (altValue && !isNullValued || altValue === '') {
- return;
- }
- context.report({
- node,
- message: '<input> 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);
- }
- };
- }
- };
|