alt-text
Ast Rule: html element
alt-text
const TYPES = {
string: getPropStringValue,
sequence: getPropSequenceValue,
array: getPropArrayValue,
object: getPropObjectValue,
object_element: getPropObjectValue,
functiondefinition: () => {},
};
function getTag(node) {
if (node && node.tag) {
return node.tag.value;
}
}
function getProp(attributes = [], prop = "") {
if (!prop) return;
return attributes.find((attribute) => {
if (attribute && attribute.name && attribute.name.value) {
return attribute.name.value === prop;
}
});
}
function getPropStringValue(value) {
if (value.value === "true") {
return true;
}
if (value.value === "false") {
return false;
}
return value.value.replace(/^\"/g, "").replace(/\"$/g, "");
}
function getPropArrayValue(value) {
const array = [];
const arrayElements = value.elements;
if (arrayElements.length) {
for (const element of arrayElements) {
array.push(TYPES[element.astType](element));
}
}
return array;
}
function getPropObjectValue(value) {
const object = {};
const objectElements = value.elements;
if (objectElements.length) {
for (const element of objectElements) {
object[element.name.value] = TYPES[element.value.astType](element.value);
}
}
return object;
}
function getPropSequenceValue(value) {
if (value.elements.length) {
const element = value.elements[0];
if (element) {
return TYPES[element.astType](element);
}
}
}
function extractValue(attribute, extractor) {
if (attribute) {
// Null valued attributes imply truthiness.
// For example: <div aria-hidden />
if (attribute.value === null) return true;
return extractor(attribute.value);
}
}
function getValue(value) {
return TYPES[value.astType](value);
}
function getPropValue(attribute) {
return extractValue(attribute, getValue);
}
function debugObject(object) {
for (const property in object) {
console.log(`${property}: ${JSON.stringify(object[property])}`);
}
}
function propHasValue(attribute) {
const value = getPropValue(attribute);
if (typeof value !== "boolean") {
return !!value;
}
return false;
}
const presentationRoles = new Set(["presentation", "none"]);
function isPresentationRole(attributes) {
const prop = getProp(attributes, "role");
if (prop) {
return presentationRoles.has(getPropValue(prop));
}
return false;
}
function imgRule(node) {
const altProp = getProp(node.attributes, "alt");
// Missing alt prop error.
if (altProp === undefined) {
if (isPresentationRole(node.attributes)) {
const prop = getProp(node.attributes, "role");
const error = buildError(
node.start.line,
node.start.col,
node.end.line,
node.end.col,
`Prefer alt="" over a presentational role. First rule of aria is to not use aria if it can be achieved via native HTML.`,
"INFO",
"BEST_PRACTICES"
);
const editUpdate = buildEditUpdate(
prop.start.line,
prop.start.col,
prop.end.line,
prop.end.col,
`alt=""`
);
const fix = buildFix("replace the `role` attribute with `alt`", [
editUpdate,
]);
addError(error.addFix(fix));
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.
const ariaLabelProp = getProp(node.attributes, "aria-label");
if (ariaLabelProp !== undefined) {
if (!getPropValue(ariaLabelProp)) {
const error = buildError(
node.start.line,
node.start.col,
node.end.line,
node.end.col,
"The aria-label attribute must have a value. The alt attribute is preferred over aria-label for images.",
"INFO",
"BEST_PRACTICES"
);
addError(error);
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.
const ariaLabelledbyProp = getProp(node.attributes, "aria-labelledby");
if (ariaLabelledbyProp !== undefined) {
if (!getPropValue(ariaLabelledbyProp)) {
context.report({
node,
message: "The aria-labelledby attribute must have a value. The alt attribute is preferred over aria-labelledby for images.",
});
}
const error = buildError(
node.start.line,
node.start.col,
node.end.line,
node.end.col,
"The aria-labelledby attribute must have a value. The alt attribute is preferred over aria-labelledby for images.",
"INFO",
"BEST_PRACTICES"
);
addError(error);
return;
}
const error = buildError(
node.start.line,
node.start.col,
node.end.line,
node.end.col,
`${node.tag.value} elements must have an alt prop, either with meaningful text, or an empty string for decorative images.`,
"INFO",
"BEST_PRACTICES"
);
const editAdd = buildEditAdd(
node.tag.start.line,
node.tag.start.col + node.tag.value.length,
` alt=""`
);
const fix = buildFix("add the `alt` attribute", [editAdd]);
addError(error.addFix(fix));
return;
}
// Check if alt prop value is undefined.
if (propHasValue(altProp)) {
return;
}
const error = buildError(
node.start.line,
node.start.col,
node.end.line,
node.end.col,
`Invalid alt value for ${getTag(
node
)}. Use alt="" for presentational images.`,
"INFO",
"BEST_PRACTICES"
);
addError(error);
}
function objectRule(node) {
const ariaLabelProp = getProp(node.attributes, "aria-label");
const arialLabelledByProp = getProp(node.attributes, "aria-labelledby");
const titleProp = getProp(node.attributes, "title");
const hasLabel =
propHasValue(ariaLabelProp) || propHasValue(arialLabelledByProp);
const hasTitle = propHasValue(titleProp);
// TODO: check for accessible child
// https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/rules/alt-text.js#L115
if (hasLabel || hasTitle) {
return;
}
const error = buildError(
node.start.line,
node.start.col,
node.end.line,
node.end.col,
"Embedded <object> elements must have alternative text by providing inner text, aria-label or aria-labelledby props.",
"INFO",
"BEST_PRACTICES"
);
addError(error);
}
function areaRule(node) {
const ariaLabelProp = getProp(node.attributes, "aria-label");
const arialLabelledByProp = getProp(node.attributes, "aria-labelledby");
const hasLabel =
propHasValue(ariaLabelProp) || propHasValue(arialLabelledByProp);
if (hasLabel) {
return;
}
const altProp = getProp(node.attributes, "alt");
if (altProp === undefined) {
const error = buildError(
node.start.line,
node.start.col,
node.end.line,
node.end.col,
"Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.",
"INFO",
"BEST_PRACTICES"
);
addError(error);
return;
}
// Check if alt prop value is undefined.
if (propHasValue(altProp)) {
return;
}
const error = buildError(
node.start.line,
node.start.col,
node.end.line,
node.end.col,
"Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.",
"INFO",
"BEST_PRACTICES"
);
addError(error);
}
function inputImageRule(node) {
// Only test input[type="image"]
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 =
propHasValue(ariaLabelProp) || propHasValue(arialLabelledByProp);
if (hasLabel) {
return;
}
const altProp = getProp(node.attributes, "alt");
if (altProp === undefined) {
const error = buildError(
node.start.line,
node.start.col,
node.end.line,
node.end.col,
'<input> elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.',
"INFO",
"BEST_PRACTICES"
);
addError(error);
return;
}
// Check if alt prop value is undefined.
if (propHasValue(altProp)) {
return;
}
const error = buildError(
node.start.line,
node.start.col,
node.end.line,
node.end.col,
'<input> elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.',
"INFO",
"BEST_PRACTICES"
);
addError(error);
}
function visit(node, filename, code) {
switch (getTag(node)) {
case "img":
imgRule(node);
break;
case "object":
objectRule(node);
break;
case "area":
areaRule(node);
break;
case "input":
inputImageRule(node);
break;
default:
break;
}
}
my-comp.jsx
Expected test result: no error
elements with atl prop