210 lines
7.0 KiB
JavaScript
210 lines
7.0 KiB
JavaScript
|
"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 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;
|
||
|
}
|
||
|
|
||
|
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 = ariaLabelProp !== undefined || arialLabelledByProp !== undefined;
|
||
|
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 ariaLabelPropValue = (0, _jsxAstUtils.getPropValue)((0, _jsxAstUtils.getProp)(node.attributes, 'aria-label'));
|
||
|
var arialLabelledByPropValue = (0, _jsxAstUtils.getPropValue)((0, _jsxAstUtils.getProp)(node.attributes, 'aria-labelledby'));
|
||
|
var hasLabel = ariaLabelPropValue !== undefined || arialLabelledByPropValue !== undefined;
|
||
|
|
||
|
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 ariaLabelPropValue = (0, _jsxAstUtils.getPropValue)((0, _jsxAstUtils.getProp)(node.attributes, 'aria-label'));
|
||
|
var arialLabelledByPropValue = (0, _jsxAstUtils.getPropValue)((0, _jsxAstUtils.getProp)(node.attributes, 'aria-labelledby'));
|
||
|
var hasLabel = ariaLabelPropValue !== undefined || arialLabelledByPropValue !== undefined;
|
||
|
|
||
|
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);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
};
|