@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.
Files changed (229) hide show
  1. package/.babelrc +17 -0
  2. package/.eslintrc +44 -0
  3. package/CHANGELOG.md +774 -0
  4. package/LICENSE.md +8 -0
  5. package/README.md +423 -0
  6. package/__mocks__/IdentifierMock.js +15 -0
  7. package/__mocks__/JSXAttributeMock.js +39 -0
  8. package/__mocks__/JSXElementMock.js +37 -0
  9. package/__mocks__/JSXExpressionContainerMock.js +15 -0
  10. package/__mocks__/JSXSpreadAttributeMock.js +18 -0
  11. package/__mocks__/JSXTextMock.js +17 -0
  12. package/__mocks__/LiteralMock.js +17 -0
  13. package/__mocks__/genInteractives.js +218 -0
  14. package/__tests__/__util__/axeMapping.js +6 -0
  15. package/__tests__/__util__/helpers/getESLintCoreRule.js +9 -0
  16. package/__tests__/__util__/helpers/parsers.js +186 -0
  17. package/__tests__/__util__/parserOptionsMapper.js +53 -0
  18. package/__tests__/__util__/ruleOptionsMapperFactory.js +33 -0
  19. package/__tests__/index-test.js +40 -0
  20. package/__tests__/src/rules/accessible-emoji-test.js +66 -0
  21. package/__tests__/src/rules/alt-text-test.js +291 -0
  22. package/__tests__/src/rules/anchor-ambiguous-text-test.js +117 -0
  23. package/__tests__/src/rules/anchor-has-content-test.js +54 -0
  24. package/__tests__/src/rules/anchor-is-valid-test.js +532 -0
  25. package/__tests__/src/rules/aria-activedescendant-has-tabindex-test.js +95 -0
  26. package/__tests__/src/rules/aria-props-test.js +69 -0
  27. package/__tests__/src/rules/aria-proptypes-test.js +311 -0
  28. package/__tests__/src/rules/aria-role-test.js +118 -0
  29. package/__tests__/src/rules/aria-unsupported-elements-test.js +75 -0
  30. package/__tests__/src/rules/autocomplete-valid-test.js +77 -0
  31. package/__tests__/src/rules/click-events-have-key-events-test.js +77 -0
  32. package/__tests__/src/rules/control-has-associated-label-test.js +327 -0
  33. package/__tests__/src/rules/heading-has-content-test.js +85 -0
  34. package/__tests__/src/rules/html-has-lang-test.js +42 -0
  35. package/__tests__/src/rules/iframe-has-title-test.js +55 -0
  36. package/__tests__/src/rules/img-redundant-alt-test.js +137 -0
  37. package/__tests__/src/rules/interactive-supports-focus-test.js +267 -0
  38. package/__tests__/src/rules/label-has-associated-control-test.js +243 -0
  39. package/__tests__/src/rules/label-has-for-test.js +235 -0
  40. package/__tests__/src/rules/lang-test.js +59 -0
  41. package/__tests__/src/rules/media-has-caption-test.js +220 -0
  42. package/__tests__/src/rules/mouse-events-have-key-events-test.js +154 -0
  43. package/__tests__/src/rules/no-access-key-test.js +48 -0
  44. package/__tests__/src/rules/no-aria-hidden-on-focusable-test.js +44 -0
  45. package/__tests__/src/rules/no-autofocus-test.js +68 -0
  46. package/__tests__/src/rules/no-distracting-elements-test.js +51 -0
  47. package/__tests__/src/rules/no-interactive-element-to-noninteractive-role-test.js +405 -0
  48. package/__tests__/src/rules/no-noninteractive-element-interactions-test.js +502 -0
  49. package/__tests__/src/rules/no-noninteractive-element-to-interactive-role-test.js +500 -0
  50. package/__tests__/src/rules/no-noninteractive-tabindex-test.js +123 -0
  51. package/__tests__/src/rules/no-onchange-test.js +57 -0
  52. package/__tests__/src/rules/no-redundant-roles-test.js +98 -0
  53. package/__tests__/src/rules/no-static-element-interactions-test.js +501 -0
  54. package/__tests__/src/rules/prefer-tag-over-role-test.js +63 -0
  55. package/__tests__/src/rules/role-has-required-aria-props-test.js +134 -0
  56. package/__tests__/src/rules/role-supports-aria-props-test.js +570 -0
  57. package/__tests__/src/rules/scope-test.js +50 -0
  58. package/__tests__/src/rules/tabindex-no-positive-test.js +55 -0
  59. package/__tests__/src/util/attributesComparator-test.js +91 -0
  60. package/__tests__/src/util/getAccessibleChildText-test.js +174 -0
  61. package/__tests__/src/util/getComputedRole-test.js +71 -0
  62. package/__tests__/src/util/getElementType-test.js +154 -0
  63. package/__tests__/src/util/getExplicitRole-test.js +35 -0
  64. package/__tests__/src/util/getImplicitRole-test.js +25 -0
  65. package/__tests__/src/util/getSuggestion-test.js +33 -0
  66. package/__tests__/src/util/getTabIndex-test.js +85 -0
  67. package/__tests__/src/util/hasAccessibleChild-test.js +157 -0
  68. package/__tests__/src/util/implicitRoles/input-test.js +87 -0
  69. package/__tests__/src/util/implicitRoles/menu-test.js +20 -0
  70. package/__tests__/src/util/implicitRoles/menuitem-test.js +38 -0
  71. package/__tests__/src/util/isAbstractRole-test.js +51 -0
  72. package/__tests__/src/util/isContentEditable-test.js +52 -0
  73. package/__tests__/src/util/isDOMElement-test.js +30 -0
  74. package/__tests__/src/util/isDisabledElement-test.js +88 -0
  75. package/__tests__/src/util/isFocusable-test.js +111 -0
  76. package/__tests__/src/util/isInteractiveElement-test.js +104 -0
  77. package/__tests__/src/util/isInteractiveRole-test.js +59 -0
  78. package/__tests__/src/util/isNonInteractiveElement-test.js +97 -0
  79. package/__tests__/src/util/isNonInteractiveRole-test.js +59 -0
  80. package/__tests__/src/util/isNonLiteralProperty-test.js +52 -0
  81. package/__tests__/src/util/isSemanticRoleElement-test.js +72 -0
  82. package/__tests__/src/util/mayContainChildComponent-test.js +219 -0
  83. package/__tests__/src/util/mayHaveAccessibleLabel-test.js +256 -0
  84. package/__tests__/src/util/parserOptionsMapper-test.js +93 -0
  85. package/__tests__/src/util/schemas-test.js +35 -0
  86. package/docs/rules/accessible-emoji.md +30 -0
  87. package/docs/rules/alt-text.md +168 -0
  88. package/docs/rules/anchor-ambiguous-text.md +91 -0
  89. package/docs/rules/anchor-has-content.md +64 -0
  90. package/docs/rules/anchor-is-valid.md +270 -0
  91. package/docs/rules/aria-activedescendant-has-tabindex.md +52 -0
  92. package/docs/rules/aria-props.md +29 -0
  93. package/docs/rules/aria-proptypes.md +30 -0
  94. package/docs/rules/aria-role.md +51 -0
  95. package/docs/rules/aria-unsupported-elements.md +30 -0
  96. package/docs/rules/autocomplete-valid.md +49 -0
  97. package/docs/rules/click-events-have-key-events.md +28 -0
  98. package/docs/rules/control-has-associated-label.md +113 -0
  99. package/docs/rules/heading-has-content.md +67 -0
  100. package/docs/rules/html-has-lang.md +31 -0
  101. package/docs/rules/iframe-has-title.md +37 -0
  102. package/docs/rules/img-redundant-alt.md +48 -0
  103. package/docs/rules/interactive-supports-focus.md +156 -0
  104. package/docs/rules/label-has-associated-control.md +152 -0
  105. package/docs/rules/label-has-for.md +130 -0
  106. package/docs/rules/lang.md +31 -0
  107. package/docs/rules/media-has-caption.md +48 -0
  108. package/docs/rules/mouse-events-have-key-events.md +58 -0
  109. package/docs/rules/no-access-key.md +30 -0
  110. package/docs/rules/no-aria-hidden-on-focusable.md +37 -0
  111. package/docs/rules/no-autofocus.md +43 -0
  112. package/docs/rules/no-distracting-elements.md +41 -0
  113. package/docs/rules/no-interactive-element-to-noninteractive-role.md +73 -0
  114. package/docs/rules/no-noninteractive-element-interactions.md +145 -0
  115. package/docs/rules/no-noninteractive-element-to-interactive-role.md +76 -0
  116. package/docs/rules/no-noninteractive-tabindex.md +115 -0
  117. package/docs/rules/no-onchange.md +36 -0
  118. package/docs/rules/no-redundant-roles.md +46 -0
  119. package/docs/rules/no-static-element-interactions.md +114 -0
  120. package/docs/rules/prefer-tag-over-role.md +32 -0
  121. package/docs/rules/role-has-required-aria-props.md +31 -0
  122. package/docs/rules/role-supports-aria-props.md +39 -0
  123. package/docs/rules/scope.md +30 -0
  124. package/docs/rules/tabindex-no-positive.md +32 -0
  125. package/lib/configs/flat-config-base.js +11 -0
  126. package/lib/configs/legacy-config-base.js +9 -0
  127. package/lib/index.js +209 -0
  128. package/lib/rules/accessible-emoji.js +63 -0
  129. package/lib/rules/alt-text.js +218 -0
  130. package/lib/rules/anchor-ambiguous-text.js +64 -0
  131. package/lib/rules/anchor-has-content.js +60 -0
  132. package/lib/rules/anchor-is-valid.js +122 -0
  133. package/lib/rules/aria-activedescendant-has-tabindex.js +66 -0
  134. package/lib/rules/aria-props.js +59 -0
  135. package/lib/rules/aria-proptypes.js +114 -0
  136. package/lib/rules/aria-role.js +89 -0
  137. package/lib/rules/aria-unsupported-elements.js +64 -0
  138. package/lib/rules/autocomplete-valid.js +67 -0
  139. package/lib/rules/click-events-have-key-events.js +68 -0
  140. package/lib/rules/control-has-associated-label.js +103 -0
  141. package/lib/rules/heading-has-content.js +61 -0
  142. package/lib/rules/html-has-lang.js +50 -0
  143. package/lib/rules/iframe-has-title.js +50 -0
  144. package/lib/rules/img-redundant-alt.js +88 -0
  145. package/lib/rules/interactive-supports-focus.js +87 -0
  146. package/lib/rules/label-has-associated-control.js +127 -0
  147. package/lib/rules/label-has-for.js +150 -0
  148. package/lib/rules/lang.js +68 -0
  149. package/lib/rules/media-has-caption.js +96 -0
  150. package/lib/rules/mouse-events-have-key-events.js +94 -0
  151. package/lib/rules/no-access-key.js +43 -0
  152. package/lib/rules/no-aria-hidden-on-focusable.js +47 -0
  153. package/lib/rules/no-autofocus.js +62 -0
  154. package/lib/rules/no-distracting-elements.js +54 -0
  155. package/lib/rules/no-interactive-element-to-noninteractive-role.js +81 -0
  156. package/lib/rules/no-noninteractive-element-interactions.js +95 -0
  157. package/lib/rules/no-noninteractive-element-to-interactive-role.js +80 -0
  158. package/lib/rules/no-noninteractive-tabindex.js +109 -0
  159. package/lib/rules/no-onchange.js +52 -0
  160. package/lib/rules/no-redundant-roles.js +86 -0
  161. package/lib/rules/no-static-element-interactions.js +102 -0
  162. package/lib/rules/prefer-tag-over-role.js +75 -0
  163. package/lib/rules/role-has-required-aria-props.js +88 -0
  164. package/lib/rules/role-supports-aria-props.js +78 -0
  165. package/lib/rules/scope.js +58 -0
  166. package/lib/rules/tabindex-no-positive.js +53 -0
  167. package/lib/util/attributesComparator.js +34 -0
  168. package/lib/util/getAccessibleChildText.js +55 -0
  169. package/lib/util/getComputedRole.js +19 -0
  170. package/lib/util/getElementType.js +30 -0
  171. package/lib/util/getExplicitRole.js +27 -0
  172. package/lib/util/getImplicitRole.js +24 -0
  173. package/lib/util/getSuggestion.js +32 -0
  174. package/lib/util/getTabIndex.js +34 -0
  175. package/lib/util/hasAccessibleChild.js +30 -0
  176. package/lib/util/implicitRoles/a.js +17 -0
  177. package/lib/util/implicitRoles/area.js +17 -0
  178. package/lib/util/implicitRoles/article.js +13 -0
  179. package/lib/util/implicitRoles/aside.js +13 -0
  180. package/lib/util/implicitRoles/body.js +13 -0
  181. package/lib/util/implicitRoles/button.js +13 -0
  182. package/lib/util/implicitRoles/datalist.js +13 -0
  183. package/lib/util/implicitRoles/details.js +13 -0
  184. package/lib/util/implicitRoles/dialog.js +13 -0
  185. package/lib/util/implicitRoles/form.js +13 -0
  186. package/lib/util/implicitRoles/h1.js +13 -0
  187. package/lib/util/implicitRoles/h2.js +13 -0
  188. package/lib/util/implicitRoles/h3.js +13 -0
  189. package/lib/util/implicitRoles/h4.js +13 -0
  190. package/lib/util/implicitRoles/h5.js +13 -0
  191. package/lib/util/implicitRoles/h6.js +13 -0
  192. package/lib/util/implicitRoles/hr.js +13 -0
  193. package/lib/util/implicitRoles/img.js +31 -0
  194. package/lib/util/implicitRoles/index.js +82 -0
  195. package/lib/util/implicitRoles/input.js +38 -0
  196. package/lib/util/implicitRoles/li.js +13 -0
  197. package/lib/util/implicitRoles/link.js +17 -0
  198. package/lib/util/implicitRoles/menu.js +19 -0
  199. package/lib/util/implicitRoles/menuitem.js +28 -0
  200. package/lib/util/implicitRoles/meter.js +13 -0
  201. package/lib/util/implicitRoles/nav.js +13 -0
  202. package/lib/util/implicitRoles/ol.js +13 -0
  203. package/lib/util/implicitRoles/option.js +13 -0
  204. package/lib/util/implicitRoles/output.js +13 -0
  205. package/lib/util/implicitRoles/progress.js +13 -0
  206. package/lib/util/implicitRoles/section.js +13 -0
  207. package/lib/util/implicitRoles/select.js +13 -0
  208. package/lib/util/implicitRoles/tbody.js +13 -0
  209. package/lib/util/implicitRoles/textarea.js +13 -0
  210. package/lib/util/implicitRoles/tfoot.js +13 -0
  211. package/lib/util/implicitRoles/thead.js +13 -0
  212. package/lib/util/implicitRoles/ul.js +13 -0
  213. package/lib/util/isAbstractRole.js +23 -0
  214. package/lib/util/isContentEditable.js +13 -0
  215. package/lib/util/isDOMElement.js +15 -0
  216. package/lib/util/isDisabledElement.js +23 -0
  217. package/lib/util/isFocusable.js +23 -0
  218. package/lib/util/isHiddenFromScreenReader.js +26 -0
  219. package/lib/util/isInteractiveElement.js +116 -0
  220. package/lib/util/isInteractiveRole.js +54 -0
  221. package/lib/util/isNonInteractiveElement.js +131 -0
  222. package/lib/util/isNonInteractiveRole.js +55 -0
  223. package/lib/util/isNonLiteralProperty.js +29 -0
  224. package/lib/util/isPresentationRole.js +13 -0
  225. package/lib/util/isSemanticRoleElement.js +54 -0
  226. package/lib/util/mayContainChildComponent.js +50 -0
  227. package/lib/util/mayHaveAccessibleLabel.js +95 -0
  228. package/lib/util/schemas.js +52 -0
  229. 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,6 @@
1
+ /* eslint-disable import/prefer-default-export, no-underscore-dangle */
2
+ import * as axe from 'axe-core';
3
+
4
+ export function axeFailMessage(checkId, data) {
5
+ return axe.utils.getCheckMessage(checkId, 'fail', data);
6
+ }
@@ -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">&#9731;</span>' },
33
+ { code: '<span role="img" aria-labelledby="id1">๐Ÿผ</span>' },
34
+ { code: '<span role="img" aria-labelledby="id1">&#9731;</span>' },
35
+ { code: '<span role="img" aria-labelledby="id1" aria-label="Snowman">&#9731;</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
+ });