This repository has been archived by the owner on Jul 15, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 198
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Mouse events have key events rule (#849)
* mouse event has key event rule * add readme bit * remove conflicting file * @next version, add csv * use set for faster look up, add more test cases * declare function outside cb * fix invalid tsx * ignore spread attributes
- Loading branch information
Liz
authored and
Josh Goldberg
committed
Apr 22, 2019
1 parent
542c89a
commit f3229e0
Showing
7 changed files
with
154 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | ||
}; | ||
} = { | ||
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import * as React from 'react' | ||
|
||
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"rules": { | ||
"react-a11y-mouse-event-has-key-event": true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters