Skip to content
This repository has been archived by the owner on Jul 15, 2023. It is now read-only.

Mouse events have key events rule #849

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,15 @@ We recommend you specify exact versions of lint libraries, including `tslint-mic
</td>
<td>2.0.11</td>
</tr>
<tr>
<td>
<code>react-a11y-mouse-event-has-key-event</code>
</td>
<td>
For accessibility of your website, elements with mouseOver/mouseOut should be accompanied by onFocus/onBlur keyboard events.
</td>
<td>@next</td>
</tr>
<tr>
<td>
<code>react-a11y-no-onchange</code>
Expand Down
1 change: 1 addition & 0 deletions configs/latest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"rulesDirectory": ["../"],
"rules": {
"react-a11y-iframes": true,
"react-a11y-mouse-event-has-key-event": true,
"void-zero": true
}
}
109 changes: 109 additions & 0 deletions src/reactA11yMouseEventHasKeyEventRule.ts
Original file line number Diff line number Diff line change
@@ -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';
};
} = {
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
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 of your website, elements with mouseOver/mouseOut should be accompanied by onFocus/onBlur keyboard events.',
rationale: `References:
<ul>
<li><a href="http://oaa-accessibility.org/wcag20/rule/59/">Focusable elements with mouseOver should also have onFocus event handlers.</a></li>
<li><a href="http://oaa-accessibility.org/wcag20/rule/60/">Focusable elements with onMouseOut should also have onBlur event handlers.</a></li>
</ul>`,
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<void>) {
function cb(node: ts.Node): void {
function checkMouseEventForFocus(
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
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);
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
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);
}
11 changes: 11 additions & 0 deletions tests/react-a11y-mouse-event-has-key-event/test.tsx.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as React from 'react'
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved

const element = (<div onMouseOver={() => {}}>click</div>)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [onMouseOver must be accompanied by onFocus.]
const elementTwo = (<div onMouseOut={() => {}}>click</div>)
~~~~~~~~~~~~~~~~~~~~~~~~~~~ [onMouseOut must be accompanied by onBlur.]
const elementSelfClosing = (<div onMouseOut={() => {}}/>)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [onMouseOut must be accompanied by onBlur.]

const elementWithFocus = <div onMouseOut={() => {}} onBlur={() => {}}>click</div>
const elementWithFocusTwo = <div onMouseOver={() => {}} onFocus={() => {}}>click</div>
5 changes: 5 additions & 0 deletions tests/react-a11y-mouse-event-has-key-event/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"react-a11y-mouse-event-has-key-event": true
}
}
1 change: 1 addition & 0 deletions tslint-warnings.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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,,
Expand Down
1 change: 1 addition & 0 deletions tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,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,
Expand Down