diff --git a/src/index.ts b/src/index.ts index 4a2e192b3..fa7c2fc8b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,7 @@ export { Rule as PreferInlineDecorator } from './preferInlineDecoratorRule'; export { Rule as PreferOutputReadonlyRule } from './preferOutputReadonlyRule'; export { Rule as TemplateConditionalComplexityRule } from './templateConditionalComplexityRule'; export { Rule as TemplateCyclomaticComplexityRule } from './templateCyclomaticComplexityRule'; +export { Rule as TemplateAccessibilityTabindexNoPositiveRule } from './templateAccessibilityTabindexNoPositiveRule'; export { Rule as TemplatesNoNegatedAsync } from './templatesNoNegatedAsyncRule'; export { Rule as TrackByFunctionRule } from './trackByFunctionRule'; export { Rule as UseHostPropertyDecoratorRule } from './useHostPropertyDecoratorRule'; diff --git a/src/templateAccessibilityTabindexNoPositiveRule.ts b/src/templateAccessibilityTabindexNoPositiveRule.ts new file mode 100644 index 000000000..2f1e6ddb7 --- /dev/null +++ b/src/templateAccessibilityTabindexNoPositiveRule.ts @@ -0,0 +1,51 @@ +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'; +import { getAttributeValue } from './util/getAttributeValue'; + +export class Rule extends Rules.AbstractRule { + static readonly metadata: IRuleMetadata = { + description: 'Ensures that the tab index is not positive', + options: null, + optionsDescription: 'Not configurable.', + rationale: 'positive values for tabidex attribute should be avoided because they mess up with the order of focus (AX_FOCUS_03)', + ruleName: 'template-accessibility-tabindex-no-positive', + type: 'functionality', + typescriptOnly: true + }; + + static readonly FAILURE_MESSAGE = 'Tabindex cannot be positive'; + + apply(sourceFile: SourceFile): RuleFailure[] { + return this.applyWithWalker( + new NgWalker(sourceFile, this.getOptions(), { + templateVisitorCtrl: TemplateAccessibilityTabindexNoPositiveVisitor + }) + ); + } +} + +class TemplateAccessibilityTabindexNoPositiveVisitor extends BasicTemplateAstVisitor { + visitElement(ast: ElementAst, context: any): any { + this.validateElement(ast); + super.visitElement(ast, context); + } + + private validateElement(element: ElementAst) { + let tabIndexValue = getAttributeValue(element, 'tabindex'); + if (tabIndexValue) { + tabIndexValue = parseInt(tabIndexValue, 10); + if (tabIndexValue > 0) { + const { + sourceSpan: { + end: { offset: endOffset }, + start: { offset: startOffset } + } + } = element; + this.addFailureFromStartToEnd(startOffset, endOffset, Rule.FAILURE_MESSAGE); + } + } + } +} diff --git a/src/util/getAttributeValue.ts b/src/util/getAttributeValue.ts new file mode 100644 index 000000000..fdfd2c9ac --- /dev/null +++ b/src/util/getAttributeValue.ts @@ -0,0 +1,12 @@ +import { ElementAst } from '@angular/compiler'; + +export const getAttributeValue = (element: ElementAst, property: string) => { + const attr = element.attrs.find(attr => attr.name === property); + const input = element.inputs.find(input => input.name === property); + if (attr) { + return attr.value; + } + if (input) { + return (input.value).ast.value; + } +}; diff --git a/test/templateAccessibilityTabindexNoPositiveRule.spec.ts b/test/templateAccessibilityTabindexNoPositiveRule.spec.ts new file mode 100644 index 000000000..aae5c3ff5 --- /dev/null +++ b/test/templateAccessibilityTabindexNoPositiveRule.spec.ts @@ -0,0 +1,63 @@ +import { Rule } from '../src/templateAccessibilityTabindexNoPositiveRule'; +import { assertAnnotated, assertSuccess } from './testHelper'; + +const { + FAILURE_MESSAGE, + metadata: { ruleName } +} = Rule; + +describe(ruleName, () => { + describe('failure', () => { + it('should fail when tabindex attr is positive', () => { + const source = ` + @Component({ + template: \` +
+ ~~~~~~~~~~~~~~~~~~ + \` + }) + class Bar {} + `; + assertAnnotated({ + message: FAILURE_MESSAGE, + ruleName, + source + }); + }); + + it('should fail when tabindex input is positive', () => { + const source = ` + @Component({ + template: \` +
+ ~~~~~~~~~~~~~~~~~~~~~~~~~ + \` + }) + class Bar {} + `; + assertAnnotated({ + message: FAILURE_MESSAGE, + ruleName, + source + }); + }); + }); + + describe('success', () => { + it('should work with tab index is not positive', () => { + const source = ` + @Component({ + template: \` + + + + + + \` + }) + class Bar {} + `; + assertSuccess(ruleName, source); + }); + }); +});