Skip to content

Commit 3a7b15d

Browse files
mohammedzamakhanmgechev
authored andcommitted
feat(rule): mouse events should accompany key events (#759)
1 parent 6b21a9e commit 3a7b15d

File tree

3 files changed

+122
-0
lines changed

3 files changed

+122
-0
lines changed

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export { Rule as TemplateAccessibilityTableScopeRule } from './templateAccessibi
3939
export { Rule as TemplateNoDistractingElementsRule } from './templateNoDistractingElementsRule';
4040
export { Rule as TemplatesNoNegatedAsync } from './templatesNoNegatedAsyncRule';
4141
export { Rule as TemplateNoAutofocusRule } from './templateNoAutofocusRule';
42+
export { Rule as TemplateMouseEventsHaveKeyEventsRule } from './templateMouseEventsHaveKeyEventsRule';
4243
export { Rule as TrackByFunctionRule } from './trackByFunctionRule';
4344
export { Rule as UseHostPropertyDecoratorRule } from './useHostPropertyDecoratorRule';
4445
export { Rule as UseInputPropertyDecoratorRule } from './useInputPropertyDecoratorRule';
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { ElementAst } from '@angular/compiler';
2+
import { IRuleMetadata, RuleFailure, Rules } from 'tslint/lib';
3+
import { SourceFile } from 'typescript/lib/typescript';
4+
import { NgWalker } from './angular/ngWalker';
5+
import { BasicTemplateAstVisitor } from './angular/templates/basicTemplateAstVisitor';
6+
7+
export class Rule extends Rules.AbstractRule {
8+
static readonly metadata: IRuleMetadata = {
9+
description: 'Ensures that the Mouse Events mouseover and mouseout are accompanied with Key Events focus and blur',
10+
options: null,
11+
optionsDescription: 'Not configurable.',
12+
rationale: 'Keyboard is important for users with physical disabilities who cannot use mouse.',
13+
ruleName: 'template-mouse-events-have-key-events',
14+
type: 'functionality',
15+
typescriptOnly: true
16+
};
17+
18+
static readonly FAILURE_STRING_MOUSE_OVER = 'mouseover must be accompanied by focus event for accessibility';
19+
static readonly FAILURE_STRING_MOUSE_OUT = 'mouseout must be accompanied by blur event for accessibility';
20+
21+
apply(sourceFile: SourceFile): RuleFailure[] {
22+
return this.applyWithWalker(
23+
new NgWalker(sourceFile, this.getOptions(), {
24+
templateVisitorCtrl: TemplateMouseEventsHaveKeyEventsVisitor
25+
})
26+
);
27+
}
28+
}
29+
30+
class TemplateMouseEventsHaveKeyEventsVisitor extends BasicTemplateAstVisitor {
31+
visitElement(el: ElementAst, context: any) {
32+
this.validateElement(el);
33+
super.visitElement(el, context);
34+
}
35+
36+
private validateElement(el: ElementAst): void {
37+
const hasMouseOver = el.outputs.some(output => output.name === 'mouseover');
38+
const hasMouseOut = el.outputs.some(output => output.name === 'mouseout');
39+
const hasFocus = el.outputs.some(output => output.name === 'focus');
40+
const hasBlur = el.outputs.some(output => output.name === 'blur');
41+
42+
if (!hasMouseOver && !hasMouseOut) {
43+
return;
44+
}
45+
46+
const {
47+
sourceSpan: {
48+
end: { offset: endOffset },
49+
start: { offset: startOffset }
50+
}
51+
} = el;
52+
53+
if (hasMouseOver && !hasFocus) {
54+
this.addFailureFromStartToEnd(startOffset, endOffset, Rule.FAILURE_STRING_MOUSE_OVER);
55+
}
56+
57+
if (hasMouseOut && !hasBlur) {
58+
this.addFailureFromStartToEnd(startOffset, endOffset, Rule.FAILURE_STRING_MOUSE_OUT);
59+
}
60+
}
61+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Rule } from '../src/templateMouseEventsHaveKeyEventsRule';
2+
import { assertAnnotated, assertSuccess } from './testHelper';
3+
4+
const {
5+
FAILURE_STRING_MOUSE_OUT,
6+
FAILURE_STRING_MOUSE_OVER,
7+
metadata: { ruleName }
8+
} = Rule;
9+
10+
describe(ruleName, () => {
11+
describe('failure', () => {
12+
it('should fail when mouseover is not accompanied with focus', () => {
13+
const source = `
14+
@Component({
15+
template: \`
16+
<div (mouseover)="onMouseOver()"></div>
17+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
18+
\`
19+
})
20+
class Bar {}
21+
`;
22+
assertAnnotated({
23+
message: FAILURE_STRING_MOUSE_OVER,
24+
ruleName,
25+
source
26+
});
27+
});
28+
29+
it('should fail when mouseout is not accompanied with blur', () => {
30+
const source = `
31+
@Component({
32+
template: \`
33+
<div (mouseout)="onMouseOut()"></div>
34+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
35+
\`
36+
})
37+
class Bar {}
38+
`;
39+
assertAnnotated({
40+
message: FAILURE_STRING_MOUSE_OUT,
41+
ruleName,
42+
source
43+
});
44+
});
45+
});
46+
47+
describe('success', () => {
48+
it('should work find when mouse events are associated with key events', () => {
49+
const source = `
50+
@Component({
51+
template: \`
52+
<div (mouseover)="onMouseOver()" (focus)="onMouseOver()" (mouseout)="onMouseOut()" (blur)="onMouseOut()"></div>
53+
\`
54+
})
55+
class Bar {}
56+
`;
57+
assertSuccess(ruleName, source);
58+
});
59+
});
60+
});

0 commit comments

Comments
 (0)