From db64898fa591f17827053ad3c2ddeafdf7297dd6 Mon Sep 17 00:00:00 2001 From: Willy Liu Date: Wed, 29 Mar 2023 14:30:22 +0100 Subject: [PATCH] [New] `mouse-events-have-key-events`: add `hoverInHandlers`/`hoverOutHandlers` config --- .../mouse-events-have-key-events-test.js | 76 +++++++++++++++++++ docs/rules/mouse-events-have-key-events.md | 30 +++++++- src/rules/mouse-events-have-key-events.js | 59 +++++++++----- 3 files changed, 146 insertions(+), 19 deletions(-) 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: ' {}} {...props} />' }, { code: ' {}} {...props} />' }, { code: ' {}} {...props} />' }, + /* Passing in empty options doesn't check any event handlers */ + { + code: '
{}} onMouseOut={() => {}} />', + options: [{ hoverInHandlers: [], hoverOutHandlers: [] }], + }, + /* Passing in custom handlers */ + { + code: '
{}} onFocus={() => {}} />', + options: [{ hoverInHandlers: ['onMouseOver'] }], + }, + { + code: '
{}} onFocus={() => {}} />', + options: [{ hoverInHandlers: ['onMouseEnter'] }], + }, + { + code: '
{}} onBlur={() => {}} />', + options: [{ hoverOutHandlers: ['onMouseOut'] }], + }, + { + code: '
{}} onBlur={() => {}} />', + options: [{ hoverOutHandlers: ['onMouseLeave'] }], + }, + { + code: '
{}} onMouseOut={() => {}} />', + options: [ + { hoverInHandlers: ['onPointerEnter'], hoverOutHandlers: ['onPointerLeave'] }, + ], + }, + /* Custom options only checks the handlers passed in */ + { + code: '
{}} />', + options: [{ hoverOutHandlers: ['onPointerLeave'] }], + }, ].map(parserOptionsMapper), invalid: [ { code: '
void 0} />;', errors: [mouseOverError] }, @@ -73,5 +114,40 @@ ruleTester.run('mouse-events-have-key-events', rule, { code: '
void 0} {...props} />', errors: [mouseOutError], }, + /* Custom options */ + { + code: '
{}} onMouseOut={() => {}} />', + options: [ + { hoverInHandlers: ['onMouseOver'], hoverOutHandlers: ['onMouseOut'] }, + ], + errors: [mouseOverError, mouseOutError], + }, + { + code: '
{}} onPointerLeave={() => {}} />', + options: [ + { hoverInHandlers: ['onPointerEnter'], hoverOutHandlers: ['onPointerLeave'] }, + ], + errors: [pointerEnterError, pointerLeaveError], + }, + { + code: '
{}} />', + options: [{ hoverInHandlers: ['onMouseOver'] }], + errors: [mouseOverError], + }, + { + code: '
{}} />', + options: [{ hoverInHandlers: ['onPointerEnter'] }], + errors: [pointerEnterError], + }, + { + code: '
{}} />', + options: [{ hoverOutHandlers: ['onMouseOut'] }], + errors: [mouseOutError], + }, + { + code: '
{}} />', + options: [{ hoverOutHandlers: ['onPointerLeave'] }], + errors: [pointerLeaveError], + }, ].map(parserOptionsMapper), }); 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);