Skip to content

Commit

Permalink
Merge pull request #651 from evcohen/jwyung-alt-text
Browse files Browse the repository at this point in the history
[alt-text] allow aria-label or aria-labelledby to provide text alternative for img (#411)
  • Loading branch information
jessebeach committed Nov 28, 2019
2 parents fda3c49 + dd49060 commit a2f2c54
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 8 deletions.
21 changes: 21 additions & 0 deletions __tests__/src/rules/alt-text-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ Use alt="" for presentational images.`,
type: 'JSXOpeningElement',
});

const ariaLabelValueError = 'The aria-label attribute must have a value. The alt attribute is preferred over aria-label for images.';
const ariaLabelledbyValueError = 'The aria-labelledby attribute must have a value. The alt attribute is preferred over aria-labelledby for images.';

const preferAltError = () => ({
message: 'Prefer alt="" over a presentational role. First rule of aria is to not use aria if it can be achieved via native HTML.',
type: 'JSXOpeningElement',
Expand Down Expand Up @@ -83,6 +86,8 @@ ruleTester.run('alt-text', rule, {
{ code: '<img alt={error ? "not working": "working"} />' },
{ code: '<img alt={undefined ? "working": "not working"} />' },
{ code: '<img alt={plugin.name + " Logo"} />' },
{ code: '<img aria-label="foo" />' },
{ code: '<img aria-labelledby="id1" />' },

// DEFAULT <object> TESTS
{ code: '<object aria-label="foo" />' },
Expand Down Expand Up @@ -168,25 +173,41 @@ ruleTester.run('alt-text', rule, {
{ code: '<img alt role="presentation" />;', errors: [altValueError('img')] },
{ code: '<img role="presentation" />;', errors: [preferAltError()] },
{ code: '<img role="none" />;', errors: [preferAltError()] },
{ code: '<img aria-label={undefined} />', errors: [ariaLabelValueError] },
{ code: '<img aria-labelledby={undefined} />', errors: [ariaLabelledbyValueError] },
{ code: '<img aria-label="" />', errors: [ariaLabelValueError] },
{ code: '<img aria-labelledby="" />', errors: [ariaLabelledbyValueError] },

// DEFAULT ELEMENT 'object' TESTS
{ code: '<object />', errors: [objectError] },
{ code: '<object><div aria-hidden /></object>', errors: [objectError] },
{ code: '<object title={undefined} />', errors: [objectError] },
{ code: '<object aria-label="" />', errors: [objectError] },
{ code: '<object aria-labelledby="" />', errors: [objectError] },
{ code: '<object aria-label={undefined} />', errors: [objectError] },
{ code: '<object aria-labelledby={undefined} />', errors: [objectError] },

// DEFAULT ELEMENT 'area' TESTS
{ code: '<area />', errors: [areaError] },
{ code: '<area alt />', errors: [areaError] },
{ code: '<area alt={undefined} />', errors: [areaError] },
{ code: '<area src="xyz" />', errors: [areaError] },
{ code: '<area {...this.props} />', errors: [areaError] },
{ code: '<area aria-label="" />', errors: [areaError] },
{ code: '<area aria-label={undefined} />', errors: [areaError] },
{ code: '<area aria-labelledby="" />', errors: [areaError] },
{ code: '<area aria-labelledby={undefined} />', errors: [areaError] },

// DEFAULT ELEMENT 'input type="image"' TESTS
{ code: '<input type="image" />', errors: [inputImageError] },
{ code: '<input type="image" alt />', errors: [inputImageError] },
{ code: '<input type="image" alt={undefined} />', errors: [inputImageError] },
{ code: '<input type="image">Foo</input>', errors: [inputImageError] },
{ code: '<input type="image" {...this.props} />', errors: [inputImageError] },
{ code: '<input type="image" aria-label="" />', errors: [inputImageError] },
{ code: '<input type="image" aria-label={undefined} />', errors: [inputImageError] },
{ code: '<input type="image" aria-labelledby="" />', errors: [inputImageError] },
{ code: '<input type="image" aria-labelledby={undefined} />', errors: [inputImageError] },

// CUSTOM ELEMENT TESTS FOR ARRAY OPTION TESTS
{
Expand Down
53 changes: 45 additions & 8 deletions src/rules/alt-text.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,20 @@ const schema = generateObjSchema({
'input[type="image"]': arraySchema,
});

const ariaLabelHasValue = (prop) => {
const value = getPropValue(prop);
if (value === undefined) {
return false;
}
if (typeof value === 'string' && value.length === 0) {
return false;
}
return true;
};

const ruleByElement = {
img(context, node) {
const nodeType = elementType(node);

const altProp = getProp(node.attributes, 'alt');

// Missing alt prop error.
Expand All @@ -47,6 +57,33 @@ const ruleByElement = {
});
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 (!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.
const ariaLabelledbyProp = 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: `${nodeType} elements must have an alt prop, either with meaningful text, or an empty string for decorative images.`,
Expand All @@ -72,7 +109,7 @@ const ruleByElement = {
object(context, node) {
const ariaLabelProp = getProp(node.attributes, 'aria-label');
const arialLabelledByProp = getProp(node.attributes, 'aria-labelledby');
const hasLabel = ariaLabelProp !== undefined || arialLabelledByProp !== undefined;
const hasLabel = ariaLabelHasValue(ariaLabelProp) || ariaLabelHasValue(arialLabelledByProp);
const titleProp = getLiteralPropValue(getProp(node.attributes, 'title'));
const hasTitleAttr = !!titleProp;

Expand All @@ -87,9 +124,9 @@ const ruleByElement = {
},

area(context, node) {
const ariaLabelPropValue = getPropValue(getProp(node.attributes, 'aria-label'));
const arialLabelledByPropValue = getPropValue(getProp(node.attributes, 'aria-labelledby'));
const hasLabel = ariaLabelPropValue !== undefined || arialLabelledByPropValue !== undefined;
const ariaLabelProp = getProp(node.attributes, 'aria-label');
const arialLabelledByProp = getProp(node.attributes, 'aria-labelledby');
const hasLabel = ariaLabelHasValue(ariaLabelProp) || ariaLabelHasValue(arialLabelledByProp);

if (hasLabel) {
return;
Expand Down Expand Up @@ -124,9 +161,9 @@ const ruleByElement = {
const typePropValue = getPropValue(getProp(node.attributes, 'type'));
if (typePropValue !== 'image') { return; }
}
const ariaLabelPropValue = getPropValue(getProp(node.attributes, 'aria-label'));
const arialLabelledByPropValue = getPropValue(getProp(node.attributes, 'aria-labelledby'));
const hasLabel = ariaLabelPropValue !== undefined || arialLabelledByPropValue !== undefined;
const ariaLabelProp = getProp(node.attributes, 'aria-label');
const arialLabelledByProp = getProp(node.attributes, 'aria-labelledby');
const hasLabel = ariaLabelHasValue(ariaLabelProp) || ariaLabelHasValue(arialLabelledByProp);

if (hasLabel) {
return;
Expand Down

0 comments on commit a2f2c54

Please sign in to comment.