-
-
Notifications
You must be signed in to change notification settings - Fork 636
/
Copy pathinteractive-supports-focus.js
141 lines (133 loc) · 4.82 KB
/
interactive-supports-focus.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/**
* @fileoverview Enforce that elements with onClick handlers must be tabbable.
* @author Ethan Cohen
* @flow
*/
import {
dom,
roles,
} from 'aria-query';
import {
getProp,
eventHandlersByType,
getLiteralPropValue,
hasAnyProp,
} from 'jsx-ast-utils';
import type { JSXOpeningElement } from 'ast-types-flow';
import includes from 'array-includes';
import type { ESLintConfig, ESLintContext, ESLintVisitorSelectorConfig } from '../../flow/eslint';
import {
enumArraySchema,
generateObjSchema,
} from '../util/schemas';
import getElementType from '../util/getElementType';
import isDisabledElement from '../util/isDisabledElement';
import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';
import isInteractiveElement from '../util/isInteractiveElement';
import isInteractiveRole from '../util/isInteractiveRole';
import isNonInteractiveElement from '../util/isNonInteractiveElement';
import isNonInteractiveRole from '../util/isNonInteractiveRole';
import isPresentationRole from '../util/isPresentationRole';
import getTabIndex from '../util/getTabIndex';
// ----------------------------------------------------------------------------
// Rule Definition
// ----------------------------------------------------------------------------
const schema = generateObjSchema({
tabbable: enumArraySchema(roles.keys().filter((name) => (
!roles.get(name).abstract
&& roles.get(name).superClass.some((klasses) => includes(klasses, 'widget'))
))),
});
const interactiveProps = [].concat(
eventHandlersByType.mouse,
eventHandlersByType.keyboard,
);
export default ({
meta: {
docs: {
url: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/interactive-supports-focus.md',
description: 'Enforce that elements with interactive handlers like `onClick` must be focusable.',
},
hasSuggestions: true,
messages: {
'tabIndex=0': 'Add `tabIndex={0}` to make the element focusable in sequential keyboard navigation.',
'tabIndex=-1': 'Add `tabIndex={-1}` to make the element focusable but not reachable via sequential keyboard navigation.',
},
schema: [schema],
},
create: (context: ESLintContext): ESLintVisitorSelectorConfig => {
const elementType = getElementType(context);
return {
JSXOpeningElement: (node: JSXOpeningElement) => {
const tabbable = (
context.options && context.options[0] && context.options[0].tabbable
) || [];
const { attributes } = node;
const type = elementType(node);
const hasInteractiveProps = hasAnyProp(attributes, interactiveProps);
const hasTabindex = getTabIndex(getProp(attributes, 'tabIndex')) !== undefined;
if (!dom.has(type)) {
// Do not test higher level JSX components, as we do not know what
// low-level DOM element this maps to.
return;
}
if (
!hasInteractiveProps
|| isDisabledElement(attributes)
|| isHiddenFromScreenReader(type, attributes)
|| isPresentationRole(type, attributes)
) {
// Presentation is an intentional signal from the author that this
// element is not meant to be perceivable. For example, a click screen
// to close a dialog .
return;
}
if (
hasInteractiveProps
&& isInteractiveRole(type, attributes)
&& !isInteractiveElement(type, attributes)
&& !isNonInteractiveElement(type, attributes)
&& !isNonInteractiveRole(type, attributes)
&& !hasTabindex
) {
const role = getLiteralPropValue(getProp(attributes, 'role'));
if (includes(tabbable, role)) {
// Always tabbable, tabIndex = 0
context.report({
node,
message: `Elements with the '${role}' interactive role must be tabbable.`,
suggest: [
{
messageId: 'tabIndex=0',
fix(fixer) {
return fixer.insertTextAfter(node.name, ' tabIndex={0}');
},
},
],
});
} else {
// Focusable, tabIndex = -1 or 0
context.report({
node,
message: `Elements with the '${role}' interactive role must be focusable.`,
suggest: [
{
messageId: 'tabIndex=0',
fix(fixer) {
return fixer.insertTextAfter(node.name, ' tabIndex={0}');
},
},
{
messageId: 'tabIndex=-1',
fix(fixer) {
return fixer.insertTextAfter(node.name, ' tabIndex={-1}');
},
},
],
});
}
}
},
};
},
}: ESLintConfig);