From d2b985fd654992db3fb72acda5c7a8812bd13105 Mon Sep 17 00:00:00 2001 From: Brian Chen Date: Sat, 13 Oct 2018 13:52:03 +0200 Subject: [PATCH 1/6] Add new rule of react-a11y-input-elements --- src/reactA11yInputElementsRule.ts | 70 ++++++++++++++++++++ src/tests/ReactA11yInputElementsRuleTests.ts | 56 ++++++++++++++++ tslint.json | 20 ++++-- 3 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 src/reactA11yInputElementsRule.ts create mode 100644 src/tests/ReactA11yInputElementsRuleTests.ts diff --git a/src/reactA11yInputElementsRule.ts b/src/reactA11yInputElementsRule.ts new file mode 100644 index 000000000..2e3cf010d --- /dev/null +++ b/src/reactA11yInputElementsRule.ts @@ -0,0 +1,70 @@ +import * as ts from 'typescript'; +import * as Lint from 'tslint'; + +import {ErrorTolerantWalker} from './utils/ErrorTolerantWalker'; +import {getJsxAttributesFromJsxElement, isEmpty} from './utils/JsxAttribute'; +import {ExtendedMetadata} from './utils/ExtendedMetadata'; + +export const MISSING_PLACEHOLDER_INPUT_FAILURE_STRING: string + = 'Input elements must include default, place-holding characters if empty'; +export const MISSING_PLACEHOLDER_TEXTAREA_FAILURE_STRING: string + = 'Textarea elements must include default, place-holding characters if empty'; + +/** + * Implementation of the react-a11y-input-elements rule. + */ +export class Rule extends Lint.Rules.AbstractRule { + + public static metadata: ExtendedMetadata = { + ruleName: 'react-a11y-input-elements', + type: 'functionality', + description: 'For accessibility of your website, HTML input boxes and text areas must include default, place-holding characters.', + options: null, + optionsDescription: '', + typescriptOnly: true, + issueClass: 'Non-SDL', + issueType: 'Warning', + severity: 'Moderate', + level: 'Opportunity for Excellence', + group: 'Accessibility' + }; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + if (sourceFile.languageVariant === ts.LanguageVariant.JSX) { + return this.applyWithWalker(new ReactA11yInputElementsRuleWalker(sourceFile, this.getOptions())); + } else { + return []; + } + } +} + +class ReactA11yInputElementsRuleWalker extends ErrorTolerantWalker { + + protected visitJsxSelfClosingElement(node: ts.JsxSelfClosingElement): void { + const tagName = node.tagName.getText(); + const attributes: { [propName: string]: ts.JsxAttribute } = getJsxAttributesFromJsxElement(node); + + if (tagName === 'input') { + if (isEmpty(attributes.value) && isEmpty(attributes.placeholder)) { + this.addFailureAt(node.getStart(), node.getWidth(), MISSING_PLACEHOLDER_INPUT_FAILURE_STRING); + } + } else if (tagName === 'textarea') { + if (isEmpty(attributes.placeholder)) { + this.addFailureAt(node.getStart(), node.getWidth(), MISSING_PLACEHOLDER_TEXTAREA_FAILURE_STRING); + } + } + super.visitJsxSelfClosingElement(node); + } + + protected visitJsxElement(node: ts.JsxElement): void { + const tagName = node.openingElement.tagName.getText(); + const attributes: { [propName: string]: ts.JsxAttribute } = getJsxAttributesFromJsxElement(node); + + if (tagName === 'textarea') { + if (node.children.length === 0 && isEmpty(attributes.placeholder)) { + this.addFailureAt(node.getStart(), node.getWidth(), MISSING_PLACEHOLDER_TEXTAREA_FAILURE_STRING); + } + } + super.visitJsxElement(node); + } +} diff --git a/src/tests/ReactA11yInputElementsRuleTests.ts b/src/tests/ReactA11yInputElementsRuleTests.ts new file mode 100644 index 000000000..48b09305e --- /dev/null +++ b/src/tests/ReactA11yInputElementsRuleTests.ts @@ -0,0 +1,56 @@ +import {TestHelper} from './TestHelper'; +import { + MISSING_PLACEHOLDER_INPUT_FAILURE_STRING, + MISSING_PLACEHOLDER_TEXTAREA_FAILURE_STRING +} from '../reactA11yInputElementsRule'; + +/** + * Unit tests. + */ +describe('reactA11yInputElementsRule', () : void => { + + const ruleName : string = 'react-a11y-input-elements'; + + it('should pass on input elements with placeholder', () : void => { + const script : string = ` + import React = require('react'); + const a = ; + const b = ; + const c = ; + `; + + TestHelper.assertViolations(ruleName, script, [ ]); + }); + + it('should fail on empty input elements without placeholder', () : void => { + const script : string = ` + import React = require('react'); + const a = ; + const b = ; + `; + + TestHelper.assertViolations(ruleName, script, [ + { + "failure": MISSING_PLACEHOLDER_INPUT_FAILURE_STRING, + "name": "file.tsx", + "ruleName": "react-a11y-input-elements", + "startPosition": { "character": 23, "line": 3 } + }, + { + "failure": MISSING_PLACEHOLDER_TEXTAREA_FAILURE_STRING, + "name": "file.tsx", + "ruleName": "react-a11y-input-elements", + "startPosition": { "character": 23, "line": 4 } + }, + { + "failure": MISSING_PLACEHOLDER_TEXTAREA_FAILURE_STRING, + "name": "file.tsx", + "ruleName": "react-a11y-input-elements", + "startPosition": { "character": 23, "line": 5 } + } + ]); + }); + +}); diff --git a/tslint.json b/tslint.json index 4f8b6d387..361ade4b1 100644 --- a/tslint.json +++ b/tslint.json @@ -3,7 +3,10 @@ "dist/src" ], "rules": { - "array-type": [true, "array"], + "array-type": [ + true, + "array" + ], "arrow-return-shorthand": false, "await-promise": false, "adjacent-overload-signatures": false, @@ -142,7 +145,9 @@ "no-mergeable-namespace": false, "no-missing-visibility-modifiers": false, "no-misused-new": true, - "no-multiline-string": [ false ], + "no-multiline-string": [ + false + ], "no-multiple-var-decl": false, "no-namespace": false, "no-null-keyword": false, @@ -201,7 +206,11 @@ "check-whitespace" ], "one-variable-per-declaration": true, - "only-arrow-functions": [true, "allow-declarations", "allow-named-functions"], + "only-arrow-functions": [ + true, + "allow-declarations", + "allow-named-functions" + ], "ordered-imports": [ false ], @@ -309,6 +318,7 @@ "space-within-parens": true, "switch-final-break": true, "type-literal-delimiter": false, - "use-default-type-parameter": false + "use-default-type-parameter": false, + "react-a11y-input-elements": true } -} +} \ No newline at end of file From 91eebd973c088c9fcb7b135ec655917e1c8b63dd Mon Sep 17 00:00:00 2001 From: Brian Chen Date: Sat, 13 Oct 2018 14:20:51 +0200 Subject: [PATCH 2/6] Update README.md and build new rule --- README.md | 1 + recommended_ruleset.js | 1 + tslint-warnings.csv | 1 + 3 files changed, 3 insertions(+) diff --git a/README.md b/README.md index 17a1ead97..d2bdb14fa 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,7 @@ Rule Name | Description | Since `react-a11y-event-has-role` | For accessibility of your website, Elements with event handlers must have explicit role or implicit role.
References:
[WCAG Rule 94](http://oaa-accessibility.org/wcag20/rule/94/)
[Using the button role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role) | 2.0.11 `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 `react-a11y-img-has-alt` | Enforce that an `img` element contains the `alt` attribute or `role='presentation'` for a decorative image. All images must have `alt` text to convey their purpose and meaning to **screen reader users**. Besides, the `alt` attribute specifies an alternate text for an image, if the image cannot be displayed. This rule accepts as a parameter a string array for tag names other than img to also check. For example, if you use a custom tag named 'Image' then configure the rule with: `[true, ['Image']]`
References:
[Web Content Accessibility Guidelines 1.0](https://www.w3.org/TR/WCAG10/wai-pageauth.html#tech-text-equivalent)
[ARIA Presentation Role](https://www.w3.org/TR/wai-aria/roles#presentation)
[WCAG Rule 31: If an image has an alt or title attribute, it should not have a presentation role](http://oaa-accessibility.org/wcag20/rule/31/) | 2.0.11 +`react-a11y-input-elements` | For accessibility of your website, HTML input boxes and text areas must include default, place-holding characters.
References:
* [WCAG 10.4](https://www.w3.org/TR/WAI-WEBCONTENT-TECHS/#tech-place-holders)
[WCAG 11.5](https://www.w3.org/TR/WCAG10-HTML-TECHS/#forms-specific) | 5.2.3 `react-a11y-lang` | For accessibility of your website, HTML elements must have a lang attribute and the attribute must be a valid language code.
References:
* [H58: Using language attributes to identify changes in the human language](https://www.w3.org/TR/WCAG20-TECHS/H58.html)
* [lang attribute must have a valid value](https://dequeuniversity.com/rules/axe/1.1/valid-lang)
[List of ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) | 2.0.11 `react-a11y-meta` | For accessibility of your website, HTML meta elements must not have http-equiv="refresh". | 2.0.11 `react-a11y-props` | For accessibility of your website, enforce all `aria-*` attributes are valid. Elements cannot use an invalid `aria-*` attribute. This rule will fail if it finds an `aria-*` attribute that is not listed in [WAI-ARIA states and properties](https://www.w3.org/WAI/PF/aria/states_and_properties#state_prop_values). | 2.0.11 diff --git a/recommended_ruleset.js b/recommended_ruleset.js index 7f4612cc1..0a7cb2570 100644 --- a/recommended_ruleset.js +++ b/recommended_ruleset.js @@ -186,6 +186,7 @@ module.exports = { "react-a11y-event-has-role": true, "react-a11y-image-button-has-alt": true, "react-a11y-img-has-alt": true, + "react-a11y-input-elements": true, "react-a11y-lang": true, "react-a11y-meta": true, "react-a11y-props": true, diff --git a/tslint-warnings.csv b/tslint-warnings.csv index 90531cb2d..a162e5924 100644 --- a/tslint-warnings.csv +++ b/tslint-warnings.csv @@ -257,6 +257,7 @@ react-a11y-aria-unsupported-elements,"Enforce that elements that do not support react-a11y-event-has-role,Elements with event handlers must have role attribute.,TSLINT18MKF94,tslint,Non-SDL,Warning,Important,Opportunity for Excellence,See description on the tslint or tslint-microsoft-contrib website,TSLint Procedure,, react-a11y-image-button-has-alt,Enforce that inputs element with type="image" must have alt attribute.,TSLINTVBN64L,tslint,Non-SDL,Warning,Important,Opportunity for Excellence,See description on the tslint or tslint-microsoft-contrib website,TSLint Procedure,, react-a11y-img-has-alt,"Enforce that an img element contains the non-empty alt attribute. For decorative images, using empty alt attribute and role="presentation".",TSLINT1OM69KS,tslint,Non-SDL,Warning,Important,Opportunity for Excellence,See description on the tslint or tslint-microsoft-contrib website,TSLint Procedure,, +react-a11y-input-elements,"For accessibility of your website, HTML input boxes and text areas must include default, place-holding characters.",TSLINTT7DC6U,tslint,Non-SDL,Warning,Moderate,Opportunity for Excellence,See description on the tslint or tslint-microsoft-contrib website,TSLint Procedure,, react-a11y-lang,"For accessibility of your website, html elements must have a valid lang attribute.",TSLINTQ046RM,tslint,Non-SDL,Warning,Low,Opportunity for Excellence,See description on the tslint or tslint-microsoft-contrib website,TSLint Procedure,, react-a11y-props,Enforce all `aria-*` attributes are valid. Elements cannot use an invalid `aria-*` attribute.,TSLINT1682S78,tslint,Non-SDL,Warning,Important,Opportunity for Excellence,See description on the tslint or tslint-microsoft-contrib website,TSLint Procedure,, react-a11y-proptypes,Enforce ARIA state and property values are valid.,TSLINT1DLB1JE,tslint,Non-SDL,Warning,Important,Opportunity for Excellence,See description on the tslint or tslint-microsoft-contrib website,TSLint Procedure,, From 07c967c8f7f574bf6e320bb5f33eb993a5f6cbc7 Mon Sep 17 00:00:00 2001 From: Brian Chen Date: Tue, 16 Oct 2018 22:40:48 +0200 Subject: [PATCH 3/6] Use getJsxAttributesFromJsxElement only for input elements --- src/reactA11yInputElementsRule.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/reactA11yInputElementsRule.ts b/src/reactA11yInputElementsRule.ts index 2e3cf010d..efead1beb 100644 --- a/src/reactA11yInputElementsRule.ts +++ b/src/reactA11yInputElementsRule.ts @@ -42,13 +42,14 @@ class ReactA11yInputElementsRuleWalker extends ErrorTolerantWalker { protected visitJsxSelfClosingElement(node: ts.JsxSelfClosingElement): void { const tagName = node.tagName.getText(); - const attributes: { [propName: string]: ts.JsxAttribute } = getJsxAttributesFromJsxElement(node); if (tagName === 'input') { + const attributes = getJsxAttributesFromJsxElement(node); if (isEmpty(attributes.value) && isEmpty(attributes.placeholder)) { this.addFailureAt(node.getStart(), node.getWidth(), MISSING_PLACEHOLDER_INPUT_FAILURE_STRING); } } else if (tagName === 'textarea') { + const attributes = getJsxAttributesFromJsxElement(node); if (isEmpty(attributes.placeholder)) { this.addFailureAt(node.getStart(), node.getWidth(), MISSING_PLACEHOLDER_TEXTAREA_FAILURE_STRING); } From 6a064fe8483aecd3422d37ceb9eede984930280c Mon Sep 17 00:00:00 2001 From: Brian Chen Date: Tue, 16 Oct 2018 22:41:37 +0200 Subject: [PATCH 4/6] Revert tslint.json formator changes --- tslint.json | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/tslint.json b/tslint.json index 361ade4b1..00d80d5ba 100644 --- a/tslint.json +++ b/tslint.json @@ -3,10 +3,7 @@ "dist/src" ], "rules": { - "array-type": [ - true, - "array" - ], + "array-type": [true,"array"], "arrow-return-shorthand": false, "await-promise": false, "adjacent-overload-signatures": false, @@ -145,9 +142,7 @@ "no-mergeable-namespace": false, "no-missing-visibility-modifiers": false, "no-misused-new": true, - "no-multiline-string": [ - false - ], + "no-multiline-string": [ false ], "no-multiple-var-decl": false, "no-namespace": false, "no-null-keyword": false, @@ -206,11 +201,7 @@ "check-whitespace" ], "one-variable-per-declaration": true, - "only-arrow-functions": [ - true, - "allow-declarations", - "allow-named-functions" - ], + "only-arrow-functions": [true, "allow-declarations", "allow-named-functions"], "ordered-imports": [ false ], @@ -321,4 +312,4 @@ "use-default-type-parameter": false, "react-a11y-input-elements": true } -} \ No newline at end of file +} From 377b56de0548218a2a6a5ec48e6c7ae6669fcc31 Mon Sep 17 00:00:00 2001 From: Brian Chen Date: Tue, 16 Oct 2018 22:43:31 +0200 Subject: [PATCH 5/6] Fix missing revert tslint changes --- tslint.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tslint.json b/tslint.json index 00d80d5ba..5289a58df 100644 --- a/tslint.json +++ b/tslint.json @@ -3,7 +3,7 @@ "dist/src" ], "rules": { - "array-type": [true,"array"], + "array-type": [true, "array"], "arrow-return-shorthand": false, "await-promise": false, "adjacent-overload-signatures": false, From 95d099c7df86e123de589736368b9e3782f2ef6d Mon Sep 17 00:00:00 2001 From: Brian Chen Date: Tue, 16 Oct 2018 22:56:28 +0200 Subject: [PATCH 6/6] Update rule and tests with latest master changes --- src/reactA11yInputElementsRule.ts | 2 +- src/tests/ReactA11yInputElementsRuleTests.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/reactA11yInputElementsRule.ts b/src/reactA11yInputElementsRule.ts index efead1beb..fb3cd0471 100644 --- a/src/reactA11yInputElementsRule.ts +++ b/src/reactA11yInputElementsRule.ts @@ -19,7 +19,7 @@ export class Rule extends Lint.Rules.AbstractRule { ruleName: 'react-a11y-input-elements', type: 'functionality', description: 'For accessibility of your website, HTML input boxes and text areas must include default, place-holding characters.', - options: null, + options: undefined, optionsDescription: '', typescriptOnly: true, issueClass: 'Non-SDL', diff --git a/src/tests/ReactA11yInputElementsRuleTests.ts b/src/tests/ReactA11yInputElementsRuleTests.ts index 48b09305e..674c7be55 100644 --- a/src/tests/ReactA11yInputElementsRuleTests.ts +++ b/src/tests/ReactA11yInputElementsRuleTests.ts @@ -1,3 +1,4 @@ +import {Utils} from '../utils/Utils'; import {TestHelper} from './TestHelper'; import { MISSING_PLACEHOLDER_INPUT_FAILURE_STRING, @@ -34,22 +35,21 @@ describe('reactA11yInputElementsRule', () : void => { TestHelper.assertViolations(ruleName, script, [ { "failure": MISSING_PLACEHOLDER_INPUT_FAILURE_STRING, - "name": "file.tsx", + "name": Utils.absolutePath("file.tsx"), "ruleName": "react-a11y-input-elements", "startPosition": { "character": 23, "line": 3 } }, { "failure": MISSING_PLACEHOLDER_TEXTAREA_FAILURE_STRING, - "name": "file.tsx", + "name": Utils.absolutePath("file.tsx"), "ruleName": "react-a11y-input-elements", "startPosition": { "character": 23, "line": 4 } }, { "failure": MISSING_PLACEHOLDER_TEXTAREA_FAILURE_STRING, - "name": "file.tsx", + "name": Utils.absolutePath("file.tsx"), "ruleName": "react-a11y-input-elements", - "startPosition": { "character": 23, "line": 5 } - } + "startPosition": { "character": 23, "line": 5 }} ]); });