Skip to content

Commit a2f2c54

Browse files
authored
Merge pull request #651 from evcohen/jwyung-alt-text
[alt-text] allow aria-label or aria-labelledby to provide text alternative for img (#411)
2 parents fda3c49 + dd49060 commit a2f2c54

File tree

2 files changed

+66
-8
lines changed

2 files changed

+66
-8
lines changed

__tests__/src/rules/alt-text-test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ Use alt="" for presentational images.`,
2929
type: 'JSXOpeningElement',
3030
});
3131

32+
const ariaLabelValueError = 'The aria-label attribute must have a value. The alt attribute is preferred over aria-label for images.';
33+
const ariaLabelledbyValueError = 'The aria-labelledby attribute must have a value. The alt attribute is preferred over aria-labelledby for images.';
34+
3235
const preferAltError = () => ({
3336
message: 'Prefer alt="" over a presentational role. First rule of aria is to not use aria if it can be achieved via native HTML.',
3437
type: 'JSXOpeningElement',
@@ -83,6 +86,8 @@ ruleTester.run('alt-text', rule, {
8386
{ code: '<img alt={error ? "not working": "working"} />' },
8487
{ code: '<img alt={undefined ? "working": "not working"} />' },
8588
{ code: '<img alt={plugin.name + " Logo"} />' },
89+
{ code: '<img aria-label="foo" />' },
90+
{ code: '<img aria-labelledby="id1" />' },
8691

8792
// DEFAULT <object> TESTS
8893
{ code: '<object aria-label="foo" />' },
@@ -168,25 +173,41 @@ ruleTester.run('alt-text', rule, {
168173
{ code: '<img alt role="presentation" />;', errors: [altValueError('img')] },
169174
{ code: '<img role="presentation" />;', errors: [preferAltError()] },
170175
{ code: '<img role="none" />;', errors: [preferAltError()] },
176+
{ code: '<img aria-label={undefined} />', errors: [ariaLabelValueError] },
177+
{ code: '<img aria-labelledby={undefined} />', errors: [ariaLabelledbyValueError] },
178+
{ code: '<img aria-label="" />', errors: [ariaLabelValueError] },
179+
{ code: '<img aria-labelledby="" />', errors: [ariaLabelledbyValueError] },
171180

172181
// DEFAULT ELEMENT 'object' TESTS
173182
{ code: '<object />', errors: [objectError] },
174183
{ code: '<object><div aria-hidden /></object>', errors: [objectError] },
175184
{ code: '<object title={undefined} />', errors: [objectError] },
185+
{ code: '<object aria-label="" />', errors: [objectError] },
186+
{ code: '<object aria-labelledby="" />', errors: [objectError] },
187+
{ code: '<object aria-label={undefined} />', errors: [objectError] },
188+
{ code: '<object aria-labelledby={undefined} />', errors: [objectError] },
176189

177190
// DEFAULT ELEMENT 'area' TESTS
178191
{ code: '<area />', errors: [areaError] },
179192
{ code: '<area alt />', errors: [areaError] },
180193
{ code: '<area alt={undefined} />', errors: [areaError] },
181194
{ code: '<area src="xyz" />', errors: [areaError] },
182195
{ code: '<area {...this.props} />', errors: [areaError] },
196+
{ code: '<area aria-label="" />', errors: [areaError] },
197+
{ code: '<area aria-label={undefined} />', errors: [areaError] },
198+
{ code: '<area aria-labelledby="" />', errors: [areaError] },
199+
{ code: '<area aria-labelledby={undefined} />', errors: [areaError] },
183200

184201
// DEFAULT ELEMENT 'input type="image"' TESTS
185202
{ code: '<input type="image" />', errors: [inputImageError] },
186203
{ code: '<input type="image" alt />', errors: [inputImageError] },
187204
{ code: '<input type="image" alt={undefined} />', errors: [inputImageError] },
188205
{ code: '<input type="image">Foo</input>', errors: [inputImageError] },
189206
{ code: '<input type="image" {...this.props} />', errors: [inputImageError] },
207+
{ code: '<input type="image" aria-label="" />', errors: [inputImageError] },
208+
{ code: '<input type="image" aria-label={undefined} />', errors: [inputImageError] },
209+
{ code: '<input type="image" aria-labelledby="" />', errors: [inputImageError] },
210+
{ code: '<input type="image" aria-labelledby={undefined} />', errors: [inputImageError] },
190211

191212
// CUSTOM ELEMENT TESTS FOR ARRAY OPTION TESTS
192213
{

src/rules/alt-text.js

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,20 @@ const schema = generateObjSchema({
3232
'input[type="image"]': arraySchema,
3333
});
3434

35+
const ariaLabelHasValue = (prop) => {
36+
const value = getPropValue(prop);
37+
if (value === undefined) {
38+
return false;
39+
}
40+
if (typeof value === 'string' && value.length === 0) {
41+
return false;
42+
}
43+
return true;
44+
};
45+
3546
const ruleByElement = {
3647
img(context, node) {
3748
const nodeType = elementType(node);
38-
3949
const altProp = getProp(node.attributes, 'alt');
4050

4151
// Missing alt prop error.
@@ -47,6 +57,33 @@ const ruleByElement = {
4757
});
4858
return;
4959
}
60+
// Check for `aria-label` to provide text alternative
61+
// Don't create an error if the attribute is used correctly. But if it
62+
// isn't, suggest that the developer use `alt` instead.
63+
const ariaLabelProp = getProp(node.attributes, 'aria-label');
64+
if (ariaLabelProp !== undefined) {
65+
if (!ariaLabelHasValue(ariaLabelProp)) {
66+
context.report({
67+
node,
68+
message: 'The aria-label attribute must have a value. The alt attribute is preferred over aria-label for images.',
69+
});
70+
}
71+
return;
72+
}
73+
// Check for `aria-labelledby` to provide text alternative
74+
// Don't create an error if the attribute is used correctly. But if it
75+
// isn't, suggest that the developer use `alt` instead.
76+
const ariaLabelledbyProp = getProp(node.attributes, 'aria-labelledby');
77+
if (ariaLabelledbyProp !== undefined) {
78+
if (!ariaLabelHasValue(ariaLabelledbyProp)) {
79+
context.report({
80+
node,
81+
message: 'The aria-labelledby attribute must have a value. The alt attribute is preferred over aria-labelledby for images.',
82+
});
83+
}
84+
return;
85+
}
86+
5087
context.report({
5188
node,
5289
message: `${nodeType} elements must have an alt prop, either with meaningful text, or an empty string for decorative images.`,
@@ -72,7 +109,7 @@ const ruleByElement = {
72109
object(context, node) {
73110
const ariaLabelProp = getProp(node.attributes, 'aria-label');
74111
const arialLabelledByProp = getProp(node.attributes, 'aria-labelledby');
75-
const hasLabel = ariaLabelProp !== undefined || arialLabelledByProp !== undefined;
112+
const hasLabel = ariaLabelHasValue(ariaLabelProp) || ariaLabelHasValue(arialLabelledByProp);
76113
const titleProp = getLiteralPropValue(getProp(node.attributes, 'title'));
77114
const hasTitleAttr = !!titleProp;
78115

@@ -87,9 +124,9 @@ const ruleByElement = {
87124
},
88125

89126
area(context, node) {
90-
const ariaLabelPropValue = getPropValue(getProp(node.attributes, 'aria-label'));
91-
const arialLabelledByPropValue = getPropValue(getProp(node.attributes, 'aria-labelledby'));
92-
const hasLabel = ariaLabelPropValue !== undefined || arialLabelledByPropValue !== undefined;
127+
const ariaLabelProp = getProp(node.attributes, 'aria-label');
128+
const arialLabelledByProp = getProp(node.attributes, 'aria-labelledby');
129+
const hasLabel = ariaLabelHasValue(ariaLabelProp) || ariaLabelHasValue(arialLabelledByProp);
93130

94131
if (hasLabel) {
95132
return;
@@ -124,9 +161,9 @@ const ruleByElement = {
124161
const typePropValue = getPropValue(getProp(node.attributes, 'type'));
125162
if (typePropValue !== 'image') { return; }
126163
}
127-
const ariaLabelPropValue = getPropValue(getProp(node.attributes, 'aria-label'));
128-
const arialLabelledByPropValue = getPropValue(getProp(node.attributes, 'aria-labelledby'));
129-
const hasLabel = ariaLabelPropValue !== undefined || arialLabelledByPropValue !== undefined;
164+
const ariaLabelProp = getProp(node.attributes, 'aria-label');
165+
const arialLabelledByProp = getProp(node.attributes, 'aria-labelledby');
166+
const hasLabel = ariaLabelHasValue(ariaLabelProp) || ariaLabelHasValue(arialLabelledByProp);
130167

131168
if (hasLabel) {
132169
return;

0 commit comments

Comments
 (0)