Skip to content

Commit c5e70b4

Browse files
committed
mouse-events-have-key-events
1 parent dec7fee commit c5e70b4

11 files changed

+144
-48
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ I'm currently working on getting parity between this project and `eslint-plugin-
6262
- [ ] interactive-supports-focus
6363
- [ ] label-has-for
6464
- [ ] media-has-caption
65-
- [ ] mouse-events-have-key-events
65+
- [x] mouse-events-have-key-events
6666
- [x] no-access-key
6767
- [x] no-autofocus
6868
- [x] no-distracting-elements
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# mouse-events-have-key-events
2+
3+
Enforce `@mouseenter`/`@mouseover`/`@mouseout`/`@mouseleave`/`@hover` are accompanied by `@focus`/`@blur`. Coding for the keyboard is important for users with physical disabilities who cannot use a mouse, AT compatibility, and screenreader users.
4+
5+
## Rule details
6+
7+
This rule takes no arguments.
8+
9+
### Succeed
10+
11+
```
12+
<div @mouseover="foo" @focus="bar" />
13+
<div @mouseout="foo" @blur="bar" />
14+
```
15+
16+
### Fail
17+
18+
```vue
19+
<div @mouseover="foo" />
20+
```

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module.exports = {
88
"click-events-have-key-events": require("./rules/click-events-have-key-events"),
99
"heading-has-content": require("./rules/heading-has-content"),
1010
"iframe-has-title": require("./rules/iframe-has-title"),
11+
"mouse-events-have-key-events": require("./rules/mouse-events-have-key-events"),
1112
"no-access-key": require("./rules/no-access-key"),
1213
"no-autofocus": require("./rules/no-autofocus"),
1314
"no-distracting-elements": require("./rules/no-distracting-elements"),
@@ -34,6 +35,7 @@ module.exports = {
3435
"vue-accessibility/click-events-have-key-events": "error",
3536
"vue-accessibility/heading-has-content": "error",
3637
"vue-accessibility/iframe-has-title": "error",
38+
"vue-accessibility/mouse-events-have-key-events": "error",
3739
"vue-accessibility/no-access-key": "error",
3840
"vue-accessibility/no-autofocus": "error",
3941
"vue-accessibility/no-distracting-elements": "error",

src/rules/__tests__/click-events-have-key-events.test.js

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,39 @@ const makeRuleTester = require("./makeRuleTester");
33

44
makeRuleTester("click-events-have-key-events", rule, {
55
valid: [
6-
"<div @click='foo' @keydown='bar' />",
7-
"<div @click='foo' @keyup='bar' />",
8-
"<div @click='foo' @keypress='bar' />",
9-
"<div @click='foo' @keydown='bar' @keydown='baz' />",
10-
"<div class='foo' />",
11-
"<div @click='foo' aria-hidden />",
12-
"<div @click='foo' aria-hidden='true' />",
13-
"<div @click='foo' aria-hidden='false' @keydown='bar' />",
14-
"<div @click='foo' @keydown='bar' aria-hidden='undefined' />",
15-
"<input type='text' @click='foo' />",
16-
"<input @click='foo' />",
17-
"<button @click='foo' class='foo' />",
18-
"<option @click='foo' class='foo' />",
19-
"<select @click='foo' class='foo' />",
20-
"<textarea @click='foo' class='foo' />",
21-
"<a @click='foo' href='http://x.y.z' />",
22-
"<a @click='foo' href='http://x.y.z' tabIndex='0' />",
23-
"<input @click='foo' type='hidden' />",
24-
"<div @click='foo' role='presentation' />",
25-
"<div @click='foo' role='none' />",
26-
"<TestComponent @click='foo' />",
27-
"<Button @click='foo' />"
6+
"<div @click='void 0' @keydown='void 0' />",
7+
"<div @click='void 0' @keyup='void 0' />",
8+
"<div @click='void 0' @keypress='void 0' />",
9+
"<div @click='void 0' @keydown='void 0' @keydown='baz' />",
10+
"<div class='void 0' />",
11+
"<div @click='void 0' aria-hidden />",
12+
"<div @click='void 0' aria-hidden='true' />",
13+
"<div @click='void 0' aria-hidden='false' @keydown='void 0' />",
14+
"<div @click='void 0' @keydown='void 0' aria-hidden='undefined' />",
15+
"<input type='text' @click='void 0' />",
16+
"<input @click='void 0' />",
17+
"<button @click='void 0' class='void 0' />",
18+
"<option @click='void 0' class='void 0' />",
19+
"<select @click='void 0' class='void 0' />",
20+
"<textarea @click='void 0' class='void 0' />",
21+
"<a @click='void 0' href='http://x.y.z' />",
22+
"<a @click='void 0' href='http://x.y.z' tabIndex='0' />",
23+
"<input @click='void 0' type='hidden' />",
24+
"<div @click='void 0' role='presentation' />",
25+
"<div @click='void 0' role='none' />",
26+
"<TestComponent @click='void 0' />",
27+
"<Button @click='void 0' />"
2828
],
2929
invalid: [
30-
"<div @click='foo' />",
31-
"<div @click='foo' role='undefined' />",
32-
"<section @click='foo' />",
33-
"<main @click='foo' />",
34-
"<article @click='foo' />",
35-
"<header @click='foo' />",
36-
"<footer @click='foo' />",
37-
"<div @click='foo' aria-hidden='false' />",
38-
"<a @click='foo' />",
39-
"<a tabIndex='0' @click='foo' />"
30+
"<div @click='void 0' />",
31+
"<div @click='void 0' role='undefined' />",
32+
"<section @click='void 0' />",
33+
"<main @click='void 0' />",
34+
"<article @click='void 0' />",
35+
"<header @click='void 0' />",
36+
"<footer @click='void 0' />",
37+
"<div @click='void 0' aria-hidden='false' />",
38+
"<a @click='void 0' />",
39+
"<a tabIndex='0' @click='void 0' />"
4040
]
4141
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const rule = require("../mouse-events-have-key-events");
2+
const makeRuleTester = require("./makeRuleTester");
3+
4+
makeRuleTester("mouse-events-have-key-events", rule, {
5+
valid: [
6+
"<div />",
7+
"<div @mouseover='void 0' @focus='void 0' />",
8+
"<div @mouseout='void 0' @blur='void 0' />"
9+
],
10+
invalid: [
11+
{
12+
code: "<template><div @mouseover='void 0' /></template>",
13+
errors: [{ message: rule.mouseOverErrorMessage }]
14+
},
15+
{
16+
code: "<template><div @mouseout='void 0' /></template>",
17+
errors: [{ message: rule.mouseOutErrorMessage }]
18+
},
19+
{
20+
code: "<template><div @mouseover='void 0' @focus='null' /></template>",
21+
errors: [{ message: rule.mouseOverErrorMessage }]
22+
},
23+
{
24+
code: "<template><div @mouseout='void 0' @blur='null' /></template>",
25+
errors: [{ message: rule.mouseOutErrorMessage }]
26+
}
27+
]
28+
});

src/rules/__tests__/no-onchange.test.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ const rule = require("../no-onchange");
22
const makeRuleTester = require("./makeRuleTester");
33

44
makeRuleTester("no-onchange", rule, {
5-
valid: [
6-
"<select><option @blur='handleOnBlur' @change='handleOnChange' /></select>"
7-
],
8-
invalid: ["<select @change='updateModel'><option /></select>"]
5+
valid: ["<select><option @blur='void 0' @change='void 0' /></select>"],
6+
invalid: ["<select @change='void 0'><option /></select>"]
97
});

src/rules/click-events-have-key-events.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const {
55
getElementAttribute,
66
getElementAttributeValue,
77
hasOnDirective,
8+
hasOnDirectives,
89
isHiddenFromScreenReader,
910
isInteractiveElement,
1011
makeDocsURL
@@ -26,11 +27,6 @@ const isPresentationRole = (node) => {
2627
return role && ["presentation", "none"].includes(role);
2728
};
2829

29-
const hasKeyEvent = (node) =>
30-
hasOnDirective(node, "keydown") ||
31-
hasOnDirective(node, "keyup") ||
32-
hasOnDirective(node, "keypress");
33-
3430
module.exports = {
3531
meta: {
3632
docs: {
@@ -46,7 +42,7 @@ module.exports = {
4642
!isHiddenFromScreenReader(node) &&
4743
!isPresentationRole(node) &&
4844
!isInteractiveElement(node) &&
49-
!hasKeyEvent(node)
45+
!hasOnDirectives(node, ["keydown", "keyup", "keypress"])
5046
) {
5147
context.report({ node, message });
5248
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const {
2+
defineTemplateBodyVisitor,
3+
hasOnDirectives,
4+
makeDocsURL
5+
} = require("../utils");
6+
7+
const mouseOverErrorMessage = `@mouseover, @mouseenter, or @hover must be \
8+
accompanied by @focusin or @focus for accessibility.`;
9+
10+
const mouseOutErrorMessage = `@mouseout or @mouseleave must be accompanied by \
11+
@focusout or @blur for accessibility.`;
12+
13+
module.exports = {
14+
meta: {
15+
docs: {
16+
url: makeDocsURL("mouse-events-have-key-events")
17+
}
18+
},
19+
create(context) {
20+
return defineTemplateBodyVisitor(context, {
21+
VElement(node) {
22+
if (
23+
hasOnDirectives(node, ["mouseover", "mouseenter", "hover"]) &&
24+
!hasOnDirectives(node, ["focus", "focusin"])
25+
) {
26+
context.report({ node, message: mouseOverErrorMessage });
27+
}
28+
29+
if (
30+
hasOnDirectives(node, ["mouseout", "mouseleave"]) &&
31+
!hasOnDirectives(node, ["blur", "focusout"])
32+
) {
33+
context.report({ node, message: mouseOutErrorMessage });
34+
}
35+
}
36+
});
37+
},
38+
mouseOverErrorMessage,
39+
mouseOutErrorMessage
40+
};

src/utils.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const getElementAttributeValue = require("./utils/getElementAttributeValue");
66
const getElementType = require("./utils/getElementType");
77
const hasContent = require("./utils/hasContent");
88
const hasOnDirective = require("./utils/hasOnDirective");
9+
const hasOnDirectives = require("./utils/hasOnDirectives");
910
const isAttribute = require("./utils/isAttribute");
1011
const isHiddenFromScreenReader = require("./utils/isHiddenFromScreenReader");
1112
const isInteractiveElement = require("./utils/isInteractiveElement");
@@ -58,6 +59,7 @@ module.exports = {
5859
getLiteralAttributeValue,
5960
hasContent,
6061
hasOnDirective,
62+
hasOnDirectives,
6163
isAttribute,
6264
isAttributeWithValue,
6365
isHiddenFromScreenReader,

src/utils/hasOnDirective.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
const hasOnDirective = (node, name) =>
2-
node.startTag.attributes.some(
3-
(attribute) =>
2+
node.startTag.attributes.some((attribute) => {
3+
const { key, value } = attribute;
4+
5+
return (
46
attribute.directive &&
5-
attribute.key.name.name === "on" &&
6-
attribute.key.argument.name === name
7-
);
7+
key.name.name === "on" &&
8+
key.argument.name === name &&
9+
!!value.expression.body
10+
);
11+
});
812

913
module.exports = hasOnDirective;

src/utils/hasOnDirectives.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const hasOnDirective = require("./hasOnDirective");
2+
3+
const hasOnDirectives = (node, names) =>
4+
names.some((name) => hasOnDirective(node, name));
5+
6+
module.exports = hasOnDirectives;

0 commit comments

Comments
 (0)