Skip to content

Commit

Permalink
feat(rule): label accessibility - should have associated control (#739)
Browse files Browse the repository at this point in the history
  • Loading branch information
mohammedzamakhan authored and mgechev committed Feb 11, 2019
1 parent 799382f commit 76c24fa
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -29,6 +29,7 @@ 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 TemplateAccessibilityLabelForVisitor } from './templateAccessibilityLabelForRule';
export { Rule as TemplatesNoNegatedAsync } from './templatesNoNegatedAsyncRule';
export { Rule as TemplateNoAutofocusRule } from './templateNoAutofocusRule';
export { Rule as TrackByFunctionRule } from './trackByFunctionRule';
Expand Down
100 changes: 100 additions & 0 deletions src/templateAccessibilityLabelForRule.ts
@@ -0,0 +1,100 @@
import { BoundDirectivePropertyAst, ElementAst } from '@angular/compiler';
import { sprintf } from 'sprintf-js';
import { IRuleMetadata, RuleFailure, Rules, Utils } from 'tslint/lib';
import { SourceFile } from 'typescript/lib/typescript';
import { NgWalker } from './angular/ngWalker';
import { BasicTemplateAstVisitor } from './angular/templates/basicTemplateAstVisitor';
import { mayContainChildComponent } from './util/mayContainChildComponent';

interface ILabelForOptions {
labelComponents: string[];
labelAttributes: string[];
controlComponents: string[];
}
export class Rule extends Rules.AbstractRule {
static readonly metadata: IRuleMetadata = {
description: 'Checks if the label has associated for attribute or a form element',
optionExamples: [[true, { labelComponents: ['app-label'], labelAttributes: ['id'], controlComponents: ['app-input', 'app-select'] }]],
options: {
items: {
type: 'object',
properties: {
labelComponents: {
type: 'array',
items: {
type: 'string'
}
},
labelAttributes: {
type: 'array',
items: {
type: 'string'
}
},
controlComponents: {
type: 'array',
items: {
type: 'string'
}
}
}
},
type: 'array'
},
optionsDescription: 'Add custom label, label attribute and controls',
rationale: Utils.dedent`
The label tag should either have a for attribute or should have associated control.
This rule supports two ways, either the label component should explicitly have a for attribute or a control nested inside the label component
It also supports adding custom control component and custom label component support.`,
ruleName: 'template-accessibility-label-for',
type: 'functionality',
typescriptOnly: true
};

static readonly FAILURE_STRING = 'A form label must be associated with a control';
static readonly FORM_ELEMENTS = ['input', 'select', 'textarea'];

apply(sourceFile: SourceFile): RuleFailure[] {
return this.applyWithWalker(
new NgWalker(sourceFile, this.getOptions(), {
templateVisitorCtrl: TemplateAccessibilityLabelForVisitor
})
);
}
}

class TemplateAccessibilityLabelForVisitor extends BasicTemplateAstVisitor {
visitElement(element: ElementAst, context: any) {
this.validateElement(element);
super.visitElement(element, context);
}

private validateElement(element: ElementAst) {
let { labelAttributes, labelComponents, controlComponents }: ILabelForOptions = this.getOptions() || {};
controlComponents = Rule.FORM_ELEMENTS.concat(controlComponents || []);
labelComponents = ['label'].concat(labelComponents || []);
labelAttributes = ['for'].concat(labelAttributes || []);

if (labelComponents.indexOf(element.name) === -1) {
return;
}
const hasForAttr = element.attrs.some(attr => labelAttributes.indexOf(attr.name) !== -1);
const hasForInput = element.inputs.some(input => {
return labelAttributes.indexOf(input.name) !== -1;
});

const hasImplicitFormElement = controlComponents.some(component => mayContainChildComponent(element, component));

if (hasForAttr || hasForInput || hasImplicitFormElement) {
return;
}
const {
sourceSpan: {
end: { offset: endOffset },
start: { offset: startOffset }
}
} = element;

this.addFailureFromStartToEnd(startOffset, endOffset, Rule.FAILURE_STRING);
}
}
22 changes: 22 additions & 0 deletions src/util/mayContainChildComponent.ts
@@ -0,0 +1,22 @@
import { ElementAst } from '@angular/compiler';

export function mayContainChildComponent(root: ElementAst, componentName: string): boolean {
function traverseChildren(node: ElementAst): boolean {
if (!node.children) {
return false;
}
if (node.children) {
for (let i = 0; i < node.children.length; i += 1) {
const childNode: ElementAst = <ElementAst>node.children[i];
if (childNode.name === componentName) {
return true;
}
if (traverseChildren(childNode)) {
return true;
}
}
}
return false;
}
return traverseChildren(root);
}
127 changes: 127 additions & 0 deletions test/templateAccessibilityLabelForRule.spec.ts
@@ -0,0 +1,127 @@
import { Rule } from '../src/templateAccessibilityLabelForRule';
import { assertAnnotated, assertSuccess } from './testHelper';

const {
FAILURE_STRING,
metadata: { ruleName }
} = Rule;

describe(ruleName, () => {
describe('failure', () => {
it("should fail when label doesn't have for attribute", () => {
const source = `
@Component({
template: \`
<label>Label</label>
~~~~~~~
\`
})
class Bar {}
`;
assertAnnotated({
message: FAILURE_STRING,
ruleName,
source
});
});

it("should fail when custom label doesn't have label attribute", () => {
const source = `
@Component({
template: \`
<app-label></app-label>
~~~~~~~~~~~
\`
})
class Bar {}
`;
assertAnnotated({
message: FAILURE_STRING,
ruleName,
source,
options: {
labelComponents: ['app-label'],
labelAttributes: ['id']
}
});
});
});

describe('success', () => {
it('should work when label has for attribute', () => {
const source = `
@Component({
template: \`
<label for="id"></label>
<label [attr.for]="id"></label>
\`
})
class Bar {}
`;
assertSuccess(ruleName, source);
});

it('should work when label are associated implicitly', () => {
const source = `
@Component({
template: \`
<label>
Label
<input />
</label>
<label>
Label
<span><input /></span>
</label>
<app-label>
<span>
<app-input></app-input>
</span>
</app-label>
\`
})
class Bar {}
`;
assertSuccess(ruleName, source, {
labelComponents: ['app-label'],
controlComponents: ['app-input']
});
});

it("should fail when label doesn't have for attribute", () => {
const source = `
@Component({
template: \`
<label>
<span>
<span>
<input>
</span>
</span>
</label>
\`
})
class Bar {}
`;
assertSuccess(ruleName, source);
});

it('should work when custom label has label attribute', () => {
const source = `
@Component({
template: \`
<app-label id="name"></app-label>
<app-label [id]="name"></app-label>
\`
})
class Bar {}
`;
assertSuccess(ruleName, source, {
labelComponents: ['app-label'],
labelAttributes: ['id']
});
});
});
});

0 comments on commit 76c24fa

Please sign in to comment.