diff --git a/src/index.ts b/src/index.ts index e5782e32d..8ec6f2cc7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,7 @@ export { Rule as TemplateNoAnyRule } from './templateNoAnyRule'; export { Rule as TemplateAccessibilityLabelForVisitor } from './templateAccessibilityLabelForRule'; export { Rule as TemplateAccessibilityValidAriaRule } from './templateAccessibilityValidAriaRule'; export { Rule as TemplatesAccessibilityAnchorContentRule } from './templateAccessibilityAnchorContentRule'; +export { Rule as TemplateClickEventsHaveKeyEventsRule } from './templateClickEventsHaveKeyEventsRule'; export { Rule as TemplateAccessibilityAltTextRule } from './templateAccessibilityAltTextRule'; export { Rule as TemplatesNoNegatedAsync } from './templatesNoNegatedAsyncRule'; export { Rule as TemplateNoAutofocusRule } from './templateNoAutofocusRule'; diff --git a/src/templateClickEventsHaveKeyEventsRule.ts b/src/templateClickEventsHaveKeyEventsRule.ts new file mode 100644 index 000000000..fd078e79d --- /dev/null +++ b/src/templateClickEventsHaveKeyEventsRule.ts @@ -0,0 +1,55 @@ +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 click event is accompanied with at least one key event keyup, keydown or keypress', + options: null, + optionsDescription: 'Not configurable.', + rationale: 'Keyboard is important for users with physical disabilities who cannot use mouse.', + ruleName: 'template-click-events-have-key-events', + type: 'functionality', + typescriptOnly: true + }; + + static readonly FAILURE_STRING = 'click must be accompanied by either keyup, keydown or keypress event for accessibility'; + + apply(sourceFile: SourceFile): RuleFailure[] { + return this.applyWithWalker( + new NgWalker(sourceFile, this.getOptions(), { + templateVisitorCtrl: TemplateClickEventsHaveKeyEventsVisitor + }) + ); + } +} + +class TemplateClickEventsHaveKeyEventsVisitor extends BasicTemplateAstVisitor { + visitElement(el: ElementAst, context: any) { + this.validateElement(el); + super.visitElement(el, context); + } + + private validateElement(el: ElementAst): void { + const hasClick = el.outputs.some(output => output.name === 'click'); + if (!hasClick) { + return; + } + const hasKeyEvent = el.outputs.some(output => output.name === 'keyup' || output.name === 'keydown' || output.name === 'keypress'); + + if (hasKeyEvent) { + return; + } + + const { + sourceSpan: { + end: { offset: endOffset }, + start: { offset: startOffset } + } + } = el; + + this.addFailureFromStartToEnd(startOffset, endOffset, Rule.FAILURE_STRING); + } +} diff --git a/test/templateClickEventsHaveKeyEventsRule.spec.ts b/test/templateClickEventsHaveKeyEventsRule.spec.ts new file mode 100644 index 000000000..544233668 --- /dev/null +++ b/test/templateClickEventsHaveKeyEventsRule.spec.ts @@ -0,0 +1,45 @@ +import { Rule } from '../src/templateClickEventsHaveKeyEventsRule'; +import { assertAnnotated, assertSuccess } from './testHelper'; + +const { + FAILURE_STRING, + metadata: { ruleName } +} = Rule; + +describe(ruleName, () => { + describe('failure', () => { + it('should fail when click is not accompanied with key events', () => { + const source = ` + @Component({ + template: \` +
+ ~~~~~~~~~~~~~~~~~~~~~~~~~ + \` + }) + class Bar {} + `; + assertAnnotated({ + message: FAILURE_STRING, + ruleName, + source + }); + }); + }); + + describe('success', () => { + it('should work find when click events are associated with key events', () => { + const source = ` + @Component({ + template: \` +
+
+
+
+ \` + }) + class Bar {} + `; + assertSuccess(ruleName, source); + }); + }); +});