-
Notifications
You must be signed in to change notification settings - Fork 235
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rule): use valid aria rules (#746)
- Loading branch information
1 parent
6ff8c56
commit 762f67f
Showing
7 changed files
with
179 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { AttrAst, BoundElementPropertyAst } 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 { aria } from 'aria-query'; | ||
import { getSuggestion } from './util/getSuggestion'; | ||
|
||
const ariaAttributes: string[] = [...(<string[]>Array.from(aria.keys()))]; | ||
|
||
export class Rule extends Rules.AbstractRule { | ||
static readonly metadata: IRuleMetadata = { | ||
description: 'Ensures that the correct ARIA attributes are used', | ||
options: null, | ||
optionsDescription: 'Not configurable.', | ||
rationale: 'Elements should not use invalid aria attributes (AX_ARIA_11)', | ||
ruleName: 'template-accessibility-valid-aria', | ||
type: 'functionality', | ||
typescriptOnly: true | ||
}; | ||
|
||
apply(sourceFile: SourceFile): RuleFailure[] { | ||
return this.applyWithWalker( | ||
new NgWalker(sourceFile, this.getOptions(), { | ||
templateVisitorCtrl: TemplateAccessibilityValidAriaVisitor | ||
}) | ||
); | ||
} | ||
} | ||
|
||
export const getFailureMessage = (name: string): string => { | ||
const suggestions = getSuggestion(name, ariaAttributes); | ||
const message = `${name}: This attribute is an invalid ARIA attribute.`; | ||
|
||
if (suggestions.length > 0) { | ||
return `${message} Did you mean to use ${suggestions}?`; | ||
} | ||
|
||
return message; | ||
}; | ||
|
||
class TemplateAccessibilityValidAriaVisitor extends BasicTemplateAstVisitor { | ||
visitAttr(ast: AttrAst, context: any) { | ||
this.validateAttribute(ast); | ||
super.visitAttr(ast, context); | ||
} | ||
|
||
visitElementProperty(ast: BoundElementPropertyAst) { | ||
this.validateAttribute(ast); | ||
super.visitElementProperty(ast, context); | ||
} | ||
|
||
private validateAttribute(ast: AttrAst | BoundElementPropertyAst) { | ||
if (ast.name.indexOf('aria-') !== 0) return; | ||
const isValid = ariaAttributes.indexOf(ast.name) > -1; | ||
if (isValid) return; | ||
|
||
const { | ||
sourceSpan: { | ||
end: { offset: endOffset }, | ||
start: { offset: startOffset } | ||
} | ||
} = ast; | ||
this.addFailureFromStartToEnd(startOffset, endOffset, getFailureMessage(ast.name)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import * as editDistance from 'damerau-levenshtein'; | ||
|
||
const THRESHOLD = 2; | ||
|
||
export const getSuggestion = (word: string, dictionary: string[] = [], limit = 2) => { | ||
const distances = dictionary.reduce((suggestions, dictionaryWord: string) => { | ||
const distance = editDistance(word.toUpperCase(), dictionaryWord.toUpperCase()); | ||
const { steps } = distance; | ||
suggestions[dictionaryWord] = steps; | ||
return suggestions; | ||
}, {}); | ||
|
||
return Object.keys(distances) | ||
.filter(suggestion => distances[suggestion] <= THRESHOLD) | ||
.sort((a, b) => distances[a] - distances[b]) | ||
.slice(0, limit); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { getFailureMessage, Rule } from '../src/templateAccessibilityValidAriaRule'; | ||
import { assertAnnotated, assertSuccess } from './testHelper'; | ||
|
||
const { | ||
metadata: { ruleName } | ||
} = Rule; | ||
|
||
describe(ruleName, () => { | ||
describe('failure', () => { | ||
it('should fail when aria attributes are misspelled or if they does not exist', () => { | ||
const source = ` | ||
@Component({ | ||
template: \` | ||
<input aria-labelby="text"> | ||
~~~~~~~~~~~~~~~~~~~ | ||
\` | ||
}) | ||
class Bar {} | ||
`; | ||
assertAnnotated({ | ||
message: getFailureMessage('aria-labelby'), | ||
ruleName, | ||
source | ||
}); | ||
}); | ||
|
||
it('should fail when using wrong aria attributes with inputs', () => { | ||
const source = ` | ||
@Component({ | ||
template: \` | ||
<input [attr.aria-labelby]="text"> | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
\` | ||
}) | ||
class Bar {} | ||
`; | ||
assertAnnotated({ | ||
message: getFailureMessage('aria-labelby'), | ||
ruleName, | ||
source | ||
}); | ||
}); | ||
}); | ||
|
||
describe('success', () => { | ||
it('should work when the aria attributes are correctly named', () => { | ||
const source = ` | ||
@Component({ | ||
template: \` | ||
<input aria-labelledby="Text"> | ||
<input [attr.aria-labelledby]="text"> | ||
\` | ||
}) | ||
class Bar {} | ||
`; | ||
assertSuccess(ruleName, source); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { expect } from 'chai'; | ||
|
||
import { getSuggestion } from '../../src/util/getSuggestion'; | ||
|
||
describe('getSuggestion', () => { | ||
it('should suggest based on dictionary', () => { | ||
const suggestion = getSuggestion('wordd', ['word', 'words', 'wording'], 2); | ||
expect(suggestion).to.deep.equals(['word', 'words']); | ||
}); | ||
|
||
it("should not suggest if the dictionary doesn't have any similar words", () => { | ||
const suggestion = getSuggestion('ink', ['word', 'words', 'wording'], 2); | ||
expect(suggestion).to.deep.equals([]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters