From 3ca10e76eb83754d568b93381fd1032280591ec7 Mon Sep 17 00:00:00 2001 From: Zama Khan Mohammed Date: Mon, 11 Feb 2019 16:40:12 -0600 Subject: [PATCH] feat(rule): mouse events should accompany key events --- src/index.ts | 1 + src/templateMouseEventsHaveKeyEventsRule.ts | 61 +++++++++++++++++++ ...mplateMouseEventsHaveKeyEventsRule.spec.ts | 60 ++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 src/templateMouseEventsHaveKeyEventsRule.ts create mode 100644 test/templateMouseEventsHaveKeyEventsRule.spec.ts diff --git a/src/index.ts b/src/index.ts index e7b5f0156..d2050dd43 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,7 @@ export { Rule as TemplateAccessibilityLabelForVisitor } from './templateAccessib export { Rule as TemplatesAccessibilityAnchorContentRule } from './templateAccessibilityAnchorContentRule'; export { Rule as TemplatesNoNegatedAsync } from './templatesNoNegatedAsyncRule'; export { Rule as TemplateNoAutofocusRule } from './templateNoAutofocusRule'; +export { Rule as TemplateMouseEventsHaveKeyEventsRule } from './templateMouseEventsHaveKeyEventsRule'; export { Rule as TrackByFunctionRule } from './trackByFunctionRule'; export { Rule as UseHostPropertyDecoratorRule } from './useHostPropertyDecoratorRule'; export { Rule as UseInputPropertyDecoratorRule } from './useInputPropertyDecoratorRule'; diff --git a/src/templateMouseEventsHaveKeyEventsRule.ts b/src/templateMouseEventsHaveKeyEventsRule.ts new file mode 100644 index 000000000..0579488a8 --- /dev/null +++ b/src/templateMouseEventsHaveKeyEventsRule.ts @@ -0,0 +1,61 @@ +import { ElementAst } from '@angular/compiler'; +import { IRuleMetadata, RuleFailure, Rules } from 'tslint/lib'; +import { SourceFile } from 'typescript/lib/typescript'; +import { NgWalker } from './angular/ngWalker'; +import { BasicTemplateAstVisitor } from './angular/templates/basicTemplateAstVisitor'; + +export class Rule extends Rules.AbstractRule { + static readonly metadata: IRuleMetadata = { + description: 'Ensures that the Mouse Events mouseover and mouseout are accompanied with Key Events focus and blur', + options: null, + optionsDescription: 'Not configurable.', + rationale: 'Keyboard is important for users with physical disabilities who cannot use mouse.', + ruleName: 'template-mouse-events-have-key-events', + type: 'functionality', + typescriptOnly: true + }; + + static readonly FAILURE_STRING_MOUSE_OVER = 'mouseover must be accompanied by focus event for accessibility'; + static readonly FAILURE_STRING_MOUSE_OUT = 'mouseout must be accompanied by blur event for accessibility'; + + apply(sourceFile: SourceFile): RuleFailure[] { + return this.applyWithWalker( + new NgWalker(sourceFile, this.getOptions(), { + templateVisitorCtrl: TemplateMouseEventsHaveKeyEventsVisitor + }) + ); + } +} + +class TemplateMouseEventsHaveKeyEventsVisitor extends BasicTemplateAstVisitor { + visitElement(el: ElementAst, context: any) { + this.validateElement(el); + super.visitElement(el, context); + } + + private validateElement(el: ElementAst): void { + const hasMouseOver = el.outputs.some(output => output.name === 'mouseover'); + const hasMouseOut = el.outputs.some(output => output.name === 'mouseout'); + const hasFocus = el.outputs.some(output => output.name === 'focus'); + const hasBlur = el.outputs.some(output => output.name === 'blur'); + + if (!hasMouseOver && !hasMouseOut) { + return; + } + + const { + sourceSpan: { + end: { offset: endOffset }, + start: { offset: startOffset } + } + } = el; + + if (hasMouseOver && !hasFocus) { + this.addFailureFromStartToEnd(startOffset, endOffset, Rule.FAILURE_STRING_MOUSE_OVER); + } + + if (hasMouseOut && !hasBlur) { + this.addFailureFromStartToEnd(startOffset, endOffset, Rule.FAILURE_STRING_MOUSE_OUT); + } + } +} diff --git a/test/templateMouseEventsHaveKeyEventsRule.spec.ts b/test/templateMouseEventsHaveKeyEventsRule.spec.ts new file mode 100644 index 000000000..21f1d6181 --- /dev/null +++ b/test/templateMouseEventsHaveKeyEventsRule.spec.ts @@ -0,0 +1,60 @@ +import { Rule } from '../src/templateMouseEventsHaveKeyEventsRule'; +import { assertAnnotated, assertSuccess } from './testHelper'; + +const { + FAILURE_STRING_MOUSE_OUT, + FAILURE_STRING_MOUSE_OVER, + metadata: { ruleName } +} = Rule; + +describe(ruleName, () => { + describe('failure', () => { + it('should fail when mouseover is not accompanied with focus', () => { + const source = ` + @Component({ + template: \` +
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + \` + }) + class Bar {} + `; + assertAnnotated({ + message: FAILURE_STRING_MOUSE_OVER, + ruleName, + source + }); + }); + + it('should fail when mouseout is not accompanied with blur', () => { + const source = ` + @Component({ + template: \` +
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + \` + }) + class Bar {} + `; + assertAnnotated({ + message: FAILURE_STRING_MOUSE_OUT, + ruleName, + source + }); + }); + }); + + describe('success', () => { + it('should work find when mouse events are associated with key events', () => { + const source = ` + @Component({ + template: \` +
+ \` + }) + class Bar {} + `; + assertSuccess(ruleName, source); + }); + }); +});