Skip to content

Commit db64898

Browse files
Willy Liuljharb
Willy Liu
authored andcommittedApr 3, 2023
[New] mouse-events-have-key-events: add hoverInHandlers/hoverOutHandlers config
1 parent 93f7885 commit db64898

File tree

3 files changed

+146
-19
lines changed

3 files changed

+146
-19
lines changed
 

‎__tests__/src/rules/mouse-events-have-key-events-test.js

+76
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,18 @@ const mouseOverError = {
2222
message: 'onMouseOver must be accompanied by onFocus for accessibility.',
2323
type: 'JSXOpeningElement',
2424
};
25+
const pointerEnterError = {
26+
message: 'onPointerEnter must be accompanied by onFocus for accessibility.',
27+
type: 'JSXOpeningElement',
28+
};
2529
const mouseOutError = {
2630
message: 'onMouseOut must be accompanied by onBlur for accessibility.',
2731
type: 'JSXOpeningElement',
2832
};
33+
const pointerLeaveError = {
34+
message: 'onPointerLeave must be accompanied by onBlur for accessibility.',
35+
type: 'JSXOpeningElement',
36+
};
2937

3038
ruleTester.run('mouse-events-have-key-events', rule, {
3139
valid: [
@@ -53,6 +61,39 @@ ruleTester.run('mouse-events-have-key-events', rule, {
5361
{ code: '<MyElement onMouseOut={() => {}} {...props} />' },
5462
{ code: '<MyElement onBlur={() => {}} {...props} />' },
5563
{ code: '<MyElement onFocus={() => {}} {...props} />' },
64+
/* Passing in empty options doesn't check any event handlers */
65+
{
66+
code: '<div onMouseOver={() => {}} onMouseOut={() => {}} />',
67+
options: [{ hoverInHandlers: [], hoverOutHandlers: [] }],
68+
},
69+
/* Passing in custom handlers */
70+
{
71+
code: '<div onMouseOver={() => {}} onFocus={() => {}} />',
72+
options: [{ hoverInHandlers: ['onMouseOver'] }],
73+
},
74+
{
75+
code: '<div onMouseEnter={() => {}} onFocus={() => {}} />',
76+
options: [{ hoverInHandlers: ['onMouseEnter'] }],
77+
},
78+
{
79+
code: '<div onMouseOut={() => {}} onBlur={() => {}} />',
80+
options: [{ hoverOutHandlers: ['onMouseOut'] }],
81+
},
82+
{
83+
code: '<div onMouseLeave={() => {}} onBlur={() => {}} />',
84+
options: [{ hoverOutHandlers: ['onMouseLeave'] }],
85+
},
86+
{
87+
code: '<div onMouseOver={() => {}} onMouseOut={() => {}} />',
88+
options: [
89+
{ hoverInHandlers: ['onPointerEnter'], hoverOutHandlers: ['onPointerLeave'] },
90+
],
91+
},
92+
/* Custom options only checks the handlers passed in */
93+
{
94+
code: '<div onMouseLeave={() => {}} />',
95+
options: [{ hoverOutHandlers: ['onPointerLeave'] }],
96+
},
5697
].map(parserOptionsMapper),
5798
invalid: [
5899
{ code: '<div onMouseOver={() => void 0} />;', errors: [mouseOverError] },
@@ -73,5 +114,40 @@ ruleTester.run('mouse-events-have-key-events', rule, {
73114
code: '<div onMouseOut={() => void 0} {...props} />',
74115
errors: [mouseOutError],
75116
},
117+
/* Custom options */
118+
{
119+
code: '<div onMouseOver={() => {}} onMouseOut={() => {}} />',
120+
options: [
121+
{ hoverInHandlers: ['onMouseOver'], hoverOutHandlers: ['onMouseOut'] },
122+
],
123+
errors: [mouseOverError, mouseOutError],
124+
},
125+
{
126+
code: '<div onPointerEnter={() => {}} onPointerLeave={() => {}} />',
127+
options: [
128+
{ hoverInHandlers: ['onPointerEnter'], hoverOutHandlers: ['onPointerLeave'] },
129+
],
130+
errors: [pointerEnterError, pointerLeaveError],
131+
},
132+
{
133+
code: '<div onMouseOver={() => {}} />',
134+
options: [{ hoverInHandlers: ['onMouseOver'] }],
135+
errors: [mouseOverError],
136+
},
137+
{
138+
code: '<div onPointerEnter={() => {}} />',
139+
options: [{ hoverInHandlers: ['onPointerEnter'] }],
140+
errors: [pointerEnterError],
141+
},
142+
{
143+
code: '<div onMouseOut={() => {}} />',
144+
options: [{ hoverOutHandlers: ['onMouseOut'] }],
145+
errors: [mouseOutError],
146+
},
147+
{
148+
code: '<div onPointerLeave={() => {}} />',
149+
options: [{ hoverOutHandlers: ['onPointerLeave'] }],
150+
errors: [pointerLeaveError],
151+
},
76152
].map(parserOptionsMapper),
77153
});

‎docs/rules/mouse-events-have-key-events.md

+28-2
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,35 @@
66

77
Enforce onmouseover/onmouseout are accompanied by onfocus/onblur. Coding for the keyboard is important for users with physical disabilities who cannot use a mouse, AT compatibility, and screenreader users.
88

9-
## Rule details
9+
## Rule options
10+
11+
By default, this rule checks that `onmouseover` is paired with `onfocus` and that `onmouseout` is paired with `onblur`. This rule takes an optional argument to specify other handlers to check for "hover in" and/or "hover out" events:
12+
13+
```json
14+
{
15+
"rules": {
16+
"jsx-a11y/mouse-events-have-key-events": [
17+
"error",
18+
{
19+
"hoverInHandlers": [
20+
"onMouseOver",
21+
"onMouseEnter",
22+
"onPointerOver",
23+
"onPointerEnter"
24+
],
25+
"hoverOutHandlers": [
26+
"onMouseOut",
27+
"onMouseLeave",
28+
"onPointerOut",
29+
"onPointerLeave"
30+
]
31+
}
32+
]
33+
}
34+
}
35+
```
1036

11-
This rule takes no arguments.
37+
Note that while `onmouseover` and `onmouseout` are checked by default if no arguments are passed in, those are *not* included by default if you *do* provide an argument, so remember to explicitly include them if you want to check them.
1238

1339
### Succeed
1440
```jsx

‎src/rules/mouse-events-have-key-events.js

+42-17
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @fileoverview Enforce onmouseover/onmouseout are
33
* accompanied by onfocus/onblur.
44
* @author Ethan Cohen
5+
* @flow
56
*/
67

78
// ----------------------------------------------------------------------------
@@ -10,14 +11,26 @@
1011

1112
import { dom } from 'aria-query';
1213
import { getProp, getPropValue } from 'jsx-ast-utils';
13-
import { generateObjSchema } from '../util/schemas';
14+
import { arraySchema, generateObjSchema } from '../util/schemas';
15+
import type { ESLintConfig, ESLintContext } from '../../flow/eslint';
1416

15-
const mouseOverErrorMessage = 'onMouseOver must be accompanied by onFocus for accessibility.';
16-
const mouseOutErrorMessage = 'onMouseOut must be accompanied by onBlur for accessibility.';
17+
const schema = generateObjSchema({
18+
hoverInHandlers: {
19+
...arraySchema,
20+
description: 'An array of events that need to be accompanied by `onFocus`',
21+
},
22+
hoverOutHandlers: {
23+
...arraySchema,
24+
description: 'An array of events that need to be accompanied by `onBlur`',
25+
},
26+
});
1727

18-
const schema = generateObjSchema();
28+
// Use `onMouseOver` and `onMouseOut` by default if no config is
29+
// passed in for backwards compatibility
30+
const DEFAULT_HOVER_IN_HANDLERS = ['onMouseOver'];
31+
const DEFAULT_HOVER_OUT_HANDLERS = ['onMouseOut'];
1932

20-
export default {
33+
export default ({
2134
meta: {
2235
docs: {
2336
url: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/mouse-events-have-key-events.md',
@@ -26,46 +39,58 @@ export default {
2639
schema: [schema],
2740
},
2841

29-
create: (context) => ({
42+
create: (context: ESLintContext) => ({
3043
JSXOpeningElement: (node) => {
3144
const { name } = node.name;
3245

3346
if (!dom.get(name)) {
3447
return;
3548
}
3649

50+
const { options } = context;
51+
52+
const hoverInHandlers: string[] = options[0]?.hoverInHandlers ?? DEFAULT_HOVER_IN_HANDLERS;
53+
const hoverOutHandlers: string[] = options[0]?.hoverOutHandlers ?? DEFAULT_HOVER_OUT_HANDLERS;
54+
3755
const { attributes } = node;
3856

39-
// Check onmouseover / onfocus pairing.
40-
const onMouseOver = getProp(attributes, 'onMouseOver');
41-
const onMouseOverValue = getPropValue(onMouseOver);
57+
// Check hover in / onfocus pairing
58+
const firstHoverInHandlerWithValue = hoverInHandlers.find((handler) => {
59+
const prop = getProp(attributes, handler);
60+
const propValue = getPropValue(prop);
61+
return propValue != null;
62+
});
4263

43-
if (onMouseOver && onMouseOverValue != null) {
64+
if (firstHoverInHandlerWithValue != null) {
4465
const hasOnFocus = getProp(attributes, 'onFocus');
4566
const onFocusValue = getPropValue(hasOnFocus);
4667

4768
if (hasOnFocus === false || onFocusValue === null || onFocusValue === undefined) {
4869
context.report({
4970
node,
50-
message: mouseOverErrorMessage,
71+
message: `${firstHoverInHandlerWithValue} must be accompanied by onFocus for accessibility.`,
5172
});
5273
}
5374
}
5475

55-
// Checkout onmouseout / onblur pairing
56-
const onMouseOut = getProp(attributes, 'onMouseOut');
57-
const onMouseOutValue = getPropValue(onMouseOut);
58-
if (onMouseOut && onMouseOutValue != null) {
76+
// Check hover out / onblur pairing
77+
const firstHoverOutHandlerWithValue = hoverOutHandlers.find((handler) => {
78+
const prop = getProp(attributes, handler);
79+
const propValue = getPropValue(prop);
80+
return propValue != null;
81+
});
82+
83+
if (firstHoverOutHandlerWithValue != null) {
5984
const hasOnBlur = getProp(attributes, 'onBlur');
6085
const onBlurValue = getPropValue(hasOnBlur);
6186

6287
if (hasOnBlur === false || onBlurValue === null || onBlurValue === undefined) {
6388
context.report({
6489
node,
65-
message: mouseOutErrorMessage,
90+
message: `${firstHoverOutHandlerWithValue} must be accompanied by onBlur for accessibility.`,
6691
});
6792
}
6893
}
6994
},
7095
}),
71-
};
96+
}: ESLintConfig);

0 commit comments

Comments
 (0)
Please sign in to comment.