@jsxtools/eslint-plugin-jsx-a11y
Version:
Static AST checker for accessibility rules on JSX elements for flat ESLint Config.
192 lines (189 loc) • 6.8 kB
JavaScript
import { getProp, getPropValue, getLiteralPropValue } from '../util/module/jsx-ast-utils.js';
import { generateObjSchema, arraySchema } from '../util/schemas.js';
import getElementType from '../util/getElementType.js';
import hasAccessibleChild from '../util/hasAccessibleChild.js';
import isPresentationRole from '../util/isPresentationRole.js';
const DEFAULT_ELEMENTS = [
"img",
"object",
"area",
'input[type="image"]'
];
const schema = generateObjSchema({
elements: arraySchema,
img: arraySchema,
object: arraySchema,
area: arraySchema,
'input[type="image"]': arraySchema
});
const ariaLabelHasValue = (prop) => {
const value = getPropValue(prop);
if (value === void 0) {
return false;
}
if (typeof value === "string" && value.length === 0) {
return false;
}
return true;
};
const ruleByElement = {
img(context, node, nodeType) {
const altProp = getProp(node.attributes, "alt");
if (altProp === void 0) {
if (isPresentationRole(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;
}
const ariaLabelProp = getProp(node.attributes, "aria-label");
if (ariaLabelProp !== void 0) {
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;
}
const ariaLabelledbyProp = getProp(node.attributes, "aria-labelledby");
if (ariaLabelledbyProp !== void 0) {
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: `${nodeType} elements must have an alt prop, either with meaningful text, or an empty string for decorative images.`
});
return;
}
const altValue = getPropValue(altProp);
const isNullValued = altProp.value === null;
if (altValue && !isNullValued || altValue === "") {
return;
}
context.report({
node,
message: `Invalid alt value for ${nodeType}. Use alt="" for presentational images.`
});
},
object(context, node, unusedNodeType, elementType) {
const ariaLabelProp = getProp(node.attributes, "aria-label");
const arialLabelledByProp = getProp(node.attributes, "aria-labelledby");
const hasLabel = ariaLabelHasValue(ariaLabelProp) || ariaLabelHasValue(arialLabelledByProp);
const titleProp = getLiteralPropValue(getProp(node.attributes, "title"));
const hasTitleAttr = !!titleProp;
if (hasLabel || hasTitleAttr || hasAccessibleChild(node.parent, elementType)) {
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) {
const ariaLabelProp = getProp(node.attributes, "aria-label");
const arialLabelledByProp = getProp(node.attributes, "aria-labelledby");
const hasLabel = ariaLabelHasValue(ariaLabelProp) || ariaLabelHasValue(arialLabelledByProp);
if (hasLabel) {
return;
}
const altProp = getProp(node.attributes, "alt");
if (altProp === void 0) {
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;
}
const altValue = getPropValue(altProp);
const 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, nodeType) {
if (nodeType === "input") {
const typePropValue = getPropValue(getProp(node.attributes, "type"));
if (typePropValue !== "image") {
return;
}
}
const ariaLabelProp = getProp(node.attributes, "aria-label");
const arialLabelledByProp = getProp(node.attributes, "aria-labelledby");
const hasLabel = ariaLabelHasValue(ariaLabelProp) || ariaLabelHasValue(arialLabelledByProp);
if (hasLabel) {
return;
}
const altProp = getProp(node.attributes, "alt");
if (altProp === void 0) {
context.report({
node,
message: '<input> elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.'
});
return;
}
const altValue = getPropValue(altProp);
const isNullValued = altProp.value === null;
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.'
});
}
};
const ruleOfAltText = {
meta: {
docs: {
url: "https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/alt-text.md",
description: "Enforce all elements that require alternative text have meaningful information to relay back to end user."
},
schema: [schema]
},
create: (context) => {
const options = context.options[0] || {};
const elementOptions = options.elements || DEFAULT_ELEMENTS;
const customComponents = elementOptions.flatMap(
(element) => options[element]
);
const typesToValidate = new Set(
[].concat(
customComponents,
elementOptions
).map((type) => type === 'input[type="image"]' ? "input" : type)
);
const elementType = getElementType(context);
return {
JSXOpeningElement(node) {
const nodeType = elementType(node);
if (!typesToValidate.has(nodeType)) {
return;
}
let DOMElement = nodeType;
if (DOMElement === "input") {
DOMElement = 'input[type="image"]';
}
if (elementOptions.indexOf(DOMElement) === -1) {
DOMElement = elementOptions.find((element) => {
const customComponentsForElement = options[element] || [];
return customComponentsForElement.indexOf(nodeType) > -1;
});
}
ruleByElement[DOMElement](context, node, nodeType, elementType);
}
};
}
};
export { ruleOfAltText as default };