Skip to content

Commit 76d0853

Browse files
nadyafebiJosh Goldberg
authored andcommitted
Fix microsoft#394: Add ignore-case and ignore-whitespace options to react-a11y-anchors (microsoft#524)
* Add ignore-case and ignore-whitespace options to react-a11y-anchors * Add unit tests for react-a11y-anchor new options * Add documentation for react-a11y-anchor new options * Add new unit tests with no options enabled for react-a11y-anchors * Update ignore-whitespace to allow trim or all whitespace * Add and update ignore-whitespace unit tests * Update ignore-whitespace documentation
1 parent 7574010 commit 76d0853

File tree

3 files changed

+165
-7
lines changed

3 files changed

+165
-7
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ Rule Name | Description | Since
128128
`prefer-array-literal` | Use array literal syntax when declaring or instantiating array types. For example, prefer the Javascript form of string[] to the TypeScript form Array<string>. Prefer '[]' to 'new Array()'. Prefer '[4, 5]' to 'new Array(4, 5)'. Prefer '[undefined, undefined]' to 'new Array(4)'. Since 2.0.10, this rule can be configured to allow Array type parameters. To ignore type parameters, configure the rule with the values: `[ true, { 'allow-type-parameters': true } ]`<br/>This rule has some overlap with the [TSLint array-type rule](https://palantir.github.io/tslint/rules/array-type), however, the version here catches more instances. | 1.0, 2.0.10
129129
`prefer-type-cast` | Prefer the tradition type casts instead of the new 'as-cast' syntax. For example, prefer `<string>myVariable` instead of `myVariable as string`. Rule ignores any file ending in .tsx. If you prefer the opposite and want to see the `as type` casts, then enable the tslint rule named 'no-angle-bracket-type-assertion'| 2.0.4
130130
`promise-must-complete` | When a Promise instance is created, then either the reject() or resolve() parameter must be called on it within all code branches in the scope. For more examples see the [feature request](https://github.com/Microsoft/tslint-microsoft-contrib/issues/34). <br/><br/>This rule has some overlap with the [tslint no-floating-promises rule](https://palantir.github.io/tslint/rules/no-floating-promises), but they are substantially different. | 1.0
131-
`react-a11y-anchors` | For accessibility of your website, anchor element link text should be at least 4 characters long. Links with the same HREF should have the same link text. Links that point to different HREFs should have different link text. Links with images and text content, the alt attribute should be unique to the text content or empty. An an anchor element's href prop value must not be undefined, null, or just #. <br/>References:<br/>[WCAG Rule 38: Link text should be as least four 4 characters long](http://oaa-accessibility.org/wcag20/rule/38/)<br/>[WCAG Rule 39: Links with the same HREF should have the same link text](http://oaa-accessibility.org/wcag20/rule/39/)<br/>[WCAG Rule 41: Links that point to different HREFs should have different link text](http://oaa-accessibility.org/wcag20/rule/41/)<br/>[WCAG Rule 43: Links with images and text content, the alt attribute should be unique to the text content or empty](http://oaa-accessibility.org/wcag20/rule/43/)<br/> | 2.0.11
131+
`react-a11y-anchors` | For accessibility of your website, anchor element link text should be at least 4 characters long. Links with the same HREF should have the same link text. Links that point to different HREFs should have different link text. This can be relaxed to allow differences in cases using `ignore-case` option. You can also allow differences in leading/trailing whitespace by adding `{"ignore-whitespace": "trim"}` option or all whitespace by adding `{"ignore-whitespace": "all"}` option. Links with images and text content, the alt attribute should be unique to the text content or empty. An an anchor element's href prop value must not be undefined, null, or just #. <br/>References:<br/>[WCAG Rule 38: Link text should be as least four 4 characters long](http://oaa-accessibility.org/wcag20/rule/38/)<br/>[WCAG Rule 39: Links with the same HREF should have the same link text](http://oaa-accessibility.org/wcag20/rule/39/)<br/>[WCAG Rule 41: Links that point to different HREFs should have different link text](http://oaa-accessibility.org/wcag20/rule/41/)<br/>[WCAG Rule 43: Links with images and text content, the alt attribute should be unique to the text content or empty](http://oaa-accessibility.org/wcag20/rule/43/)<br/> | 2.0.11
132132
`react-a11y-aria-unsupported-elements` | For accessibility of your website, enforce that elements that do not support ARIA roles, states, and properties do not have those attributes. | 2.0.11
133133
`react-a11y-event-has-role` | For accessibility of your website, Elements with event handlers must have explicit role or implicit role.<br/>References:<br/>[WCAG Rule 94](http://oaa-accessibility.org/wcag20/rule/94/)<br/>[Using the button role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role) | 2.0.11
134134
`react-a11y-image-button-has-alt` | For accessibility of your website, enforce that inputs element with `type="image"` must have non-empty alt attribute. | 2.0.11

src/reactA11yAnchorsRule.ts

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import {
1111
isEmpty
1212
} from './utils/JsxAttribute';
1313

14+
export const OPTION_IGNORE_CASE: string = 'ignore-case';
15+
export const OPTION_IGNORE_WHITESPACE: string = 'ignore-whitespace';
16+
1417
const ROLE_STRING: string = 'role';
1518

1619
export const NO_HASH_FAILURE_STRING: string =
@@ -36,8 +39,21 @@ export class Rule extends Lint.Rules.AbstractRule {
3639
ruleName: 'react-a11y-anchors',
3740
type: 'functionality',
3841
description: 'For accessibility of your website, anchor elements must have a href different from # and a text longer than 4.',
39-
options: null,
40-
optionsDescription: '',
42+
options: {
43+
type: 'array',
44+
items: {
45+
type: 'string',
46+
enum: [OPTION_IGNORE_CASE, OPTION_IGNORE_WHITESPACE]
47+
},
48+
minLength: 0,
49+
maxLength: 2
50+
},
51+
optionsDescription: Lint.Utils.dedent`
52+
Optional arguments to relax the same HREF same link text rule are provided:
53+
* \`${OPTION_IGNORE_CASE}\` ignore differences in cases.
54+
* \`{"${OPTION_IGNORE_WHITESPACE}": "trim"}\` ignore differences only in leading/trailing whitespace.
55+
* \`{"${OPTION_IGNORE_WHITESPACE}": "all"}\` ignore differences in all whitespace.
56+
`,
4157
typescriptOnly: true,
4258
issueClass: 'Non-SDL',
4359
issueType: 'Warning',
@@ -60,9 +76,27 @@ export class Rule extends Lint.Rules.AbstractRule {
6076
}
6177

6278
class ReactA11yAnchorsRuleWalker extends ErrorTolerantWalker {
63-
79+
private ignoreCase: boolean = false;
80+
private ignoreWhitespace: string = '';
6481
private anchorInfoList: IAnchorInfo[] = [];
6582

83+
constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) {
84+
super(sourceFile, options);
85+
this.parseOptions();
86+
}
87+
88+
private parseOptions(): void {
89+
this.getOptions().forEach((opt: any) => {
90+
if (typeof opt === 'string' && opt === OPTION_IGNORE_CASE) {
91+
this.ignoreCase = true;
92+
}
93+
94+
if (typeof opt === 'object') {
95+
this.ignoreWhitespace = opt[OPTION_IGNORE_WHITESPACE];
96+
}
97+
});
98+
}
99+
66100
public validateAllAnchors(): void {
67101
const sameHrefDifferentTexts: IAnchorInfo[] = [];
68102
const differentHrefSameText: IAnchorInfo[] = [];
@@ -72,7 +106,7 @@ class ReactA11yAnchorsRuleWalker extends ErrorTolerantWalker {
72106
this.anchorInfoList.forEach((anchorInfo: IAnchorInfo): void => {
73107
if (current.href &&
74108
current.href === anchorInfo.href &&
75-
(current.text !== anchorInfo.text || current.altText !== anchorInfo.altText) &&
109+
!this.compareAnchorsText(current, anchorInfo) &&
76110
!Utils.contains(sameHrefDifferentTexts, anchorInfo)) {
77111

78112
// Same href - different text...
@@ -82,8 +116,7 @@ class ReactA11yAnchorsRuleWalker extends ErrorTolerantWalker {
82116
}
83117

84118
if (current.href !== anchorInfo.href &&
85-
current.text === anchorInfo.text &&
86-
current.altText === anchorInfo.altText &&
119+
this.compareAnchorsText(current, anchorInfo) &&
87120
!Utils.contains(differentHrefSameText, anchorInfo)) {
88121

89122
// Different href - same text...
@@ -95,6 +128,37 @@ class ReactA11yAnchorsRuleWalker extends ErrorTolerantWalker {
95128
}
96129
}
97130

131+
private compareAnchorsText(anchor1: IAnchorInfo, anchor2: IAnchorInfo): boolean {
132+
let text1: string = anchor1.text;
133+
let text2: string = anchor2.text;
134+
let altText1: string = anchor1.altText;
135+
let altText2: string = anchor2.altText;
136+
137+
if (this.ignoreCase) {
138+
text1 = text1.toLowerCase();
139+
text2 = text2.toLowerCase();
140+
altText1 = altText1.toLowerCase();
141+
altText2 = altText2.toLowerCase();
142+
}
143+
144+
if (this.ignoreWhitespace === 'trim') {
145+
text1 = text1.trim();
146+
text2 = text2.trim();
147+
altText1 = altText1.trim();
148+
altText2 = altText2.trim();
149+
}
150+
151+
if (this.ignoreWhitespace === 'all') {
152+
const regex: RegExp = /\s/g;
153+
text1 = text1.replace(regex, '');
154+
text2 = text2.replace(regex, '');
155+
altText1 = altText1.replace(regex, '');
156+
altText2 = altText2.replace(regex, '');
157+
}
158+
159+
return text1 === text2 && altText1 === altText2;
160+
}
161+
98162
private firstPosition(anchorInfo: IAnchorInfo): string {
99163
const startPosition: ts.LineAndCharacter =
100164
this.createFailure(anchorInfo.start, anchorInfo.width, '').getStartPosition().getLineAndCharacter();

src/tests/ReactA11yAnchorsRuleTests.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {TestHelper} from './TestHelper';
22
import {
3+
OPTION_IGNORE_CASE,
4+
OPTION_IGNORE_WHITESPACE,
35
MISSING_HREF_FAILURE_STRING,
46
NO_HASH_FAILURE_STRING,
57
LINK_TEXT_TOO_SHORT_FAILURE_STRING,
@@ -259,6 +261,48 @@ describe('reactA11yAnchorsRule', () : void => {
259261
TestHelper.assertViolations(ruleName, script, [ ]);
260262
});
261263

264+
it('should pass when identical hrefs have texts with different cases on ignore-case', () : void => {
265+
const script : string = `
266+
import React = require('react);
267+
const anchor1 = <a href="someRef1">someTitle1</a>;
268+
const anchor2 = <a href="someRef2">someTitle2</a>;
269+
const anchor3 = <a href="someRef1">SomeTitle1</a>;
270+
const anchor4 = <a href="someRef2">sometitle2</a>;
271+
`;
272+
273+
TestHelper.assertNoViolationWithOptions(ruleName, [true, OPTION_IGNORE_CASE], script);
274+
});
275+
276+
it('should pass when identical hrefs have texts with different leading/trailing whitespace on ignore-whitespace trim', () : void => {
277+
const opt : any = {};
278+
opt[OPTION_IGNORE_WHITESPACE] = 'trim';
279+
280+
const script : string = `
281+
import React = require('react);
282+
const anchor1 = <a href="someRef1">someTitle1</a>;
283+
const anchor2 = <a href="someRef2"><span>someTitle</span><img alt="someAlt2" /></a>;
284+
const anchor3 = <a href="someRef1">someTitle1 </a>;
285+
const anchor4 = <a href="someRef2"><span>someTitle </span><img alt="someAlt2" /></a>
286+
`;
287+
288+
TestHelper.assertNoViolationWithOptions(ruleName, [true, opt], script);
289+
});
290+
291+
it('should pass when identical hrefs have texts with different whitespace on ignore-whitespace all', () : void => {
292+
const opt : any = {};
293+
opt[OPTION_IGNORE_WHITESPACE] = 'all';
294+
295+
const script : string = `
296+
import React = require('react);
297+
const anchor1 = <a href="someRef1">someTitle1</a>;
298+
const anchor2 = <a href="someRef2"><span>someTitle</span><img alt="someAlt2" /></a>;
299+
const anchor3 = <a href="someRef1">s o m e T i t l e 1</a>;
300+
const anchor4 = <a href="someRef2"><span>some Title</span><img alt="someAlt2" /></a>
301+
`;
302+
303+
TestHelper.assertNoViolationWithOptions(ruleName, [true, opt], script);
304+
});
305+
262306
it('should fail when identical hrefs have different texts', () : void => {
263307
const script : string = `
264308
import React = require('react');
@@ -284,6 +328,56 @@ describe('reactA11yAnchorsRule', () : void => {
284328
]);
285329
});
286330

331+
it('should fail when identical hrefs have texts with different cases', () : void => {
332+
const script : string = `
333+
import React = require('react');
334+
const anchor1 = <a href="someRef">someTitle1</a>;
335+
const anchor2 = <a href="someRef1">someTitle2</a>;
336+
const anchor3 = <a href="someRef">SomeTitle1</a>; // should fail with line 3
337+
const anchor4 = <a href="someRef1">sometitle2</a>; // should fail with line 4
338+
`;
339+
340+
TestHelper.assertViolations(ruleName, script, [
341+
{
342+
"failure": `${SAME_HREF_SAME_TEXT_FAILURE_STRING} First link at character: 29 line: 3`,
343+
"name": "file.tsx",
344+
"ruleName": "react-a11y-anchors",
345+
"startPosition": { "character": 29, "line": 5 }
346+
},
347+
{
348+
"failure": `${SAME_HREF_SAME_TEXT_FAILURE_STRING} First link at character: 29 line: 4`,
349+
"name": "file.tsx",
350+
"ruleName": "react-a11y-anchors",
351+
"startPosition": { "character": 29, "line": 6 }
352+
}
353+
]);
354+
});
355+
356+
it('should fail when identical hrefs have texts with different whitespace', () : void => {
357+
const script : string = `
358+
import React = require('react);
359+
const anchor1 = <a href="someRef1">someTitle1</a>;
360+
const anchor2 = <a href="someRef2"><span>someTitle</span><img alt="someAlt2" /></a>;
361+
const anchor3 = <a href="someRef1">someTitle1 </a>; // should fail with line 3
362+
const anchor4 = <a href="someRef2"><span>some Title</span><img alt="someAlt2" /></a> // should fail with line 4
363+
`;
364+
365+
TestHelper.assertViolations(ruleName, script, [
366+
{
367+
"failure": `${SAME_HREF_SAME_TEXT_FAILURE_STRING} First link at character: 29 line: 3`,
368+
"name": "file.tsx",
369+
"ruleName": "react-a11y-anchors",
370+
"startPosition": { "character": 29, "line": 5 }
371+
},
372+
{
373+
"failure": `${SAME_HREF_SAME_TEXT_FAILURE_STRING} First link at character: 29 line: 4`,
374+
"name": "file.tsx",
375+
"ruleName": "react-a11y-anchors",
376+
"startPosition": { "character": 29, "line": 6 }
377+
}
378+
]);
379+
});
380+
287381
it('should fail when identical hrefs have different alt texts', () : void => {
288382
const script : string = `
289383
import React = require('react');

0 commit comments

Comments
 (0)