@arcaauth/eslint-plugin-jsx-a11y 6.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.babelrc +17 -0
- package/.eslintrc +44 -0
- package/CHANGELOG.md +774 -0
- package/LICENSE.md +8 -0
- package/README.md +423 -0
- package/__mocks__/IdentifierMock.js +15 -0
- package/__mocks__/JSXAttributeMock.js +39 -0
- package/__mocks__/JSXElementMock.js +37 -0
- package/__mocks__/JSXExpressionContainerMock.js +15 -0
- package/__mocks__/JSXSpreadAttributeMock.js +18 -0
- package/__mocks__/JSXTextMock.js +17 -0
- package/__mocks__/LiteralMock.js +17 -0
- package/__mocks__/genInteractives.js +218 -0
- package/__tests__/__util__/axeMapping.js +6 -0
- package/__tests__/__util__/helpers/getESLintCoreRule.js +9 -0
- package/__tests__/__util__/helpers/parsers.js +186 -0
- package/__tests__/__util__/parserOptionsMapper.js +53 -0
- package/__tests__/__util__/ruleOptionsMapperFactory.js +33 -0
- package/__tests__/index-test.js +40 -0
- package/__tests__/src/rules/accessible-emoji-test.js +66 -0
- package/__tests__/src/rules/alt-text-test.js +291 -0
- package/__tests__/src/rules/anchor-ambiguous-text-test.js +117 -0
- package/__tests__/src/rules/anchor-has-content-test.js +54 -0
- package/__tests__/src/rules/anchor-is-valid-test.js +532 -0
- package/__tests__/src/rules/aria-activedescendant-has-tabindex-test.js +95 -0
- package/__tests__/src/rules/aria-props-test.js +69 -0
- package/__tests__/src/rules/aria-proptypes-test.js +311 -0
- package/__tests__/src/rules/aria-role-test.js +118 -0
- package/__tests__/src/rules/aria-unsupported-elements-test.js +75 -0
- package/__tests__/src/rules/autocomplete-valid-test.js +77 -0
- package/__tests__/src/rules/click-events-have-key-events-test.js +77 -0
- package/__tests__/src/rules/control-has-associated-label-test.js +327 -0
- package/__tests__/src/rules/heading-has-content-test.js +85 -0
- package/__tests__/src/rules/html-has-lang-test.js +42 -0
- package/__tests__/src/rules/iframe-has-title-test.js +55 -0
- package/__tests__/src/rules/img-redundant-alt-test.js +137 -0
- package/__tests__/src/rules/interactive-supports-focus-test.js +267 -0
- package/__tests__/src/rules/label-has-associated-control-test.js +243 -0
- package/__tests__/src/rules/label-has-for-test.js +235 -0
- package/__tests__/src/rules/lang-test.js +59 -0
- package/__tests__/src/rules/media-has-caption-test.js +220 -0
- package/__tests__/src/rules/mouse-events-have-key-events-test.js +154 -0
- package/__tests__/src/rules/no-access-key-test.js +48 -0
- package/__tests__/src/rules/no-aria-hidden-on-focusable-test.js +44 -0
- package/__tests__/src/rules/no-autofocus-test.js +68 -0
- package/__tests__/src/rules/no-distracting-elements-test.js +51 -0
- package/__tests__/src/rules/no-interactive-element-to-noninteractive-role-test.js +405 -0
- package/__tests__/src/rules/no-noninteractive-element-interactions-test.js +502 -0
- package/__tests__/src/rules/no-noninteractive-element-to-interactive-role-test.js +500 -0
- package/__tests__/src/rules/no-noninteractive-tabindex-test.js +123 -0
- package/__tests__/src/rules/no-onchange-test.js +57 -0
- package/__tests__/src/rules/no-redundant-roles-test.js +98 -0
- package/__tests__/src/rules/no-static-element-interactions-test.js +501 -0
- package/__tests__/src/rules/prefer-tag-over-role-test.js +63 -0
- package/__tests__/src/rules/role-has-required-aria-props-test.js +134 -0
- package/__tests__/src/rules/role-supports-aria-props-test.js +570 -0
- package/__tests__/src/rules/scope-test.js +50 -0
- package/__tests__/src/rules/tabindex-no-positive-test.js +55 -0
- package/__tests__/src/util/attributesComparator-test.js +91 -0
- package/__tests__/src/util/getAccessibleChildText-test.js +174 -0
- package/__tests__/src/util/getComputedRole-test.js +71 -0
- package/__tests__/src/util/getElementType-test.js +154 -0
- package/__tests__/src/util/getExplicitRole-test.js +35 -0
- package/__tests__/src/util/getImplicitRole-test.js +25 -0
- package/__tests__/src/util/getSuggestion-test.js +33 -0
- package/__tests__/src/util/getTabIndex-test.js +85 -0
- package/__tests__/src/util/hasAccessibleChild-test.js +157 -0
- package/__tests__/src/util/implicitRoles/input-test.js +87 -0
- package/__tests__/src/util/implicitRoles/menu-test.js +20 -0
- package/__tests__/src/util/implicitRoles/menuitem-test.js +38 -0
- package/__tests__/src/util/isAbstractRole-test.js +51 -0
- package/__tests__/src/util/isContentEditable-test.js +52 -0
- package/__tests__/src/util/isDOMElement-test.js +30 -0
- package/__tests__/src/util/isDisabledElement-test.js +88 -0
- package/__tests__/src/util/isFocusable-test.js +111 -0
- package/__tests__/src/util/isInteractiveElement-test.js +104 -0
- package/__tests__/src/util/isInteractiveRole-test.js +59 -0
- package/__tests__/src/util/isNonInteractiveElement-test.js +97 -0
- package/__tests__/src/util/isNonInteractiveRole-test.js +59 -0
- package/__tests__/src/util/isNonLiteralProperty-test.js +52 -0
- package/__tests__/src/util/isSemanticRoleElement-test.js +72 -0
- package/__tests__/src/util/mayContainChildComponent-test.js +219 -0
- package/__tests__/src/util/mayHaveAccessibleLabel-test.js +256 -0
- package/__tests__/src/util/parserOptionsMapper-test.js +93 -0
- package/__tests__/src/util/schemas-test.js +35 -0
- package/docs/rules/accessible-emoji.md +30 -0
- package/docs/rules/alt-text.md +168 -0
- package/docs/rules/anchor-ambiguous-text.md +91 -0
- package/docs/rules/anchor-has-content.md +64 -0
- package/docs/rules/anchor-is-valid.md +270 -0
- package/docs/rules/aria-activedescendant-has-tabindex.md +52 -0
- package/docs/rules/aria-props.md +29 -0
- package/docs/rules/aria-proptypes.md +30 -0
- package/docs/rules/aria-role.md +51 -0
- package/docs/rules/aria-unsupported-elements.md +30 -0
- package/docs/rules/autocomplete-valid.md +49 -0
- package/docs/rules/click-events-have-key-events.md +28 -0
- package/docs/rules/control-has-associated-label.md +113 -0
- package/docs/rules/heading-has-content.md +67 -0
- package/docs/rules/html-has-lang.md +31 -0
- package/docs/rules/iframe-has-title.md +37 -0
- package/docs/rules/img-redundant-alt.md +48 -0
- package/docs/rules/interactive-supports-focus.md +156 -0
- package/docs/rules/label-has-associated-control.md +152 -0
- package/docs/rules/label-has-for.md +130 -0
- package/docs/rules/lang.md +31 -0
- package/docs/rules/media-has-caption.md +48 -0
- package/docs/rules/mouse-events-have-key-events.md +58 -0
- package/docs/rules/no-access-key.md +30 -0
- package/docs/rules/no-aria-hidden-on-focusable.md +37 -0
- package/docs/rules/no-autofocus.md +43 -0
- package/docs/rules/no-distracting-elements.md +41 -0
- package/docs/rules/no-interactive-element-to-noninteractive-role.md +73 -0
- package/docs/rules/no-noninteractive-element-interactions.md +145 -0
- package/docs/rules/no-noninteractive-element-to-interactive-role.md +76 -0
- package/docs/rules/no-noninteractive-tabindex.md +115 -0
- package/docs/rules/no-onchange.md +36 -0
- package/docs/rules/no-redundant-roles.md +46 -0
- package/docs/rules/no-static-element-interactions.md +114 -0
- package/docs/rules/prefer-tag-over-role.md +32 -0
- package/docs/rules/role-has-required-aria-props.md +31 -0
- package/docs/rules/role-supports-aria-props.md +39 -0
- package/docs/rules/scope.md +30 -0
- package/docs/rules/tabindex-no-positive.md +32 -0
- package/lib/configs/flat-config-base.js +11 -0
- package/lib/configs/legacy-config-base.js +9 -0
- package/lib/index.js +209 -0
- package/lib/rules/accessible-emoji.js +63 -0
- package/lib/rules/alt-text.js +218 -0
- package/lib/rules/anchor-ambiguous-text.js +64 -0
- package/lib/rules/anchor-has-content.js +60 -0
- package/lib/rules/anchor-is-valid.js +122 -0
- package/lib/rules/aria-activedescendant-has-tabindex.js +66 -0
- package/lib/rules/aria-props.js +59 -0
- package/lib/rules/aria-proptypes.js +114 -0
- package/lib/rules/aria-role.js +89 -0
- package/lib/rules/aria-unsupported-elements.js +64 -0
- package/lib/rules/autocomplete-valid.js +67 -0
- package/lib/rules/click-events-have-key-events.js +68 -0
- package/lib/rules/control-has-associated-label.js +103 -0
- package/lib/rules/heading-has-content.js +61 -0
- package/lib/rules/html-has-lang.js +50 -0
- package/lib/rules/iframe-has-title.js +50 -0
- package/lib/rules/img-redundant-alt.js +88 -0
- package/lib/rules/interactive-supports-focus.js +87 -0
- package/lib/rules/label-has-associated-control.js +127 -0
- package/lib/rules/label-has-for.js +150 -0
- package/lib/rules/lang.js +68 -0
- package/lib/rules/media-has-caption.js +96 -0
- package/lib/rules/mouse-events-have-key-events.js +94 -0
- package/lib/rules/no-access-key.js +43 -0
- package/lib/rules/no-aria-hidden-on-focusable.js +47 -0
- package/lib/rules/no-autofocus.js +62 -0
- package/lib/rules/no-distracting-elements.js +54 -0
- package/lib/rules/no-interactive-element-to-noninteractive-role.js +81 -0
- package/lib/rules/no-noninteractive-element-interactions.js +95 -0
- package/lib/rules/no-noninteractive-element-to-interactive-role.js +80 -0
- package/lib/rules/no-noninteractive-tabindex.js +109 -0
- package/lib/rules/no-onchange.js +52 -0
- package/lib/rules/no-redundant-roles.js +86 -0
- package/lib/rules/no-static-element-interactions.js +102 -0
- package/lib/rules/prefer-tag-over-role.js +75 -0
- package/lib/rules/role-has-required-aria-props.js +88 -0
- package/lib/rules/role-supports-aria-props.js +78 -0
- package/lib/rules/scope.js +58 -0
- package/lib/rules/tabindex-no-positive.js +53 -0
- package/lib/util/attributesComparator.js +34 -0
- package/lib/util/getAccessibleChildText.js +55 -0
- package/lib/util/getComputedRole.js +19 -0
- package/lib/util/getElementType.js +30 -0
- package/lib/util/getExplicitRole.js +27 -0
- package/lib/util/getImplicitRole.js +24 -0
- package/lib/util/getSuggestion.js +32 -0
- package/lib/util/getTabIndex.js +34 -0
- package/lib/util/hasAccessibleChild.js +30 -0
- package/lib/util/implicitRoles/a.js +17 -0
- package/lib/util/implicitRoles/area.js +17 -0
- package/lib/util/implicitRoles/article.js +13 -0
- package/lib/util/implicitRoles/aside.js +13 -0
- package/lib/util/implicitRoles/body.js +13 -0
- package/lib/util/implicitRoles/button.js +13 -0
- package/lib/util/implicitRoles/datalist.js +13 -0
- package/lib/util/implicitRoles/details.js +13 -0
- package/lib/util/implicitRoles/dialog.js +13 -0
- package/lib/util/implicitRoles/form.js +13 -0
- package/lib/util/implicitRoles/h1.js +13 -0
- package/lib/util/implicitRoles/h2.js +13 -0
- package/lib/util/implicitRoles/h3.js +13 -0
- package/lib/util/implicitRoles/h4.js +13 -0
- package/lib/util/implicitRoles/h5.js +13 -0
- package/lib/util/implicitRoles/h6.js +13 -0
- package/lib/util/implicitRoles/hr.js +13 -0
- package/lib/util/implicitRoles/img.js +31 -0
- package/lib/util/implicitRoles/index.js +82 -0
- package/lib/util/implicitRoles/input.js +38 -0
- package/lib/util/implicitRoles/li.js +13 -0
- package/lib/util/implicitRoles/link.js +17 -0
- package/lib/util/implicitRoles/menu.js +19 -0
- package/lib/util/implicitRoles/menuitem.js +28 -0
- package/lib/util/implicitRoles/meter.js +13 -0
- package/lib/util/implicitRoles/nav.js +13 -0
- package/lib/util/implicitRoles/ol.js +13 -0
- package/lib/util/implicitRoles/option.js +13 -0
- package/lib/util/implicitRoles/output.js +13 -0
- package/lib/util/implicitRoles/progress.js +13 -0
- package/lib/util/implicitRoles/section.js +13 -0
- package/lib/util/implicitRoles/select.js +13 -0
- package/lib/util/implicitRoles/tbody.js +13 -0
- package/lib/util/implicitRoles/textarea.js +13 -0
- package/lib/util/implicitRoles/tfoot.js +13 -0
- package/lib/util/implicitRoles/thead.js +13 -0
- package/lib/util/implicitRoles/ul.js +13 -0
- package/lib/util/isAbstractRole.js +23 -0
- package/lib/util/isContentEditable.js +13 -0
- package/lib/util/isDOMElement.js +15 -0
- package/lib/util/isDisabledElement.js +23 -0
- package/lib/util/isFocusable.js +23 -0
- package/lib/util/isHiddenFromScreenReader.js +26 -0
- package/lib/util/isInteractiveElement.js +116 -0
- package/lib/util/isInteractiveRole.js +54 -0
- package/lib/util/isNonInteractiveElement.js +131 -0
- package/lib/util/isNonInteractiveRole.js +55 -0
- package/lib/util/isNonLiteralProperty.js +29 -0
- package/lib/util/isPresentationRole.js +13 -0
- package/lib/util/isSemanticRoleElement.js +54 -0
- package/lib/util/mayContainChildComponent.js +50 -0
- package/lib/util/mayHaveAccessibleLabel.js +95 -0
- package/lib/util/schemas.js +52 -0
- package/package.json +120 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @flow
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { dom, roles } from 'aria-query';
|
|
6
|
+
import includes from 'array-includes';
|
|
7
|
+
import fromEntries from 'object.fromentries';
|
|
8
|
+
|
|
9
|
+
import JSXAttributeMock from './JSXAttributeMock';
|
|
10
|
+
import JSXElementMock from './JSXElementMock';
|
|
11
|
+
|
|
12
|
+
import type { JSXAttributeMockType } from './JSXAttributeMock';
|
|
13
|
+
import type { JSXElementMockType } from './JSXElementMock';
|
|
14
|
+
|
|
15
|
+
const domElements = dom.keys();
|
|
16
|
+
const roleNames = roles.keys();
|
|
17
|
+
|
|
18
|
+
const interactiveElementsMap = {
|
|
19
|
+
a: [{ prop: 'href', value: '#' }],
|
|
20
|
+
area: [{ prop: 'href', value: '#' }],
|
|
21
|
+
audio: [],
|
|
22
|
+
button: [],
|
|
23
|
+
canvas: [],
|
|
24
|
+
datalist: [],
|
|
25
|
+
embed: [],
|
|
26
|
+
input: [],
|
|
27
|
+
'input[type="button"]': [{ prop: 'type', value: 'button' }],
|
|
28
|
+
'input[type="checkbox"]': [{ prop: 'type', value: 'checkbox' }],
|
|
29
|
+
'input[type="color"]': [{ prop: 'type', value: 'color' }],
|
|
30
|
+
'input[type="date"]': [{ prop: 'type', value: 'date' }],
|
|
31
|
+
'input[type="datetime"]': [{ prop: 'type', value: 'datetime' }],
|
|
32
|
+
'input[type="email"]': [{ prop: 'type', value: 'email' }],
|
|
33
|
+
'input[type="file"]': [{ prop: 'type', value: 'file' }],
|
|
34
|
+
'input[type="image"]': [{ prop: 'type', value: 'image' }],
|
|
35
|
+
'input[type="month"]': [{ prop: 'type', value: 'month' }],
|
|
36
|
+
'input[type="number"]': [{ prop: 'type', value: 'number' }],
|
|
37
|
+
'input[type="password"]': [{ prop: 'type', value: 'password' }],
|
|
38
|
+
'input[type="radio"]': [{ prop: 'type', value: 'radio' }],
|
|
39
|
+
'input[type="range"]': [{ prop: 'type', value: 'range' }],
|
|
40
|
+
'input[type="reset"]': [{ prop: 'type', value: 'reset' }],
|
|
41
|
+
'input[type="search"]': [{ prop: 'type', value: 'search' }],
|
|
42
|
+
'input[type="submit"]': [{ prop: 'type', value: 'submit' }],
|
|
43
|
+
'input[type="tel"]': [{ prop: 'type', value: 'tel' }],
|
|
44
|
+
'input[type="text"]': [{ prop: 'type', value: 'text' }],
|
|
45
|
+
'input[type="time"]': [{ prop: 'type', value: 'time' }],
|
|
46
|
+
'input[type="url"]': [{ prop: 'type', value: 'url' }],
|
|
47
|
+
'input[type="week"]': [{ prop: 'type', value: 'week' }],
|
|
48
|
+
menuitem: [],
|
|
49
|
+
option: [],
|
|
50
|
+
select: [],
|
|
51
|
+
summary: [],
|
|
52
|
+
// Whereas ARIA makes a distinction between cell and gridcell, the AXObject
|
|
53
|
+
// treats them both as CellRole and since gridcell is interactive, we consider
|
|
54
|
+
// cell interactive as well.
|
|
55
|
+
td: [],
|
|
56
|
+
th: [],
|
|
57
|
+
tr: [],
|
|
58
|
+
textarea: [],
|
|
59
|
+
video: [],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const nonInteractiveElementsMap: {[string]: Array<{[string]: string}>} = {
|
|
63
|
+
abbr: [],
|
|
64
|
+
address: [],
|
|
65
|
+
article: [],
|
|
66
|
+
aside: [],
|
|
67
|
+
blockquote: [],
|
|
68
|
+
br: [],
|
|
69
|
+
caption: [],
|
|
70
|
+
code: [],
|
|
71
|
+
dd: [],
|
|
72
|
+
del: [],
|
|
73
|
+
details: [],
|
|
74
|
+
dfn: [],
|
|
75
|
+
dialog: [],
|
|
76
|
+
dir: [],
|
|
77
|
+
dl: [],
|
|
78
|
+
dt: [],
|
|
79
|
+
em: [],
|
|
80
|
+
fieldset: [],
|
|
81
|
+
figcaption: [],
|
|
82
|
+
figure: [],
|
|
83
|
+
footer: [],
|
|
84
|
+
form: [],
|
|
85
|
+
h1: [],
|
|
86
|
+
h2: [],
|
|
87
|
+
h3: [],
|
|
88
|
+
h4: [],
|
|
89
|
+
h5: [],
|
|
90
|
+
h6: [],
|
|
91
|
+
hr: [],
|
|
92
|
+
html: [],
|
|
93
|
+
iframe: [],
|
|
94
|
+
img: [],
|
|
95
|
+
ins: [],
|
|
96
|
+
label: [],
|
|
97
|
+
legend: [],
|
|
98
|
+
li: [],
|
|
99
|
+
main: [],
|
|
100
|
+
mark: [],
|
|
101
|
+
marquee: [],
|
|
102
|
+
menu: [],
|
|
103
|
+
meter: [],
|
|
104
|
+
nav: [],
|
|
105
|
+
ol: [],
|
|
106
|
+
optgroup: [],
|
|
107
|
+
output: [],
|
|
108
|
+
p: [],
|
|
109
|
+
pre: [],
|
|
110
|
+
progress: [],
|
|
111
|
+
ruby: [],
|
|
112
|
+
'section[aria-label]': [{ prop: 'aria-label' }],
|
|
113
|
+
'section[aria-labelledby]': [{ prop: 'aria-labelledby' }],
|
|
114
|
+
strong: [],
|
|
115
|
+
sub: [],
|
|
116
|
+
sup: [],
|
|
117
|
+
table: [],
|
|
118
|
+
tbody: [],
|
|
119
|
+
tfoot: [],
|
|
120
|
+
thead: [],
|
|
121
|
+
time: [],
|
|
122
|
+
ul: [],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const indeterminantInteractiveElementsMap: { [key: string]: Array<any> } = fromEntries(domElements.map((name) => [name, []]));
|
|
126
|
+
|
|
127
|
+
Object.keys(interactiveElementsMap)
|
|
128
|
+
.concat(Object.keys(nonInteractiveElementsMap))
|
|
129
|
+
.forEach((name) => delete indeterminantInteractiveElementsMap[name]);
|
|
130
|
+
|
|
131
|
+
const abstractRoles = roleNames.filter((role) => roles.get(role).abstract);
|
|
132
|
+
|
|
133
|
+
const nonAbstractRoles = roleNames.filter((role) => !roles.get(role).abstract);
|
|
134
|
+
|
|
135
|
+
const interactiveRoles = []
|
|
136
|
+
.concat(
|
|
137
|
+
roleNames,
|
|
138
|
+
// 'toolbar' does not descend from widget, but it does support
|
|
139
|
+
// aria-activedescendant, thus in practice we treat it as a widget.
|
|
140
|
+
'toolbar',
|
|
141
|
+
)
|
|
142
|
+
.filter((role) => (
|
|
143
|
+
!roles.get(role).abstract
|
|
144
|
+
&& roles.get(role).superClass.some((klasses) => includes(klasses, 'widget'))
|
|
145
|
+
));
|
|
146
|
+
|
|
147
|
+
const nonInteractiveRoles = roleNames
|
|
148
|
+
.filter((role) => (
|
|
149
|
+
!roles.get(role).abstract
|
|
150
|
+
&& !roles.get(role).superClass.some((klasses) => includes(klasses, 'widget'))
|
|
151
|
+
|
|
152
|
+
// 'toolbar' does not descend from widget, but it does support
|
|
153
|
+
// aria-activedescendant, thus in practice we treat it as a widget.
|
|
154
|
+
&& !includes(['toolbar'], role)
|
|
155
|
+
));
|
|
156
|
+
|
|
157
|
+
export function genElementSymbol(openingElement: Object): string {
|
|
158
|
+
return (
|
|
159
|
+
openingElement.name.name + (openingElement.attributes.length > 0
|
|
160
|
+
? `${openingElement.attributes.map((attr) => `[${attr.name.name}="${attr.value.value}"]`).join('')}`
|
|
161
|
+
: ''
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function genInteractiveElements(): Array<JSXElementMockType> {
|
|
167
|
+
return Object.keys(interactiveElementsMap).map((elementSymbol: string): JSXElementMockType => {
|
|
168
|
+
const bracketIndex = elementSymbol.indexOf('[');
|
|
169
|
+
let name = elementSymbol;
|
|
170
|
+
if (bracketIndex > -1) {
|
|
171
|
+
name = elementSymbol.slice(0, bracketIndex);
|
|
172
|
+
}
|
|
173
|
+
const attributes = interactiveElementsMap[elementSymbol].map(({ prop, value }) => JSXAttributeMock(prop, value));
|
|
174
|
+
return JSXElementMock(name, attributes);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function genInteractiveRoleElements(): Array<JSXElementMockType> {
|
|
179
|
+
return interactiveRoles.concat('button article', 'fakerole button article').map((value): JSXElementMockType => JSXElementMock(
|
|
180
|
+
'div',
|
|
181
|
+
[JSXAttributeMock('role', value)],
|
|
182
|
+
));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function genNonInteractiveElements(): Array<JSXElementMockType> {
|
|
186
|
+
return Object.keys(nonInteractiveElementsMap).map((elementSymbol): JSXElementMockType => {
|
|
187
|
+
const bracketIndex = elementSymbol.indexOf('[');
|
|
188
|
+
let name = elementSymbol;
|
|
189
|
+
if (bracketIndex > -1) {
|
|
190
|
+
name = elementSymbol.slice(0, bracketIndex);
|
|
191
|
+
}
|
|
192
|
+
const attributes = nonInteractiveElementsMap[elementSymbol].map(({ prop, value }) => JSXAttributeMock(prop, value));
|
|
193
|
+
return JSXElementMock(name, attributes);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function genNonInteractiveRoleElements(): Array<JSXElementMockType> {
|
|
198
|
+
return [
|
|
199
|
+
...nonInteractiveRoles,
|
|
200
|
+
'article button',
|
|
201
|
+
'fakerole article button',
|
|
202
|
+
].map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)]));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function genAbstractRoleElements(): Array<JSXElementMockType> {
|
|
206
|
+
return abstractRoles.map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)]));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function genNonAbstractRoleElements(): Array<JSXElementMockType> {
|
|
210
|
+
return nonAbstractRoles.map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)]));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function genIndeterminantInteractiveElements(): Array<JSXElementMockType> {
|
|
214
|
+
return Object.keys(indeterminantInteractiveElementsMap).map((name) => {
|
|
215
|
+
const attributes = indeterminantInteractiveElementsMap[name].map(({ prop, value }): JSXAttributeMockType => JSXAttributeMock(prop, value));
|
|
216
|
+
return JSXElementMock(name, attributes);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { version } from 'eslint/package.json';
|
|
2
|
+
import semver from 'semver';
|
|
3
|
+
|
|
4
|
+
const isESLintV8 = semver.major(version) >= 8;
|
|
5
|
+
|
|
6
|
+
// eslint-disable-next-line global-require, import/no-dynamic-require, import/no-unresolved
|
|
7
|
+
const getESLintCoreRule = (ruleId) => (isESLintV8 ? require('eslint/use-at-your-own-risk').builtinRules.get(ruleId) : require(`eslint/lib/rules/${ruleId}`));
|
|
8
|
+
|
|
9
|
+
export default getESLintCoreRule;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import semver from 'semver';
|
|
3
|
+
import entries from 'object.entries';
|
|
4
|
+
import { version } from 'eslint/package.json';
|
|
5
|
+
import flatMap from 'array.prototype.flatmap';
|
|
6
|
+
|
|
7
|
+
let tsParserVersion;
|
|
8
|
+
try {
|
|
9
|
+
// eslint-disable-next-line import/no-unresolved, global-require
|
|
10
|
+
tsParserVersion = require('@typescript-eslint/parser/package.json').version;
|
|
11
|
+
} catch (e) { /**/ }
|
|
12
|
+
|
|
13
|
+
const disableNewTS = semver.satisfies(tsParserVersion, '>= 4.1') // this rule is not useful on v4.1+ of the TS parser
|
|
14
|
+
? (x) => ({ ...x, features: [].concat(x.features, 'no-ts-new') })
|
|
15
|
+
: (x) => x;
|
|
16
|
+
|
|
17
|
+
function minEcmaVersion(features, parserOptions) {
|
|
18
|
+
const minEcmaVersionForFeatures = {
|
|
19
|
+
'class fields': 2022,
|
|
20
|
+
'optional chaining': 2020,
|
|
21
|
+
'nullish coalescing': 2020,
|
|
22
|
+
};
|
|
23
|
+
const result = Math.max(
|
|
24
|
+
...[].concat(
|
|
25
|
+
(parserOptions && parserOptions.ecmaVersion) || [],
|
|
26
|
+
flatMap(entries(minEcmaVersionForFeatures), (entry) => {
|
|
27
|
+
const f = entry[0];
|
|
28
|
+
const y = entry[1];
|
|
29
|
+
return features.has(f) ? y : [];
|
|
30
|
+
}),
|
|
31
|
+
).map((y) => (y > 5 && y < 2015 ? y + 2009 : y)), // normalize editions to years
|
|
32
|
+
);
|
|
33
|
+
return Number.isFinite(result) ? result : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const NODE_MODULES = '../../node_modules';
|
|
37
|
+
|
|
38
|
+
const parsers = {
|
|
39
|
+
BABEL_ESLINT: path.join(__dirname, NODE_MODULES, 'babel-eslint'),
|
|
40
|
+
'@BABEL_ESLINT': path.join(__dirname, NODE_MODULES, '@babel/eslint-parser'),
|
|
41
|
+
TYPESCRIPT_ESLINT: path.join(__dirname, NODE_MODULES, 'typescript-eslint-parser'),
|
|
42
|
+
'@TYPESCRIPT_ESLINT': path.join(__dirname, NODE_MODULES, '@typescript-eslint/parser'),
|
|
43
|
+
disableNewTS,
|
|
44
|
+
babelParserOptions: function parserOptions(test, features) {
|
|
45
|
+
return {
|
|
46
|
+
...test.parserOptions,
|
|
47
|
+
requireConfigFile: false,
|
|
48
|
+
babelOptions: {
|
|
49
|
+
presets: [
|
|
50
|
+
'@babel/preset-react',
|
|
51
|
+
],
|
|
52
|
+
plugins: [
|
|
53
|
+
'@babel/plugin-syntax-do-expressions',
|
|
54
|
+
'@babel/plugin-syntax-function-bind',
|
|
55
|
+
['@babel/plugin-syntax-decorators', { legacy: true }],
|
|
56
|
+
],
|
|
57
|
+
parserOpts: {
|
|
58
|
+
allowSuperOutsideMethod: false,
|
|
59
|
+
allowReturnOutsideFunction: false,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
ecmaFeatures: {
|
|
63
|
+
|
|
64
|
+
...test.parserOptions && test.parserOptions.ecmaFeatures,
|
|
65
|
+
jsx: true,
|
|
66
|
+
modules: true,
|
|
67
|
+
legacyDecorators: features.has('decorators'),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
all: function all(tests) {
|
|
72
|
+
const t = flatMap(tests, (test) => {
|
|
73
|
+
/* eslint no-param-reassign: 0 */
|
|
74
|
+
if (typeof test === 'string') {
|
|
75
|
+
test = { code: test };
|
|
76
|
+
}
|
|
77
|
+
if ('parser' in test) {
|
|
78
|
+
delete test.features;
|
|
79
|
+
return test;
|
|
80
|
+
}
|
|
81
|
+
const features = new Set([].concat(test.features || []));
|
|
82
|
+
delete test.features;
|
|
83
|
+
|
|
84
|
+
const es = minEcmaVersion(features, test.parserOptions);
|
|
85
|
+
|
|
86
|
+
function addComment(testObject, parser) {
|
|
87
|
+
const extras = [].concat(
|
|
88
|
+
`features: [${Array.from(features).join(',')}]`,
|
|
89
|
+
`parser: ${parser}`,
|
|
90
|
+
testObject.parserOptions ? `parserOptions: ${JSON.stringify(testObject.parserOptions)}` : [],
|
|
91
|
+
testObject.options ? `options: ${JSON.stringify(testObject.options)}` : [],
|
|
92
|
+
testObject.settings ? `settings: ${JSON.stringify(testObject.settings)}` : [],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const extraComment = `\n// ${extras.join(', ')}`;
|
|
96
|
+
|
|
97
|
+
// Augment expected fix code output with extraComment
|
|
98
|
+
const nextCode = { code: testObject.code + extraComment };
|
|
99
|
+
const nextOutput = testObject.output && { output: testObject.output + extraComment };
|
|
100
|
+
|
|
101
|
+
// Augment expected suggestion outputs with extraComment
|
|
102
|
+
// `errors` may be a number (expected number of errors) or an array of
|
|
103
|
+
// error objects.
|
|
104
|
+
const nextErrors = testObject.errors
|
|
105
|
+
&& typeof testObject.errors !== 'number'
|
|
106
|
+
&& {
|
|
107
|
+
errors: testObject.errors.map(
|
|
108
|
+
(errorObject) => {
|
|
109
|
+
const nextSuggestions = errorObject.suggestions && {
|
|
110
|
+
suggestions: errorObject.suggestions.map((suggestion) => ({ ...suggestion, output: suggestion.output + extraComment })),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return { ...errorObject, ...nextSuggestions };
|
|
114
|
+
},
|
|
115
|
+
),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
|
|
120
|
+
...testObject,
|
|
121
|
+
...nextCode,
|
|
122
|
+
...nextOutput,
|
|
123
|
+
...nextErrors,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const skipBase = (features.has('class fields') && semver.satisfies(version, '< 8'))
|
|
128
|
+
|| (es >= 2020 && semver.satisfies(version, '< 6'))
|
|
129
|
+
|| features.has('no-default')
|
|
130
|
+
|| features.has('bind operator')
|
|
131
|
+
|| features.has('do expressions')
|
|
132
|
+
|| features.has('decorators')
|
|
133
|
+
|| features.has('flow')
|
|
134
|
+
|| features.has('ts')
|
|
135
|
+
|| features.has('types')
|
|
136
|
+
|| (features.has('fragment') && semver.satisfies(version, '< 5'));
|
|
137
|
+
|
|
138
|
+
const skipBabel = features.has('no-babel');
|
|
139
|
+
const skipOldBabel = skipBabel
|
|
140
|
+
|| features.has('no-babel-old')
|
|
141
|
+
|| features.has('optional chaining')
|
|
142
|
+
|| semver.satisfies(version, '>= 8');
|
|
143
|
+
const skipNewBabel = skipBabel
|
|
144
|
+
|| features.has('no-babel-new')
|
|
145
|
+
|| !semver.satisfies(version, '^7.5.0') // require('@babel/eslint-parser/package.json').peerDependencies.eslint
|
|
146
|
+
|| features.has('flow')
|
|
147
|
+
|| features.has('types')
|
|
148
|
+
|| features.has('ts');
|
|
149
|
+
const skipTS = semver.satisfies(version, '<= 5') // TODO: make these pass on eslint 5
|
|
150
|
+
|| features.has('no-ts')
|
|
151
|
+
|| features.has('flow')
|
|
152
|
+
|| features.has('jsx namespace')
|
|
153
|
+
|| features.has('bind operator')
|
|
154
|
+
|| features.has('do expressions');
|
|
155
|
+
const tsOld = !skipTS && !features.has('no-ts-old');
|
|
156
|
+
const tsNew = !skipTS && !features.has('no-ts-new');
|
|
157
|
+
|
|
158
|
+
return [].concat(
|
|
159
|
+
skipBase ? [] : addComment(
|
|
160
|
+
{
|
|
161
|
+
...test,
|
|
162
|
+
...typeof es === 'number' && {
|
|
163
|
+
parserOptions: { ...test.parserOptions, ecmaVersion: es },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
'default',
|
|
167
|
+
),
|
|
168
|
+
skipOldBabel ? [] : addComment({
|
|
169
|
+
...test,
|
|
170
|
+
parser: parsers.BABEL_ESLINT,
|
|
171
|
+
parserOptions: parsers.babelParserOptions(test, features),
|
|
172
|
+
}, 'babel-eslint'),
|
|
173
|
+
skipNewBabel ? [] : addComment({
|
|
174
|
+
...test,
|
|
175
|
+
parser: parsers['@BABEL_ESLINT'],
|
|
176
|
+
parserOptions: parsers.babelParserOptions(test, features),
|
|
177
|
+
}, '@babel/eslint-parser'),
|
|
178
|
+
tsOld ? addComment({ ...test, parser: parsers.TYPESCRIPT_ESLINT }, 'typescript-eslint') : [],
|
|
179
|
+
tsNew ? addComment({ ...test, parser: parsers['@TYPESCRIPT_ESLINT'] }, '@typescript-eslint/parser') : [],
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
return t;
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export default parsers;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { version as eslintVersion } from 'eslint/package.json';
|
|
2
|
+
import semver from 'semver';
|
|
3
|
+
|
|
4
|
+
const usingLegacy = semver.major(eslintVersion) < 9;
|
|
5
|
+
|
|
6
|
+
const defaultParserOptions = {
|
|
7
|
+
ecmaFeatures: {
|
|
8
|
+
experimentalObjectRestSpread: true,
|
|
9
|
+
jsx: true,
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const defaultLegacyParserOptions = {
|
|
14
|
+
...defaultParserOptions,
|
|
15
|
+
ecmaVersion: 2018,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const defaultLanguageOptions = {
|
|
19
|
+
ecmaVersion: 'latest',
|
|
20
|
+
parserOptions: {
|
|
21
|
+
...defaultParserOptions,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default function parserOptionsMapper({
|
|
26
|
+
code,
|
|
27
|
+
errors,
|
|
28
|
+
options = [],
|
|
29
|
+
languageOptions = {},
|
|
30
|
+
settings = {},
|
|
31
|
+
}) {
|
|
32
|
+
return usingLegacy
|
|
33
|
+
? {
|
|
34
|
+
code,
|
|
35
|
+
errors,
|
|
36
|
+
options,
|
|
37
|
+
parserOptions: {
|
|
38
|
+
...defaultLegacyParserOptions,
|
|
39
|
+
...languageOptions,
|
|
40
|
+
},
|
|
41
|
+
settings,
|
|
42
|
+
}
|
|
43
|
+
: {
|
|
44
|
+
code,
|
|
45
|
+
errors,
|
|
46
|
+
options,
|
|
47
|
+
languageOptions: {
|
|
48
|
+
...defaultLanguageOptions,
|
|
49
|
+
...languageOptions,
|
|
50
|
+
},
|
|
51
|
+
settings,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @flow
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import entries from 'object.entries';
|
|
6
|
+
import flatMap from 'array.prototype.flatmap';
|
|
7
|
+
import fromEntries from 'object.fromentries';
|
|
8
|
+
|
|
9
|
+
type ESLintTestRunnerTestCase = {
|
|
10
|
+
code: string,
|
|
11
|
+
errors: ?Array<{ message: string, type: string }>,
|
|
12
|
+
options: ?Array<mixed>,
|
|
13
|
+
parserOptions: ?Array<mixed>,
|
|
14
|
+
settings?: {[string]: mixed},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type RuleOptionsMapperFactoryType = (
|
|
18
|
+
params: ESLintTestRunnerTestCase
|
|
19
|
+
) => ESLintTestRunnerTestCase;
|
|
20
|
+
|
|
21
|
+
export default function ruleOptionsMapperFactory(ruleOptions: Array<mixed> = []): RuleOptionsMapperFactoryType {
|
|
22
|
+
// eslint-disable-next-line
|
|
23
|
+
return ({ code, errors, options, parserOptions, settings }: ESLintTestRunnerTestCase): ESLintTestRunnerTestCase => {
|
|
24
|
+
return {
|
|
25
|
+
code,
|
|
26
|
+
errors,
|
|
27
|
+
// Flatten the array of objects in an array of one object.
|
|
28
|
+
options: [fromEntries(flatMap((options || []).concat(ruleOptions), (item) => entries(item)))],
|
|
29
|
+
parserOptions,
|
|
30
|
+
settings,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* eslint global-require: 0 */
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import test from 'tape';
|
|
6
|
+
|
|
7
|
+
import plugin from '../src';
|
|
8
|
+
|
|
9
|
+
const rules = fs.readdirSync(path.resolve(__dirname, '../src/rules/'))
|
|
10
|
+
.map((f) => path.basename(f, '.js'));
|
|
11
|
+
|
|
12
|
+
test('all rule files should be exported by the plugin', (t) => {
|
|
13
|
+
rules.forEach((ruleName) => {
|
|
14
|
+
t.equal(
|
|
15
|
+
plugin.rules[ruleName],
|
|
16
|
+
require(path.join('../src/rules', ruleName)), // eslint-disable-line import/no-dynamic-require
|
|
17
|
+
`exports ${ruleName}`,
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
t.end();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('configurations', (t) => {
|
|
25
|
+
t.notEqual(plugin.configs.recommended, undefined, 'exports a \'recommended\' configuration');
|
|
26
|
+
|
|
27
|
+
t.end();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('schemas', (t) => {
|
|
31
|
+
rules.forEach((ruleName) => {
|
|
32
|
+
const rule = require(path.join('../src/rules', ruleName)); // eslint-disable-line import/no-dynamic-require
|
|
33
|
+
const schema = rule.meta && rule.meta.schema && rule.meta.schema[0];
|
|
34
|
+
const { type } = schema;
|
|
35
|
+
|
|
36
|
+
t.equal(type, 'object', `${ruleName} exports a schema with type object`);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
t.end();
|
|
40
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Enforce <marquee> elements are not used.
|
|
3
|
+
* @author Ethan Cohen
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// -----------------------------------------------------------------------------
|
|
7
|
+
// Requirements
|
|
8
|
+
// -----------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
import { RuleTester } from 'eslint';
|
|
11
|
+
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
|
|
12
|
+
import rule from '../../../src/rules/accessible-emoji';
|
|
13
|
+
import parsers from '../../__util__/helpers/parsers';
|
|
14
|
+
|
|
15
|
+
// -----------------------------------------------------------------------------
|
|
16
|
+
// Tests
|
|
17
|
+
// -----------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
const ruleTester = new RuleTester();
|
|
20
|
+
|
|
21
|
+
const expectedError = {
|
|
22
|
+
message: 'Emojis should be wrapped in <span>, have role="img", and have an accessible description with aria-label or aria-labelledby.',
|
|
23
|
+
type: 'JSXOpeningElement',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
ruleTester.run('accessible-emoji', rule, {
|
|
27
|
+
valid: parsers.all([].concat(
|
|
28
|
+
{ code: '<div />;' },
|
|
29
|
+
{ code: '<span />' },
|
|
30
|
+
{ code: '<span>No emoji here!</span>' },
|
|
31
|
+
{ code: '<span role="img" aria-label="Panda face">๐ผ</span>' },
|
|
32
|
+
{ code: '<span role="img" aria-label="Snowman">☃</span>' },
|
|
33
|
+
{ code: '<span role="img" aria-labelledby="id1">๐ผ</span>' },
|
|
34
|
+
{ code: '<span role="img" aria-labelledby="id1">☃</span>' },
|
|
35
|
+
{ code: '<span role="img" aria-labelledby="id1" aria-label="Snowman">☃</span>' },
|
|
36
|
+
{ code: '<span>{props.emoji}</span>' },
|
|
37
|
+
{ code: '<span aria-hidden>{props.emoji}</span>' },
|
|
38
|
+
{ code: '<span aria-hidden="true">๐ผ</span>' },
|
|
39
|
+
{ code: '<span aria-hidden>๐ผ</span>' },
|
|
40
|
+
{ code: '<div aria-hidden="true">๐ผ</div>' },
|
|
41
|
+
{ code: '<input type="hidden">๐ผ</input>' },
|
|
42
|
+
{
|
|
43
|
+
code: '<CustomInput type="hidden">๐ผ</CustomInput>',
|
|
44
|
+
settings: { 'jsx-a11y': { components: { CustomInput: 'input' } } },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
code: '<Box as="input" type="hidden">๐ผ</Box>',
|
|
48
|
+
settings: { 'jsx-a11y': { polymorphicPropName: 'as' } },
|
|
49
|
+
},
|
|
50
|
+
)).map(parserOptionsMapper),
|
|
51
|
+
invalid: parsers.all([].concat(
|
|
52
|
+
{ code: '<span>๐ผ</span>', errors: [expectedError] },
|
|
53
|
+
{ code: '<span>foo๐ผbar</span>', errors: [expectedError] },
|
|
54
|
+
{ code: '<span>foo ๐ผ bar</span>', errors: [expectedError] },
|
|
55
|
+
{ code: '<i role="img" aria-label="Panda face">๐ผ</i>', errors: [expectedError] },
|
|
56
|
+
{ code: '<i role="img" aria-labelledby="id1">๐ผ</i>', errors: [expectedError] },
|
|
57
|
+
{ code: '<Foo>๐ผ</Foo>', errors: [expectedError] },
|
|
58
|
+
{ code: '<span aria-hidden="false">๐ผ</span>', errors: [expectedError] },
|
|
59
|
+
{ code: '<CustomInput type="hidden">๐ผ</CustomInput>', errors: [expectedError] },
|
|
60
|
+
{
|
|
61
|
+
code: '<Box as="span">๐ผ</Box>',
|
|
62
|
+
settings: { 'jsx-a11y': { polymorphicPropName: 'as' } },
|
|
63
|
+
errors: [expectedError],
|
|
64
|
+
},
|
|
65
|
+
)).map(parserOptionsMapper),
|
|
66
|
+
});
|