Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 0815ec5

Browse filesBrowse files
mohammedzamakhanmgechev
authored andcommitted
feat(rule): accessibility rule for alt text (#741)
1 parent 762f67f commit 0815ec5
Copy full SHA for 0815ec5

3 files changed

+242Lines changed: 242 additions & 0 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎src/index.ts‎

Copy file name to clipboardExpand all lines: src/index.ts
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export { Rule as TemplateAccessibilityTabindexNoPositiveRule } from './templateA
3232
export { Rule as TemplateAccessibilityLabelForVisitor } from './templateAccessibilityLabelForRule';
3333
export { Rule as TemplateAccessibilityValidAriaRule } from './templateAccessibilityValidAriaRule';
3434
export { Rule as TemplatesAccessibilityAnchorContentRule } from './templateAccessibilityAnchorContentRule';
35+
export { Rule as TemplateAccessibilityAltTextRule } from './templateAccessibilityAltTextRule';
3536
export { Rule as TemplatesNoNegatedAsync } from './templatesNoNegatedAsyncRule';
3637
export { Rule as TemplateNoAutofocusRule } from './templateNoAutofocusRule';
3738
export { Rule as TrackByFunctionRule } from './trackByFunctionRule';
Collapse file
+102Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { ElementAst, AttrAst, BoundElementPropertyAst, TextAst } from '@angular/compiler';
2+
import { sprintf } from 'sprintf-js';
3+
import { IRuleMetadata, RuleFailure, Rules } from 'tslint/lib';
4+
import { SourceFile } from 'typescript/lib/typescript';
5+
import { NgWalker } from './angular/ngWalker';
6+
import { BasicTemplateAstVisitor } from './angular/templates/basicTemplateAstVisitor';
7+
8+
export class Rule extends Rules.AbstractRule {
9+
static readonly metadata: IRuleMetadata = {
10+
description: 'Enforces alternate text for elements which require the alt, aria-label, aria-labelledby attributes',
11+
options: null,
12+
optionsDescription: 'Not configurable.',
13+
rationale: 'Alternate text lets screen readers provide more information to end users.',
14+
ruleName: 'template-accessibility-alt-text',
15+
type: 'functionality',
16+
typescriptOnly: true
17+
};
18+
19+
static readonly FAILURE_STRING = '%s element must have a text alternative.';
20+
static readonly DEFAULT_ELEMENTS = ['img', 'object', 'area', 'input[type="image"]'];
21+
22+
apply(sourceFile: SourceFile): RuleFailure[] {
23+
return this.applyWithWalker(
24+
new NgWalker(sourceFile, this.getOptions(), {
25+
templateVisitorCtrl: TemplateAccessibilityAltTextVisitor
26+
})
27+
);
28+
}
29+
}
30+
31+
export const getFailureMessage = (name: string): string => {
32+
return sprintf(Rule.FAILURE_STRING, name);
33+
};
34+
35+
class TemplateAccessibilityAltTextVisitor extends BasicTemplateAstVisitor {
36+
visitElement(ast: ElementAst, context: any) {
37+
this.validateElement(ast);
38+
super.visitElement(ast, context);
39+
}
40+
41+
validateElement(element: ElementAst) {
42+
const typesToValidate = Rule.DEFAULT_ELEMENTS.map(type => {
43+
if (type === 'input[type="image"]') {
44+
return 'input';
45+
}
46+
return type;
47+
});
48+
if (typesToValidate.indexOf(element.name) === -1) {
49+
return;
50+
}
51+
52+
const isValid = this[element.name](element);
53+
if (isValid) {
54+
return;
55+
}
56+
const {
57+
sourceSpan: {
58+
end: { offset: endOffset },
59+
start: { offset: startOffset }
60+
}
61+
} = element;
62+
this.addFailureFromStartToEnd(startOffset, endOffset, getFailureMessage(element.name));
63+
}
64+
65+
img(element: ElementAst) {
66+
const hasAltAttr = element.attrs.some(attr => attr.name === 'alt');
67+
const hasAltInput = element.inputs.some(input => input.name === 'alt');
68+
return hasAltAttr || hasAltInput;
69+
}
70+
71+
object(element: ElementAst) {
72+
let elementHasText: string = '';
73+
const hasLabelAttr = element.attrs.some(attr => attr.name === 'aria-label' || attr.name === 'aria-labelledby');
74+
const hasLabelInput = element.inputs.some(input => input.name === 'aria-label' || input.name === 'aria-labelledby');
75+
const hasTitleAttr = element.attrs.some(attr => attr.name === 'title');
76+
const hasTitleInput = element.inputs.some(input => input.name === 'title');
77+
if (element.children.length) {
78+
elementHasText = (<TextAst>element.children[0]).value;
79+
}
80+
return hasLabelAttr || hasLabelInput || hasTitleAttr || hasTitleInput || elementHasText;
81+
}
82+
83+
area(element: ElementAst) {
84+
const hasLabelAttr = element.attrs.some(attr => attr.name === 'aria-label' || attr.name === 'aria-labelledby');
85+
const hasLabelInput = element.inputs.some(input => input.name === 'aria-label' || input.name === 'aria-labelledby');
86+
const hasAltAttr = element.attrs.some(attr => attr.name === 'alt');
87+
const hasAltInput = element.inputs.some(input => input.name === 'alt');
88+
console.log(element);
89+
return hasAltAttr || hasAltInput || hasLabelAttr || hasLabelInput;
90+
}
91+
92+
input(element: ElementAst) {
93+
const attrType: AttrAst = element.attrs.find(attr => attr.name === 'type') || <AttrAst>{};
94+
const inputType: BoundElementPropertyAst = element.inputs.find(input => input.name === 'type') || <BoundElementPropertyAst>{};
95+
const type = attrType.value || inputType.value;
96+
if (type !== 'image') {
97+
return true;
98+
}
99+
100+
return this.area(element);
101+
}
102+
}
Collapse file
+139Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { getFailureMessage, Rule } from '../src/templateAccessibilityAltTextRule';
2+
import { assertAnnotated, assertSuccess } from './testHelper';
3+
4+
const {
5+
metadata: { ruleName }
6+
} = Rule;
7+
8+
describe(ruleName, () => {
9+
describe('failure', () => {
10+
it('should fail image does not have alt text', () => {
11+
const source = `
12+
@Component({
13+
template: \`
14+
<img src="foo">
15+
~~~~~~~~~~~~~~~
16+
\`
17+
})
18+
class Bar {}
19+
`;
20+
assertAnnotated({
21+
message: getFailureMessage('img'),
22+
ruleName,
23+
source
24+
});
25+
});
26+
27+
it('should fail when object does not have alt text or labels', () => {
28+
const source = `
29+
@Component({
30+
template: \`
31+
<object></object>
32+
~~~~~~~~
33+
\`
34+
})
35+
class Bar {}
36+
`;
37+
assertAnnotated({
38+
message: getFailureMessage('object'),
39+
ruleName,
40+
source
41+
});
42+
});
43+
44+
it('should fail when area does not have alt or label text', () => {
45+
const source = `
46+
@Component({
47+
template: \`
48+
<area></area>
49+
~~~~~~
50+
\`
51+
})
52+
class Bar {}
53+
`;
54+
assertAnnotated({
55+
message: getFailureMessage('area'),
56+
ruleName,
57+
source
58+
});
59+
});
60+
61+
it('should fail when input element with type image does not have alt or text image', () => {
62+
const source = `
63+
@Component({
64+
template: \`
65+
<input type="image"></input>
66+
~~~~~~~~~~~~~~~~~~~~
67+
\`
68+
})
69+
class Bar {}
70+
`;
71+
assertAnnotated({
72+
message: getFailureMessage('input'),
73+
ruleName,
74+
source
75+
});
76+
});
77+
});
78+
79+
describe('success', () => {
80+
it('should work with img with alternative text', () => {
81+
const source = `
82+
@Component({
83+
template: \`
84+
<img src="foo" alt="Foo eating a sandwich.">
85+
<img src="foo" [attr.alt]="altText">
86+
<img src="foo" [attr.alt]="'Alt Text'">
87+
<img src="foo" alt="">
88+
\`
89+
})
90+
class Bar {}
91+
`;
92+
assertSuccess(ruleName, source);
93+
});
94+
95+
it('should work with object having label, title or meaningful description', () => {
96+
const source = `
97+
@Component({
98+
template: \`
99+
<object aria-label="foo">
100+
<object aria-labelledby="id1">
101+
<object>Meaningful description</object>
102+
<object title="An object">
103+
\`
104+
})
105+
class Bar {}
106+
`;
107+
assertSuccess(ruleName, source);
108+
});
109+
110+
it('should work with area having label or alternate text', () => {
111+
const source = `
112+
@Component({
113+
template: \`
114+
<area aria-label="foo"></area>
115+
<area aria-labelledby="id1"></area>
116+
<area alt="This is descriptive!"></area>
117+
\`
118+
})
119+
class Bar {}
120+
`;
121+
assertSuccess(ruleName, source);
122+
});
123+
124+
it('should work with input type image having alterate text and labels', () => {
125+
const source = `
126+
@Component({
127+
template: \`
128+
<input type="text">
129+
<input type="image" alt="This is descriptive!">
130+
<input type="image" aria-label="foo">
131+
<input type="image" aria-labelledby="id1">
132+
\`
133+
})
134+
class Bar {}
135+
`;
136+
assertSuccess(ruleName, source);
137+
});
138+
});
139+
});

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.