From 212df0ea28eaf594a8e849a9d67e95ed80055d30 Mon Sep 17 00:00:00 2001 From: lizzzp1 Date: Sun, 10 Mar 2019 19:41:43 -0400 Subject: [PATCH 1/8] mouse event has key event rule --- recommended_ruleset.js | 1 + src/reactA11yMouseEventHasKeyEventRule.ts | 109 ++++++++++++++++++ .../test.tsx.lint | 11 ++ .../tslint.json | 5 + tslint.json | 1 + 5 files changed, 127 insertions(+) create mode 100644 src/reactA11yMouseEventHasKeyEventRule.ts create mode 100644 tests/react-a11y-mouse-event-has-key-event/test.tsx.lint create mode 100644 tests/react-a11y-mouse-event-has-key-event/tslint.json diff --git a/recommended_ruleset.js b/recommended_ruleset.js index 4d523fdbf..9fa162dff 100644 --- a/recommended_ruleset.js +++ b/recommended_ruleset.js @@ -195,6 +195,7 @@ module.exports = { 'react-a11y-input-elements': true, 'react-a11y-lang': true, 'react-a11y-meta': true, + "react-a11y-mouse-event-has-key-event": true, 'react-a11y-no-onchange': true, 'react-a11y-props': true, 'react-a11y-proptypes': true, diff --git a/src/reactA11yMouseEventHasKeyEventRule.ts b/src/reactA11yMouseEventHasKeyEventRule.ts new file mode 100644 index 000000000..298c2ee88 --- /dev/null +++ b/src/reactA11yMouseEventHasKeyEventRule.ts @@ -0,0 +1,109 @@ +import * as ts from 'typescript'; +import * as Lint from 'tslint'; +import * as tsutils from 'tsutils'; + +import { ExtendedMetadata } from './utils/ExtendedMetadata'; +import { getJsxAttributesFromJsxElement } from './utils/JsxAttribute'; + +const MOUSE_EVENTS: { + onMouseOver: { + value: 'onmouseover'; + jsxValue: 'onMouseOver'; + }; + onMouseOut: { + value: 'onmouseout'; + jsxValue: 'onMouseOut'; + }; +} = { + onMouseOver: { + value: 'onmouseover', + jsxValue: 'onMouseOver' + }, + onMouseOut: { + value: 'onmouseout', + jsxValue: 'onMouseOut' + } +}; + +const FOCUS_EVENTS: { + onFocus: { + value: 'onfocus'; + jsxValue: 'onFocus'; + }; + onBlur: { + value: 'onblur'; + jsxValue: 'onBlur'; + }; +} = { + onFocus: { + value: 'onfocus', + jsxValue: 'onFocus' + }, + onBlur: { + value: 'onblur', + jsxValue: 'onBlur' + } +}; + +type MouseEvents = keyof typeof MOUSE_EVENTS; +type FocusEvents = keyof typeof FOCUS_EVENTS; +type AttributeType = { [propName: string]: ts.JsxAttribute }; + +function getFailureString(mouseEvent: MouseEvents, focusEvent: FocusEvents) { + return `${mouseEvent} must be accompanied by ${focusEvent}.`; +} + +export class Rule extends Lint.Rules.AbstractRule { + public static metadata: ExtendedMetadata = { + ruleName: 'react-a11y-mouse-event-has-key-event', + type: 'maintainability', + description: + 'For accessibility purposes, elements with mouseOver/mouseOut should be accompanied by onFocus/onBlur keyboard events.', + rationale: `References: + `, + options: null, // tslint:disable-line:no-null-keyword + optionsDescription: '', + typescriptOnly: true, + issueClass: 'Non-SDL', + issueType: 'Error', + severity: 'Important', + level: 'Opportunity for Excellence', + group: 'Accessibility' + }; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithFunction(sourceFile, walk); + } +} + +function walk(ctx: Lint.WalkContext) { + function cb(node: ts.Node): void { + function checkMouseEventForFocus( + mouseEvent: typeof MOUSE_EVENTS.onMouseOver | typeof MOUSE_EVENTS.onMouseOut, + focusEvent: typeof FOCUS_EVENTS.onBlur | typeof FOCUS_EVENTS.onFocus + ): void { + const attributes: AttributeType = getJsxAttributesFromJsxElement(node); + + if (attributes === undefined) { + return; + } + + const attributeKeys = Object.keys(attributes); + if (attributeKeys.indexOf(mouseEvent.value) !== -1 && attributeKeys.indexOf(focusEvent.value) === -1) { + const errorMessage = getFailureString(mouseEvent.jsxValue, focusEvent.jsxValue); + ctx.addFailureAt(node.getStart(), node.getWidth(), errorMessage); + } + } + + if (tsutils.isJsxSelfClosingElement(node) || tsutils.isJsxOpeningElement(node)) { + checkMouseEventForFocus(MOUSE_EVENTS.onMouseOver, FOCUS_EVENTS.onFocus); + checkMouseEventForFocus(MOUSE_EVENTS.onMouseOut, FOCUS_EVENTS.onBlur); + } + return ts.forEachChild(node, cb); + } + + return ts.forEachChild(ctx.sourceFile, cb); +} diff --git a/tests/react-a11y-mouse-event-has-key-event/test.tsx.lint b/tests/react-a11y-mouse-event-has-key-event/test.tsx.lint new file mode 100644 index 000000000..ad06b743e --- /dev/null +++ b/tests/react-a11y-mouse-event-has-key-event/test.tsx.lint @@ -0,0 +1,11 @@ +import * as React from 'react' + +const element = (
{}}>click
) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [onMouseOver must be accompanied by onFocus.] +const elementTwo = (
{}}>click
) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ [onMouseOut must be accompanied by onBlur.] +const elementSelfClosing = (
{}}/>) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [onMouseOut must be accompanied by onBlur.] + +const elementWithFocus =
{}} onBlur={() => {}}>click
+const elementWithFocusTwo =
{}} onFocus={() => {}}>click
\ No newline at end of file diff --git a/tests/react-a11y-mouse-event-has-key-event/tslint.json b/tests/react-a11y-mouse-event-has-key-event/tslint.json new file mode 100644 index 000000000..e5ad4ec34 --- /dev/null +++ b/tests/react-a11y-mouse-event-has-key-event/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "react-a11y-mouse-event-has-key-event": true + } +} diff --git a/tslint.json b/tslint.json index 7ef1eb817..2814fc8cb 100644 --- a/tslint.json +++ b/tslint.json @@ -131,6 +131,7 @@ "react-a11y-input-elements": true, "react-a11y-lang": true, "react-a11y-meta": true, + "react-a11y-mouse-event-has-key-event": true, "react-a11y-no-onchange": true, "react-a11y-props": true, "react-a11y-proptypes": true, From 8ee316095fc2516c0fa96b3bd6c45e9b59772316 Mon Sep 17 00:00:00 2001 From: lizzzp1 Date: Wed, 13 Mar 2019 20:20:41 -0400 Subject: [PATCH 2/8] add readme bit --- README.md | 9 +++++++++ src/reactA11yMouseEventHasKeyEventRule.ts | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ea6689a38..d1a14e7d5 100644 --- a/README.md +++ b/README.md @@ -938,6 +938,15 @@ We recommend you specify exact versions of lint libraries, including `tslint-mic 2.0.11 + + + react-a11y-mouse-event-has-key-event + + + For accessibility of your website, elements with mouseOver/mouseOut should be accompanied by onFocus/onBlur keyboard events. + + 6.1.0 + react-a11y-no-onchange diff --git a/src/reactA11yMouseEventHasKeyEventRule.ts b/src/reactA11yMouseEventHasKeyEventRule.ts index 298c2ee88..df751d111 100644 --- a/src/reactA11yMouseEventHasKeyEventRule.ts +++ b/src/reactA11yMouseEventHasKeyEventRule.ts @@ -58,11 +58,11 @@ export class Rule extends Lint.Rules.AbstractRule { ruleName: 'react-a11y-mouse-event-has-key-event', type: 'maintainability', description: - 'For accessibility purposes, elements with mouseOver/mouseOut should be accompanied by onFocus/onBlur keyboard events.', + 'For accessibility of your website, elements with mouseOver/mouseOut should be accompanied by onFocus/onBlur keyboard events.', rationale: `References: `, options: null, // tslint:disable-line:no-null-keyword optionsDescription: '', From f4df07544f6944e68dbbae195db45364fecc2821 Mon Sep 17 00:00:00 2001 From: lizzzp1 Date: Sat, 16 Mar 2019 15:22:47 -0400 Subject: [PATCH 3/8] remove conflicting file --- recommended_ruleset.js | 284 ----------------------------------------- 1 file changed, 284 deletions(-) delete mode 100644 recommended_ruleset.js diff --git a/recommended_ruleset.js b/recommended_ruleset.js deleted file mode 100644 index 9fa162dff..000000000 --- a/recommended_ruleset.js +++ /dev/null @@ -1,284 +0,0 @@ -/** - * These rule settings are a broad, general recommendation for a good default configuration. - * This file is exported in the npm/nuget package as ./tslint.json. - */ -module.exports = { - 'rules': { - /** - * Security Rules. The following rules should be turned on because they find security issues - * or are recommended in the Microsoft Secure Development Lifecycle (SDL) - */ - 'function-constructor': true, - 'insecure-random': true, - 'no-banned-terms': true, - 'no-cookies': true, - 'no-delete-expression': true, - 'no-disable-auto-sanitization': true, - 'no-document-domain': true, - 'no-document-write': true, - 'no-eval': true, - 'no-exec-script': true, - 'no-function-constructor-with-string-args': false, // use tslint function-constructor rule intsead - 'no-http-string': [true, 'http://www.example.com/?.*', 'http://localhost:?.*'], - 'no-inner-html': true, - 'no-octal-literal': true, - 'no-string-based-set-immediate': true, - 'no-string-based-set-interval': true, - 'no-string-based-set-timeout': true, - 'non-literal-fs-path': true, - 'non-literal-require': true, - 'possible-timing-attack': true, - 'react-anchor-blank-noopener': true, - 'react-iframe-missing-sandbox': true, - 'react-no-dangerous-html': true, - - /** - * Common Bugs and Correctness. The following rules should be turned on because they find - * common bug patterns in the code or enforce type safety. - */ - 'await-promise': true, - 'forin': true, - 'increment-decrement': true, - 'jquery-deferred-must-complete': true, - 'label-position': true, - 'match-default-export-name': true, - 'mocha-avoid-only': true, - 'mocha-no-side-effect-code': true, - 'no-any': true, - 'no-arg': true, - 'no-backbone-get-set-outside-model': true, - 'no-bitwise': true, - 'no-conditional-assignment': true, - 'no-console': [true, 'debug', 'info', 'error', 'log', 'time', 'timeEnd', 'trace'], - 'no-constant-condition': true, - 'no-control-regex': true, - 'no-debugger': true, - 'no-default-import': false, - 'no-duplicate-super': true, - 'no-duplicate-switch-case': true, - 'no-duplicate-variable': true, - 'no-empty': true, - 'no-floating-promises': true, - 'no-for-in-array': true, - 'no-implicit-dependencies': true, - 'no-import-side-effect': true, - 'no-increment-decrement': false, // use tslint increment-decrement rule instead - 'no-invalid-regexp': true, - 'no-invalid-template-strings': true, - 'no-invalid-this': true, - 'no-jquery-raw-elements': true, - 'no-misused-new': true, - 'no-non-null-assertion': true, - 'no-object-literal-type-assertion': true, - 'no-parameter-reassignment': true, - 'no-reference-import': true, - 'no-regex-spaces': true, - 'no-sparse-arrays': true, - 'no-string-literal': true, - 'no-string-throw': true, - 'no-submodule-imports': true, - 'no-unnecessary-bind': false, // use tslint unnecessary-bind rule instead - 'no-unnecessary-callback-wrapper': true, - 'no-unnecessary-initializer': true, - 'no-unnecessary-override': true, - 'no-unsafe-any': true, - 'no-unsafe-finally': true, - 'no-unused-expression': true, - 'no-use-before-declare': true, - 'no-with-statement': true, - 'promise-function-async': true, - 'promise-must-complete': true, - 'radix': true, - 'react-this-binding-issue': true, - 'react-unused-props-and-state': true, - 'restrict-plus-operands': true, // the plus operand should really only be used for strings and numbers - 'strict-boolean-expressions': true, - 'switch-default': true, - 'switch-final-break': true, - 'triple-equals': [true, 'allow-null-check'], - 'unnecessary-bind': true, - 'unnecessary-constructor': true, - 'use-isnan': true, - 'use-named-parameter': true, - 'use-simple-attributes': true, - - /** - * Code Clarity. The following rules should be turned on because they make the code - * generally more clear to the reader. - */ - 'adjacent-overload-signatures': true, - 'array-type': [true, 'array'], - 'arrow-parens': false, // for simple functions the parens on arrow functions are not needed - 'ban-comma-operator': true, // possibly controversial - 'binary-expression-operand-order': true, - 'callable-types': true, - 'chai-prefer-contains-to-index-of': true, - 'chai-vague-errors': true, - 'class-name': true, - 'comment-format': true, - 'comment-type': false, - 'completed-docs': [true, 'classes'], - 'export-name': true, - 'file-name-casing': true, - 'function-name': true, - 'import-name': true, - 'informative-docs': true, - 'interface-name': true, - 'jsdoc-format': true, - 'max-classes-per-file': [true, 3], // we generally recommend making one public class per file - 'max-file-line-count': true, - 'max-func-body-length': [true, 100, { 'ignore-parameters-to-function-regex': '^describe$' }], - 'max-line-length': [true, 140], - 'member-access': true, - 'member-ordering': [true, { 'order': 'fields-first' }], - 'mocha-unneeded-done': true, - 'new-parens': true, - 'newline-per-chained-call': true, - 'no-construct': true, - 'no-default-export': true, - 'no-duplicate-imports': true, - 'no-dynamic-delete': true, - 'no-empty-interface': true, - 'no-for-in': true, - 'no-function-expression': true, - 'no-inferrable-types': false, // turn no-inferrable-types off in order to make the code consistent in its use of type decorations - 'no-multiline-string': false, - 'no-null-keyword': true, - 'no-parameter-properties': true, - 'no-redundant-jsdoc': true, - 'no-relative-imports': true, - 'no-require-imports': true, - 'no-return-await': true, - 'no-shadowed-variable': true, - 'no-suspicious-comment': true, - 'no-this-assignment': true, - 'no-typeof-undefined': true, - 'no-unnecessary-field-initialization': true, - 'no-unnecessary-local-variable': true, - 'no-unnecessary-qualifier': true, - 'no-unnecessary-type-assertion': true, - 'no-unsupported-browser-code': true, - 'no-useless-files': true, - 'no-var-keyword': true, - 'no-var-requires': true, - 'no-void-expression': true, - 'number-literal-format': true, - 'object-literal-sort-keys': false, // turn object-literal-sort-keys off and sort keys in a meaningful manner - 'one-variable-per-declaration': true, - 'only-arrow-functions': false, // there are many valid reasons to declare a function - 'ordered-imports': true, - 'prefer-array-literal': true, - 'prefer-const': true, - 'prefer-for-of': true, - 'prefer-method-signature': true, - 'prefer-object-spread': true, - 'prefer-readonly': true, - 'prefer-template': true, - 'prefer-while': true, - 'type-literal-delimiter': true, - 'typedef': [true, 'call-signature', 'arrow-call-signature', 'parameter', 'arrow-parameter', 'property-declaration', 'variable-declaration', 'member-variable-declaration'], - 'underscore-consistent-invocation': true, - 'unified-signatures': true, - 'use-default-type-parameter': true, - 'variable-name': true, - - /** - * Accessibility. The following rules should be turned on to guarantee the best user - * experience for keyboard and screen reader users. - */ - 'react-a11y-accessible-headings': true, - 'react-a11y-anchors': true, - 'react-a11y-aria-unsupported-elements': true, - '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-mouse-event-has-key-event": true, - 'react-a11y-no-onchange': true, - 'react-a11y-props': true, - 'react-a11y-proptypes': true, - 'react-a11y-required': true, - 'react-a11y-role': true, - 'react-a11y-role-has-required-aria-props': true, - 'react-a11y-role-supports-aria-props': true, - 'react-a11y-tabindex-no-positive': true, - 'react-a11y-titles': true, - - /** - * Whitespace related rules. The only recommended whitespace strategy is to pick a single format and - * be consistent. - */ - 'align': [true, 'parameters', 'arguments', 'statements'], - 'curly': true, - 'encoding': true, - 'eofline': true, - 'import-spacing': true, - 'indent': [true, 'spaces'], - 'linebreak-style': true, - 'newline-before-return': true, - 'no-consecutive-blank-lines': true, - 'no-empty-line-after-opening-brace': false, - 'no-irregular-whitespace': true, - 'no-single-line-block-comment': true, - 'no-trailing-whitespace': true, - 'no-unnecessary-semicolons': true, - 'object-literal-key-quotes': [true, 'as-needed'], - 'one-line': [true, 'check-open-brace', 'check-catch', 'check-else', 'check-whitespace'], - 'quotemark': [true, 'single'], - 'semicolon': [true, 'always'], - 'space-within-parens': true, - 'trailing-comma': [true, { 'singleline': 'never', 'multiline': 'never' }], // forcing trailing commas for multi-line - // lists results in lists that are easier to reorder and version control diffs that are more clear. - // Many teams like to have multiline be 'always'. There is no clear consensus on this rule but the - // internal MS JavaScript coding standard does discourage it. - 'typedef-whitespace': false, - 'whitespace': [true, 'check-branch', 'check-decl', 'check-operator', 'check-separator', 'check-type'], - - /** - * Controversial/Configurable rules. - */ - 'ban': false, // only enable this if you have some code pattern that you want to ban - 'ban-ts-ignore': false, - 'ban-types': true, - 'cyclomatic-complexity': true, - 'deprecation': false, // deprecated APIs are sometimes unavoidable - 'file-header': false, // enable this rule only if you are legally required to add a file header - 'import-blacklist': false, // enable and configure this as you desire - 'interface-over-type-literal': false, // there are plenty of reasons to prefer interfaces - 'no-angle-bracket-type-assertion': false, // pick either type-cast format and use it consistently - 'no-inferred-empty-object-type': false, // if the compiler is satisfied then this is probably not an issue - 'no-internal-module': false, // only enable this if you are not using internal modules - 'no-magic-numbers': false, // by default it will find too many false positives - 'no-mergeable-namespace': false, // your project may require mergeable namespaces - 'no-namespace': false, // only enable this if you are not using modules/namespaces - 'no-reference': true, // in general you should use a module system and not /// reference imports - 'no-unexternalized-strings': false, // the VS Code team has a specific localization process that this rule enforces - 'object-literal-shorthand': false, // object-literal-shorthand offers an abbreviation not an abstraction - 'prefer-conditional-expression': false, // unnecessarily strict - 'prefer-switch': false, // more of a style preference - 'prefer-type-cast': true, // pick either type-cast format and use it consistently - 'return-undefined': false, // this actually affects the readability of the code - 'space-before-function-paren': false, // turn this on if this is really your coding standard - - /** - * Deprecated rules. The following rules are deprecated for various reasons. - */ - 'missing-jsdoc': false, - 'missing-optional-annotation': false, // now supported by TypeScript compiler - 'no-duplicate-case': false, - 'no-duplicate-parameter-names': false, // now supported by TypeScript compiler - 'no-empty-interfaces': false, // use tslint no-empty-interface rule instead - 'no-missing-visibility-modifiers': false, // use tslint member-access rule instead - 'no-multiple-var-decl': false, // use tslint one-variable-per-declaration rule instead - 'no-reserved-keywords': false, - 'no-stateless-class': false, - 'no-switch-case-fall-through': false, // now supported by TypeScript compiler - 'no-unnecessary-class': true, - 'no-var-self': false, - 'react-tsx-curly-spacing': false, - 'typeof-compare': false, // the valid-typeof rule is currently superior to this version - 'valid-typeof': false, - } -}; From 6d337ac5ad2b8b71cab80362c9ba20f9656e25ec Mon Sep 17 00:00:00 2001 From: lizzzp1 Date: Sat, 16 Mar 2019 15:41:46 -0400 Subject: [PATCH 4/8] @next version, add csv --- README.md | 2 +- configs/latest.json | 1 + tslint-warnings.csv | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0fad4df49..c766a9851 100644 --- a/README.md +++ b/README.md @@ -992,7 +992,7 @@ We recommend you specify exact versions of lint libraries, including `tslint-mic For accessibility of your website, elements with mouseOver/mouseOut should be accompanied by onFocus/onBlur keyboard events. - 6.1.0 + @next diff --git a/configs/latest.json b/configs/latest.json index 8e9892034..3ba4abbd6 100644 --- a/configs/latest.json +++ b/configs/latest.json @@ -3,6 +3,7 @@ "rulesDirectory": ["../"], "rules": { "react-a11y-iframes": true, + "react-a11y-mouse-event-has-key-event": true, "void-zero": true } } diff --git a/tslint-warnings.csv b/tslint-warnings.csv index feb041d08..082bb0691 100644 --- a/tslint-warnings.csv +++ b/tslint-warnings.csv @@ -266,6 +266,7 @@ react-a11y-image-button-has-alt,Enforce that inputs element with type="image" mu 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-mouse-event-has-key-event,"For accessibility of your website, elements with mouseOver/mouseOut should be accompanied by onFocus/onBlur keyboard events.",TSLINT2DDJKM,tslint,Non-SDL,Error,Important,Opportunity for Excellence,See description on the tslint or tslint-microsoft-contrib website,TSLint Procedure,, react-a11y-no-onchange,"For accessibility of your website, enforce usage of onBlur over onChange on select menus.",TSLINTNO0TDD,tslint,Non-SDL,Warning,Important,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 e1208a78a75a0066275225c90e0572a55d59c9b6 Mon Sep 17 00:00:00 2001 From: lizzzp1 Date: Thu, 21 Mar 2019 08:31:47 -0400 Subject: [PATCH 5/8] use set for faster look up, add more test cases --- src/reactA11yMouseEventHasKeyEventRule.ts | 4 ++-- tests/react-a11y-mouse-event-has-key-event/test.tsx.lint | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/reactA11yMouseEventHasKeyEventRule.ts b/src/reactA11yMouseEventHasKeyEventRule.ts index df751d111..842e316a9 100644 --- a/src/reactA11yMouseEventHasKeyEventRule.ts +++ b/src/reactA11yMouseEventHasKeyEventRule.ts @@ -91,8 +91,8 @@ function walk(ctx: Lint.WalkContext) { return; } - const attributeKeys = Object.keys(attributes); - if (attributeKeys.indexOf(mouseEvent.value) !== -1 && attributeKeys.indexOf(focusEvent.value) === -1) { + const attributeKeys = new Set(Object.keys(attributes)); + if (attributeKeys.has(mouseEvent.value) && !attributeKeys.has(focusEvent.value)) { const errorMessage = getFailureString(mouseEvent.jsxValue, focusEvent.jsxValue); ctx.addFailureAt(node.getStart(), node.getWidth(), errorMessage); } diff --git a/tests/react-a11y-mouse-event-has-key-event/test.tsx.lint b/tests/react-a11y-mouse-event-has-key-event/test.tsx.lint index ad06b743e..9a1d65e42 100644 --- a/tests/react-a11y-mouse-event-has-key-event/test.tsx.lint +++ b/tests/react-a11y-mouse-event-has-key-event/test.tsx.lint @@ -7,5 +7,9 @@ const elementTwo = (
{}}>click
) const elementSelfClosing = (
{}}/>) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [onMouseOut must be accompanied by onBlur.] +
{}} {...props} /> +
{}} {...props} /> +
+
const elementWithFocus =
{}} onBlur={() => {}}>click
const elementWithFocusTwo =
{}} onFocus={() => {}}>click
\ No newline at end of file From 5b7b44218998a97fa46dbc16c4b882cd576bf10e Mon Sep 17 00:00:00 2001 From: lizzzp1 Date: Fri, 22 Mar 2019 08:20:37 -0400 Subject: [PATCH 6/8] declare function outside cb --- src/reactA11yMouseEventHasKeyEventRule.ts | 40 ++++++++++++----------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/reactA11yMouseEventHasKeyEventRule.ts b/src/reactA11yMouseEventHasKeyEventRule.ts index 842e316a9..e3dead33f 100644 --- a/src/reactA11yMouseEventHasKeyEventRule.ts +++ b/src/reactA11yMouseEventHasKeyEventRule.ts @@ -48,6 +48,12 @@ const FOCUS_EVENTS: { type MouseEvents = keyof typeof MOUSE_EVENTS; type FocusEvents = keyof typeof FOCUS_EVENTS; type AttributeType = { [propName: string]: ts.JsxAttribute }; +interface CheckMouseEventArgs { + mouseEvent: typeof MOUSE_EVENTS.onMouseOver | typeof MOUSE_EVENTS.onMouseOut; + focusEvent: typeof FOCUS_EVENTS.onBlur | typeof FOCUS_EVENTS.onFocus; + node: ts.Node; + ctx: Lint.WalkContext; +} function getFailureString(mouseEvent: MouseEvents, focusEvent: FocusEvents) { return `${mouseEvent} must be accompanied by ${focusEvent}.`; @@ -78,29 +84,25 @@ export class Rule extends Lint.Rules.AbstractRule { return this.applyWithFunction(sourceFile, walk); } } +function checkMouseEventForFocus({ mouseEvent, focusEvent, node, ctx }: CheckMouseEventArgs): void { + const attributes: AttributeType = getJsxAttributesFromJsxElement(node); -function walk(ctx: Lint.WalkContext) { - function cb(node: ts.Node): void { - function checkMouseEventForFocus( - mouseEvent: typeof MOUSE_EVENTS.onMouseOver | typeof MOUSE_EVENTS.onMouseOut, - focusEvent: typeof FOCUS_EVENTS.onBlur | typeof FOCUS_EVENTS.onFocus - ): void { - const attributes: AttributeType = getJsxAttributesFromJsxElement(node); - - if (attributes === undefined) { - return; - } + if (attributes === undefined) { + return; + } - const attributeKeys = new Set(Object.keys(attributes)); - if (attributeKeys.has(mouseEvent.value) && !attributeKeys.has(focusEvent.value)) { - const errorMessage = getFailureString(mouseEvent.jsxValue, focusEvent.jsxValue); - ctx.addFailureAt(node.getStart(), node.getWidth(), errorMessage); - } - } + const attributeKeys = new Set(Object.keys(attributes)); + if (attributeKeys.has(mouseEvent.value) && !attributeKeys.has(focusEvent.value)) { + const errorMessage = getFailureString(mouseEvent.jsxValue, focusEvent.jsxValue); + ctx.addFailureAt(node.getStart(), node.getWidth(), errorMessage); + } +} +function walk(ctx: Lint.WalkContext) { + function cb(node: ts.Node): void { if (tsutils.isJsxSelfClosingElement(node) || tsutils.isJsxOpeningElement(node)) { - checkMouseEventForFocus(MOUSE_EVENTS.onMouseOver, FOCUS_EVENTS.onFocus); - checkMouseEventForFocus(MOUSE_EVENTS.onMouseOut, FOCUS_EVENTS.onBlur); + checkMouseEventForFocus({ mouseEvent: MOUSE_EVENTS.onMouseOver, focusEvent: FOCUS_EVENTS.onFocus, node, ctx }); + checkMouseEventForFocus({ mouseEvent: MOUSE_EVENTS.onMouseOut, focusEvent: FOCUS_EVENTS.onBlur, node, ctx }); } return ts.forEachChild(node, cb); } From a3d6833a3bc08d9cf14bdf0e79b4d6cca19938ee Mon Sep 17 00:00:00 2001 From: lizzzp1 Date: Thu, 4 Apr 2019 20:32:50 -0400 Subject: [PATCH 7/8] fix invalid tsx --- .../react-a11y-mouse-event-has-key-event/test.tsx.lint | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/react-a11y-mouse-event-has-key-event/test.tsx.lint b/tests/react-a11y-mouse-event-has-key-event/test.tsx.lint index 9a1d65e42..169f85617 100644 --- a/tests/react-a11y-mouse-event-has-key-event/test.tsx.lint +++ b/tests/react-a11y-mouse-event-has-key-event/test.tsx.lint @@ -7,9 +7,11 @@ const elementTwo = (
{}}>click
) const elementSelfClosing = (
{}}/>) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [onMouseOut must be accompanied by onBlur.] -
{}} {...props} /> -
{}} {...props} /> -
-
+const Foo = (
{}} {...props} />) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [onMouseOver must be accompanied by onFocus.] +const Bar = (
{}} {...props} />) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [onMouseOut must be accompanied by onBlur.] +const Baz =
+const Foobar =
const elementWithFocus =
{}} onBlur={() => {}}>click
const elementWithFocusTwo =
{}} onFocus={() => {}}>click
\ No newline at end of file From 7b65818fb3767d0300fa73477cef055413116ce3 Mon Sep 17 00:00:00 2001 From: lizzzp1 Date: Mon, 22 Apr 2019 08:09:53 -0400 Subject: [PATCH 8/8] ignore spread attributes --- src/reactA11yMouseEventHasKeyEventRule.ts | 13 ++++++++++++- .../test.tsx.lint | 2 -- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/reactA11yMouseEventHasKeyEventRule.ts b/src/reactA11yMouseEventHasKeyEventRule.ts index e3dead33f..50de81b8f 100644 --- a/src/reactA11yMouseEventHasKeyEventRule.ts +++ b/src/reactA11yMouseEventHasKeyEventRule.ts @@ -3,7 +3,8 @@ import * as Lint from 'tslint'; import * as tsutils from 'tsutils'; import { ExtendedMetadata } from './utils/ExtendedMetadata'; -import { getJsxAttributesFromJsxElement } from './utils/JsxAttribute'; +import { getAllAttributesFromJsxElement, getJsxAttributesFromJsxElement } from './utils/JsxAttribute'; +import { isJsxSpreadAttribute } from './utils/TypeGuard'; const MOUSE_EVENTS: { onMouseOver: { @@ -84,6 +85,12 @@ export class Rule extends Lint.Rules.AbstractRule { return this.applyWithFunction(sourceFile, walk); } } + +function isSpreadAttribute(node: ts.Node): boolean { + const nodeAttributes = getAllAttributesFromJsxElement(node); + return nodeAttributes !== undefined && nodeAttributes.some(isJsxSpreadAttribute); +} + function checkMouseEventForFocus({ mouseEvent, focusEvent, node, ctx }: CheckMouseEventArgs): void { const attributes: AttributeType = getJsxAttributesFromJsxElement(node); @@ -91,6 +98,10 @@ function checkMouseEventForFocus({ mouseEvent, focusEvent, node, ctx }: CheckMou return; } + if (isSpreadAttribute(node)) { + return; + } + const attributeKeys = new Set(Object.keys(attributes)); if (attributeKeys.has(mouseEvent.value) && !attributeKeys.has(focusEvent.value)) { const errorMessage = getFailureString(mouseEvent.jsxValue, focusEvent.jsxValue); diff --git a/tests/react-a11y-mouse-event-has-key-event/test.tsx.lint b/tests/react-a11y-mouse-event-has-key-event/test.tsx.lint index 169f85617..f79e8fb21 100644 --- a/tests/react-a11y-mouse-event-has-key-event/test.tsx.lint +++ b/tests/react-a11y-mouse-event-has-key-event/test.tsx.lint @@ -8,9 +8,7 @@ const elementSelfClosing = (
{}}/>) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [onMouseOut must be accompanied by onBlur.] const Foo = (
{}} {...props} />) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [onMouseOver must be accompanied by onFocus.] const Bar = (
{}} {...props} />) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [onMouseOut must be accompanied by onBlur.] const Baz =
const Foobar =
const elementWithFocus =
{}} onBlur={() => {}}>click