diff --git a/README.md b/README.md index 50be33dd8..169f37568 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ Rule Name | Description | Since `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-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-no-onchange` | For accessibility of your website, enforce usage of onBlur over onChange on select menus. | 5.2.3 `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 `react-a11y-proptypes` | For accessibility of your website, enforce the type of aria state and property values are correct. | 2.0.11 `react-a11y-role-has-required-aria-props` | For accessibility of your website, elements with aria roles must have all required attributes according to the role.
References:
[ARIA Definition of Roles](https://www.w3.org/TR/wai-aria/roles#role_definitions)
[WCAG Rule 90: Required properties and states should be defined](http://oaa-accessibility.org/wcag20/rule/90/)
[WCAG Rule 91: Required properties and states must not be empty](http://oaa-accessibility.org/wcag20/rule/91/)
| 2.0.11 diff --git a/recommended_ruleset.js b/recommended_ruleset.js index feedb7c84..26d5071b3 100644 --- a/recommended_ruleset.js +++ b/recommended_ruleset.js @@ -188,6 +188,7 @@ module.exports = { "react-a11y-img-has-alt": true, "react-a11y-lang": true, "react-a11y-meta": true, + "react-a11y-no-onchange": true, "react-a11y-props": true, "react-a11y-proptypes": true, "react-a11y-role": true, diff --git a/src/reactA11yNoOnchangeRule.ts b/src/reactA11yNoOnchangeRule.ts new file mode 100644 index 000000000..838d2ba0b --- /dev/null +++ b/src/reactA11yNoOnchangeRule.ts @@ -0,0 +1,64 @@ +import * as ts from 'typescript'; +import * as Lint from 'tslint'; + +import {ErrorTolerantWalker} from './utils/ErrorTolerantWalker'; +import {ExtendedMetadata} from './utils/ExtendedMetadata'; +import {getJsxAttributesFromJsxElement} from './utils/JsxAttribute'; + +/** + * Implementation of the react-a11y-no-onchange rule. + */ +export class Rule extends Lint.Rules.AbstractRule { + + public static metadata: ExtendedMetadata = { + ruleName: 'react-a11y-no-onchange', + type: 'functionality', + description: 'For accessibility of your website, enforce usage of onBlur over onChange on select menus.', + options: 'string[]', + optionsDescription: 'Additional tag names to validate.', + optionExamples: ['true', '[true, ["Select"]]'], + typescriptOnly: false, + issueClass: 'Non-SDL', + issueType: 'Warning', + severity: 'Important', + level: 'Opportunity for Excellence', + group: 'Accessibility' + }; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return sourceFile.languageVariant === ts.LanguageVariant.JSX ? + this.applyWithWalker(new ReactA11yNoOnchangeRuleWalker(sourceFile, this.getOptions())) : + []; + } +} + +class ReactA11yNoOnchangeRuleWalker extends ErrorTolerantWalker { + protected visitJsxSelfClosingElement(node: ts.JsxSelfClosingElement): void { + this.checkJsxOpeningElement(node); + super.visitJsxSelfClosingElement(node); + } + + protected visitJsxElement(node: ts.JsxElement): void { + this.checkJsxOpeningElement(node.openingElement); + super.visitJsxElement(node); + } + + private checkJsxOpeningElement(node: ts.JsxOpeningLikeElement) { + const tagName: string = node.tagName.getText(); + const options: any[] = this.getOptions(); + + const additionalTagNames: string[] = options.length > 0 ? options[0] : []; + + const targetTagNames: string[] = ['select', ...additionalTagNames]; + + if (!tagName || targetTagNames.indexOf(tagName) === -1) { + return; + } + + const attributes = getJsxAttributesFromJsxElement(node); + if (attributes.hasOwnProperty('onchange')) { + const errorMessage = `onChange event handler should not be used with the <${tagName}>. Please use onBlur instead.`; + this.addFailureAt(node.getStart(), node.getWidth(), errorMessage); + } + } +} diff --git a/src/tests/ReactA11yNoOnchangeRuleTests.ts b/src/tests/ReactA11yNoOnchangeRuleTests.ts new file mode 100644 index 000000000..0fdc38039 --- /dev/null +++ b/src/tests/ReactA11yNoOnchangeRuleTests.ts @@ -0,0 +1,61 @@ +import {Utils} from '../utils/Utils'; +import {TestHelper} from './TestHelper'; + +/** + * Unit tests. + */ +describe('reactA11yNoOnchangeRule', () : void => { + const ruleName : string = 'react-a11y-no-onchange'; + const errorMessage = (tagName: string): string => + `onChange event handler should not be used with the <${tagName}>. Please use onBlur instead.`; + + it('should pass if select element attributes without onChange event', () : void => { + const script: string = ` + import React = require('react'); + const selectElement = `; + + TestHelper.assertNoViolation(ruleName, script); + }); + + it('should fail if select element attributes contains onChange event', () : void => { + const script : string = ` + import React = require('react'); + const selectElementWithOnChange = + const selectElementWithOnChange =