Skip to content

Commit

Permalink
[New] mouse-events-have-key-events: add hoverInHandlers/`hoverOut…
Browse files Browse the repository at this point in the history
…Handlers` config
  • Loading branch information
Willy Liu authored and ljharb committed Apr 3, 2023
1 parent 93f7885 commit db64898
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 19 deletions.
76 changes: 76 additions & 0 deletions __tests__/src/rules/mouse-events-have-key-events-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,18 @@ const mouseOverError = {
message: 'onMouseOver must be accompanied by onFocus for accessibility.',
type: 'JSXOpeningElement',
};
const pointerEnterError = {
message: 'onPointerEnter must be accompanied by onFocus for accessibility.',
type: 'JSXOpeningElement',
};
const mouseOutError = {
message: 'onMouseOut must be accompanied by onBlur for accessibility.',
type: 'JSXOpeningElement',
};
const pointerLeaveError = {
message: 'onPointerLeave must be accompanied by onBlur for accessibility.',
type: 'JSXOpeningElement',
};

ruleTester.run('mouse-events-have-key-events', rule, {
valid: [
Expand Down Expand Up @@ -53,6 +61,39 @@ ruleTester.run('mouse-events-have-key-events', rule, {
{ code: '<MyElement onMouseOut={() => {}} {...props} />' },
{ code: '<MyElement onBlur={() => {}} {...props} />' },
{ code: '<MyElement onFocus={() => {}} {...props} />' },
/* Passing in empty options doesn't check any event handlers */
{
code: '<div onMouseOver={() => {}} onMouseOut={() => {}} />',
options: [{ hoverInHandlers: [], hoverOutHandlers: [] }],
},
/* Passing in custom handlers */
{
code: '<div onMouseOver={() => {}} onFocus={() => {}} />',
options: [{ hoverInHandlers: ['onMouseOver'] }],
},
{
code: '<div onMouseEnter={() => {}} onFocus={() => {}} />',
options: [{ hoverInHandlers: ['onMouseEnter'] }],
},
{
code: '<div onMouseOut={() => {}} onBlur={() => {}} />',
options: [{ hoverOutHandlers: ['onMouseOut'] }],
},
{
code: '<div onMouseLeave={() => {}} onBlur={() => {}} />',
options: [{ hoverOutHandlers: ['onMouseLeave'] }],
},
{
code: '<div onMouseOver={() => {}} onMouseOut={() => {}} />',
options: [
{ hoverInHandlers: ['onPointerEnter'], hoverOutHandlers: ['onPointerLeave'] },
],
},
/* Custom options only checks the handlers passed in */
{
code: '<div onMouseLeave={() => {}} />',
options: [{ hoverOutHandlers: ['onPointerLeave'] }],
},
].map(parserOptionsMapper),
invalid: [
{ code: '<div onMouseOver={() => void 0} />;', errors: [mouseOverError] },
Expand All @@ -73,5 +114,40 @@ ruleTester.run('mouse-events-have-key-events', rule, {
code: '<div onMouseOut={() => void 0} {...props} />',
errors: [mouseOutError],
},
/* Custom options */
{
code: '<div onMouseOver={() => {}} onMouseOut={() => {}} />',
options: [
{ hoverInHandlers: ['onMouseOver'], hoverOutHandlers: ['onMouseOut'] },
],
errors: [mouseOverError, mouseOutError],
},
{
code: '<div onPointerEnter={() => {}} onPointerLeave={() => {}} />',
options: [
{ hoverInHandlers: ['onPointerEnter'], hoverOutHandlers: ['onPointerLeave'] },
],
errors: [pointerEnterError, pointerLeaveError],
},
{
code: '<div onMouseOver={() => {}} />',
options: [{ hoverInHandlers: ['onMouseOver'] }],
errors: [mouseOverError],
},
{
code: '<div onPointerEnter={() => {}} />',
options: [{ hoverInHandlers: ['onPointerEnter'] }],
errors: [pointerEnterError],
},
{
code: '<div onMouseOut={() => {}} />',
options: [{ hoverOutHandlers: ['onMouseOut'] }],
errors: [mouseOutError],
},
{
code: '<div onPointerLeave={() => {}} />',
options: [{ hoverOutHandlers: ['onPointerLeave'] }],
errors: [pointerLeaveError],
},
].map(parserOptionsMapper),
});
30 changes: 28 additions & 2 deletions docs/rules/mouse-events-have-key-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,35 @@

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.

## Rule details
## Rule options

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:

```json
{
"rules": {
"jsx-a11y/mouse-events-have-key-events": [
"error",
{
"hoverInHandlers": [
"onMouseOver",
"onMouseEnter",
"onPointerOver",
"onPointerEnter"
],
"hoverOutHandlers": [
"onMouseOut",
"onMouseLeave",
"onPointerOut",
"onPointerLeave"
]
}
]
}
}
```

This rule takes no arguments.
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.

### Succeed
```jsx
Expand Down
59 changes: 42 additions & 17 deletions src/rules/mouse-events-have-key-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* @fileoverview Enforce onmouseover/onmouseout are
* accompanied by onfocus/onblur.
* @author Ethan Cohen
* @flow
*/

// ----------------------------------------------------------------------------
Expand All @@ -10,14 +11,26 @@

import { dom } from 'aria-query';
import { getProp, getPropValue } from 'jsx-ast-utils';
import { generateObjSchema } from '../util/schemas';
import { arraySchema, generateObjSchema } from '../util/schemas';
import type { ESLintConfig, ESLintContext } from '../../flow/eslint';

const mouseOverErrorMessage = 'onMouseOver must be accompanied by onFocus for accessibility.';
const mouseOutErrorMessage = 'onMouseOut must be accompanied by onBlur for accessibility.';
const schema = generateObjSchema({
hoverInHandlers: {
...arraySchema,
description: 'An array of events that need to be accompanied by `onFocus`',
},
hoverOutHandlers: {
...arraySchema,
description: 'An array of events that need to be accompanied by `onBlur`',
},
});

const schema = generateObjSchema();
// Use `onMouseOver` and `onMouseOut` by default if no config is
// passed in for backwards compatibility
const DEFAULT_HOVER_IN_HANDLERS = ['onMouseOver'];
const DEFAULT_HOVER_OUT_HANDLERS = ['onMouseOut'];

export default {
export default ({
meta: {
docs: {
url: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/mouse-events-have-key-events.md',
Expand All @@ -26,46 +39,58 @@ export default {
schema: [schema],
},

create: (context) => ({
create: (context: ESLintContext) => ({
JSXOpeningElement: (node) => {
const { name } = node.name;

if (!dom.get(name)) {
return;
}

const { options } = context;

const hoverInHandlers: string[] = options[0]?.hoverInHandlers ?? DEFAULT_HOVER_IN_HANDLERS;
const hoverOutHandlers: string[] = options[0]?.hoverOutHandlers ?? DEFAULT_HOVER_OUT_HANDLERS;

const { attributes } = node;

// Check onmouseover / onfocus pairing.
const onMouseOver = getProp(attributes, 'onMouseOver');
const onMouseOverValue = getPropValue(onMouseOver);
// Check hover in / onfocus pairing
const firstHoverInHandlerWithValue = hoverInHandlers.find((handler) => {
const prop = getProp(attributes, handler);
const propValue = getPropValue(prop);
return propValue != null;
});

if (onMouseOver && onMouseOverValue != null) {
if (firstHoverInHandlerWithValue != null) {
const hasOnFocus = getProp(attributes, 'onFocus');
const onFocusValue = getPropValue(hasOnFocus);

if (hasOnFocus === false || onFocusValue === null || onFocusValue === undefined) {
context.report({
node,
message: mouseOverErrorMessage,
message: `${firstHoverInHandlerWithValue} must be accompanied by onFocus for accessibility.`,
});
}
}

// Checkout onmouseout / onblur pairing
const onMouseOut = getProp(attributes, 'onMouseOut');
const onMouseOutValue = getPropValue(onMouseOut);
if (onMouseOut && onMouseOutValue != null) {
// Check hover out / onblur pairing
const firstHoverOutHandlerWithValue = hoverOutHandlers.find((handler) => {
const prop = getProp(attributes, handler);
const propValue = getPropValue(prop);
return propValue != null;
});

if (firstHoverOutHandlerWithValue != null) {
const hasOnBlur = getProp(attributes, 'onBlur');
const onBlurValue = getPropValue(hasOnBlur);

if (hasOnBlur === false || onBlurValue === null || onBlurValue === undefined) {
context.report({
node,
message: mouseOutErrorMessage,
message: `${firstHoverOutHandlerWithValue} must be accompanied by onBlur for accessibility.`,
});
}
}
},
}),
};
}: ESLintConfig);

0 comments on commit db64898

Please sign in to comment.