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 all 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
}
}
122 changes: 122 additions & 0 deletions src/reactA11yMouseEventHasKeyEventRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import * as ts from 'typescript';
import * as Lint from 'tslint';
import * as tsutils from 'tsutils';

import { ExtendedMetadata } from './utils/ExtendedMetadata';
import { getAllAttributesFromJsxElement, getJsxAttributesFromJsxElement } from './utils/JsxAttribute';
import { isJsxSpreadAttribute } from './utils/TypeGuard';

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 };
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<void>;
}

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 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);

if (attributes === undefined) {
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);
ctx.addFailureAt(node.getStart(), node.getWidth(), errorMessage);
}
}

function walk(ctx: Lint.WalkContext<void>) {
function cb(node: ts.Node): void {
if (tsutils.isJsxSelfClosingElement(node) || tsutils.isJsxOpeningElement(node)) {
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);
}

return ts.forEachChild(ctx.sourceFile, cb);
}
15 changes: 15 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,15 @@
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 Foo = (<div onMouseOver={() => {}} {...props} />)
const Bar = (<div onMouseOut={() => {}} {...props} />)
const Baz = <div />
const Foobar = <div {...props} />
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