Repository: jsx-eslint/eslint-plugin-jsx-a11y (JavaScript)

Issue:
Should `mouse-events-have-key-events` check for onMouseEnter/Leave, onPointerOver/Enter/Leave/Out?
As the title says, I'm wondering if the rule `mouse-events-have-key-events.js` should, in addition to only `onMouseOver` and `onMouseOut`, also check for the `onMouseEnter/Leave` and `onPointerOver/Enter/Leave/Out` events?

Some context for this question is that I want to enable `mouse-events-have-key-events` in our codebase, but it turns out we have a separate custom rule that tells you to use `onMouseEnter` instead of `onMouseOver` and `onMouseLeave` instead of `onMouseOut`, so `mouse-events-have-key-events` would effectively not do anything for us I guess...

What do we think? I'm happy to submit a PR, but wanted to ask first in case there are reasons for not supporting those additional events.

Thanks!


Discussion:
that seems reasonable to me - @jessebeach?
These options should be turned into configuration. I think we just never got around to making this rule configurable. @willwull would you like to propose a PR to make this rule configurable? https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/rules/mouse-events-have-key-events.js
@jessebeach sure thing, let me take a stab at this. I'll try to have something within a week



Patch:
diff --git a/docs/rules/mouse-events-have-key-events.md b/docs/rules/mouse-events-have-key-events.md
index c3a1ff233..a8e6ce497 100644
--- a/docs/rules/mouse-events-have-key-events.md
+++ b/docs/rules/mouse-events-have-key-events.md
@@ -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
diff --git a/src/rules/mouse-events-have-key-events.js b/src/rules/mouse-events-have-key-events.js
index 5761551f2..ce30d6025 100644
--- a/src/rules/mouse-events-have-key-events.js
+++ b/src/rules/mouse-events-have-key-events.js
@@ -2,6 +2,7 @@
  * @fileoverview Enforce onmouseover/onmouseout are
  *  accompanied by onfocus/onblur.
  * @author Ethan Cohen
+ * @flow
  */
 
 // ----------------------------------------------------------------------------
@@ -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',
@@ -26,7 +39,7 @@ export default {
     schema: [schema],
   },
 
-  create: (context) => ({
+  create: (context: ESLintContext) => ({
     JSXOpeningElement: (node) => {
       const { name } = node.name;
 
@@ -34,38 +47,50 @@ export default {
         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);


Test patch:
diff --git a/__tests__/src/rules/mouse-events-have-key-events-test.js b/__tests__/src/rules/mouse-events-have-key-events-test.js
index aaa520db8..4c1f41b44 100644
--- a/__tests__/src/rules/mouse-events-have-key-events-test.js
+++ b/__tests__/src/rules/mouse-events-have-key-events-test.js
@@ -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: [
@@ -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] },
@@ -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),
 });