@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,69 @@
1
+ /**
2
+ * @fileoverview Enforce all aria-* properties are valid.
3
+ * @author Ethan Cohen
4
+ */
5
+
6
+ // -----------------------------------------------------------------------------
7
+ // Requirements
8
+ // -----------------------------------------------------------------------------
9
+
10
+ import { aria } from 'aria-query';
11
+ import { RuleTester } from 'eslint';
12
+ import parserOptionsMapper from '../../__util__/parserOptionsMapper';
13
+ import parsers from '../../__util__/helpers/parsers';
14
+ import rule from '../../../src/rules/aria-props';
15
+ import getSuggestion from '../../../src/util/getSuggestion';
16
+
17
+ // -----------------------------------------------------------------------------
18
+ // Tests
19
+ // -----------------------------------------------------------------------------
20
+
21
+ const ruleTester = new RuleTester();
22
+ const ariaAttributes = aria.keys();
23
+
24
+ const errorMessage = (name) => {
25
+ const suggestions = getSuggestion(name, ariaAttributes);
26
+ const message = `${name}: This attribute is an invalid ARIA attribute.`;
27
+
28
+ if (suggestions.length > 0) {
29
+ return {
30
+ type: 'JSXAttribute',
31
+ message: `${message} Did you mean to use ${suggestions}?`,
32
+ };
33
+ }
34
+
35
+ return {
36
+ type: 'JSXAttribute',
37
+ message,
38
+ };
39
+ };
40
+
41
+ // Create basic test cases using all valid role types.
42
+ const basicValidityTests = ariaAttributes.map((prop) => ({
43
+ code: `<div ${prop.toLowerCase()}="foobar" />`,
44
+ }));
45
+
46
+ ruleTester.run('aria-props', rule, {
47
+ valid: parsers.all([].concat(
48
+ // Variables should pass, as we are only testing literals.
49
+ { code: '<div />' },
50
+ { code: '<div></div>' },
51
+ { code: '<div aria="wee"></div>' }, // Needs aria-*
52
+ { code: '<div abcARIAdef="true"></div>' },
53
+ { code: '<div fooaria-foobar="true"></div>' },
54
+ { code: '<div fooaria-hidden="true"></div>' },
55
+ { code: '<Bar baz />' },
56
+ { code: '<input type="text" aria-errormessage="foobar" />' },
57
+ )).concat(basicValidityTests).map(parserOptionsMapper),
58
+ invalid: parsers.all([].concat(
59
+ { code: '<div aria-="foobar" />', errors: [errorMessage('aria-')] },
60
+ {
61
+ code: '<div aria-labeledby="foobar" />',
62
+ errors: [errorMessage('aria-labeledby')],
63
+ },
64
+ {
65
+ code: '<div aria-skldjfaria-klajsd="foobar" />',
66
+ errors: [errorMessage('aria-skldjfaria-klajsd')],
67
+ },
68
+ )).map(parserOptionsMapper),
69
+ });
@@ -0,0 +1,311 @@
1
+ /**
2
+ * @fileoverview Enforce ARIA state and property values are valid.
3
+ * @author Ethan Cohen
4
+ */
5
+
6
+ // -----------------------------------------------------------------------------
7
+ // Requirements
8
+ // -----------------------------------------------------------------------------
9
+
10
+ import { aria } from 'aria-query';
11
+ import { RuleTester } from 'eslint';
12
+ import test from 'tape';
13
+
14
+ import parserOptionsMapper from '../../__util__/parserOptionsMapper';
15
+ import parsers from '../../__util__/helpers/parsers';
16
+ import rule from '../../../src/rules/aria-proptypes';
17
+
18
+ const { validityCheck } = rule;
19
+
20
+ // -----------------------------------------------------------------------------
21
+ // Tests
22
+ // -----------------------------------------------------------------------------
23
+
24
+ const ruleTester = new RuleTester();
25
+
26
+ const errorMessage = (name) => {
27
+ const {
28
+ type,
29
+ values: permittedValues,
30
+ } = aria.get(name.toLowerCase());
31
+
32
+ switch (type) {
33
+ case 'tristate':
34
+ return { message: `The value for ${name} must be a boolean or the string "mixed".` };
35
+ case 'token':
36
+ return { message: `The value for ${name} must be a single token from the following: ${permittedValues}.` };
37
+ case 'tokenlist':
38
+ return {
39
+ message: `The value for ${name} must be a list of one or more \
40
+ tokens from the following: ${permittedValues}.`,
41
+ };
42
+ case 'idlist':
43
+ return { message: `The value for ${name} must be a list of strings that represent DOM element IDs (idlist)` };
44
+ case 'id':
45
+ return { message: `The value for ${name} must be a string that represents a DOM element ID` };
46
+ case 'boolean':
47
+ case 'string':
48
+ case 'integer':
49
+ case 'number':
50
+ default:
51
+ return { message: `The value for ${name} must be a ${type}.` };
52
+ }
53
+ };
54
+
55
+ test('validityCheck', (t) => {
56
+ t.equal(
57
+ validityCheck(null, null),
58
+ false,
59
+ 'is false for an unknown expected type',
60
+ );
61
+
62
+ t.end();
63
+ });
64
+
65
+ ruleTester.run('aria-proptypes', rule, {
66
+ valid: parsers.all([].concat(
67
+ // DON'T TEST INVALID ARIA-* PROPS
68
+ { code: '<div aria-foo="true" />' },
69
+ { code: '<div abcaria-foo="true" />' },
70
+
71
+ // BOOLEAN
72
+ { code: '<div aria-hidden={true} />' },
73
+ { code: '<div aria-hidden="true" />' },
74
+ { code: '<div aria-hidden={"false"} />' },
75
+ { code: '<div aria-hidden={!false} />' },
76
+ { code: '<div aria-hidden />' },
77
+ { code: '<div aria-hidden={false} />' },
78
+ { code: '<div aria-hidden={!true} />' },
79
+ { code: '<div aria-hidden={!"yes"} />' },
80
+ { code: '<div aria-hidden={foo} />' },
81
+ { code: '<div aria-hidden={foo.bar} />' },
82
+ { code: '<div aria-hidden={null} />' },
83
+ { code: '<div aria-hidden={undefined} />' },
84
+ { code: '<div aria-hidden={<div />} />' },
85
+
86
+ // STRING
87
+ { code: '<div aria-label="Close" />' },
88
+ { code: '<div aria-label={`Close`} />' },
89
+ { code: '<div aria-label={foo} />' },
90
+ { code: '<div aria-label={foo.bar} />' },
91
+ { code: '<div aria-label={null} />' },
92
+ { code: '<div aria-label={undefined} />' },
93
+ { code: '<input aria-invalid={error ? "true" : "false"} />' },
94
+ { code: '<input aria-invalid={undefined ? "true" : "false"} />' },
95
+
96
+ // TRISTATE
97
+ { code: '<div aria-checked={true} />' },
98
+ { code: '<div aria-checked="true" />' },
99
+ { code: '<div aria-checked={"false"} />' },
100
+ { code: '<div aria-checked={!false} />' },
101
+ { code: '<div aria-checked />' },
102
+ { code: '<div aria-checked={false} />' },
103
+ { code: '<div aria-checked={!true} />' },
104
+ { code: '<div aria-checked={!"yes"} />' },
105
+ { code: '<div aria-checked={foo} />' },
106
+ { code: '<div aria-checked={foo.bar} />' },
107
+ { code: '<div aria-checked="mixed" />' },
108
+ { code: '<div aria-checked={`mixed`} />' },
109
+ { code: '<div aria-checked={null} />' },
110
+ { code: '<div aria-checked={undefined} />' },
111
+
112
+ // INTEGER
113
+ { code: '<div aria-level={123} />' },
114
+ { code: '<div aria-level={-123} />' },
115
+ { code: '<div aria-level={+123} />' },
116
+ { code: '<div aria-level={~123} />' },
117
+ { code: '<div aria-level={"123"} />' },
118
+ { code: '<div aria-level={`123`} />' },
119
+ { code: '<div aria-level="123" />' },
120
+ { code: '<div aria-level={foo} />' },
121
+ { code: '<div aria-level={foo.bar} />' },
122
+ { code: '<div aria-level={null} />' },
123
+ { code: '<div aria-level={undefined} />' },
124
+
125
+ // NUMBER
126
+ { code: '<div aria-valuemax={123} />' },
127
+ { code: '<div aria-valuemax={-123} />' },
128
+ { code: '<div aria-valuemax={+123} />' },
129
+ { code: '<div aria-valuemax={~123} />' },
130
+ { code: '<div aria-valuemax={"123"} />' },
131
+ { code: '<div aria-valuemax={`123`} />' },
132
+ { code: '<div aria-valuemax="123" />' },
133
+ { code: '<div aria-valuemax={foo} />' },
134
+ { code: '<div aria-valuemax={foo.bar} />' },
135
+ { code: '<div aria-valuemax={null} />' },
136
+ { code: '<div aria-valuemax={undefined} />' },
137
+
138
+ // TOKEN
139
+ { code: '<div aria-sort="ascending" />' },
140
+ { code: '<div aria-sort="ASCENDING" />' },
141
+ { code: '<div aria-sort={"ascending"} />' },
142
+ { code: '<div aria-sort={`ascending`} />' },
143
+ { code: '<div aria-sort="descending" />' },
144
+ { code: '<div aria-sort={"descending"} />' },
145
+ { code: '<div aria-sort={`descending`} />' },
146
+ { code: '<div aria-sort="none" />' },
147
+ { code: '<div aria-sort={"none"} />' },
148
+ { code: '<div aria-sort={`none`} />' },
149
+ { code: '<div aria-sort="other" />' },
150
+ { code: '<div aria-sort={"other"} />' },
151
+ { code: '<div aria-sort={`other`} />' },
152
+ { code: '<div aria-sort={foo} />' },
153
+ { code: '<div aria-sort={foo.bar} />' },
154
+ { code: '<div aria-invalid={true} />' },
155
+ { code: '<div aria-invalid="true" />' },
156
+ { code: '<div aria-invalid={false} />' },
157
+ { code: '<div aria-invalid="false" />' },
158
+ { code: '<div aria-invalid="grammar" />' },
159
+ { code: '<div aria-invalid="spelling" />' },
160
+ { code: '<div aria-invalid={null} />' },
161
+ { code: '<div aria-invalid={undefined} />' },
162
+
163
+ // TOKENLIST
164
+ { code: '<div aria-relevant="additions" />' },
165
+ { code: '<div aria-relevant={"additions"} />' },
166
+ { code: '<div aria-relevant={`additions`} />' },
167
+ { code: '<div aria-relevant="additions removals" />' },
168
+ { code: '<div aria-relevant="additions additions" />' },
169
+ { code: '<div aria-relevant={"additions removals"} />' },
170
+ { code: '<div aria-relevant={`additions removals`} />' },
171
+ { code: '<div aria-relevant="additions removals text" />' },
172
+ { code: '<div aria-relevant={"additions removals text"} />' },
173
+ { code: '<div aria-relevant={`additions removals text`} />' },
174
+ { code: '<div aria-relevant="additions removals text all" />' },
175
+ { code: '<div aria-relevant={"additions removals text all"} />' },
176
+ { code: '<div aria-relevant={`removals additions text all`} />' },
177
+ { code: '<div aria-relevant={foo} />' },
178
+ { code: '<div aria-relevant={foo.bar} />' },
179
+ { code: '<div aria-relevant={null} />' },
180
+ { code: '<div aria-relevant={undefined} />' },
181
+
182
+ // ID
183
+ { code: '<div aria-activedescendant="ascending" />' },
184
+ { code: '<div aria-activedescendant="ASCENDING" />' },
185
+ { code: '<div aria-activedescendant={"ascending"} />' },
186
+ { code: '<div aria-activedescendant={`ascending`} />' },
187
+ { code: '<div aria-activedescendant="descending" />' },
188
+ { code: '<div aria-activedescendant={"descending"} />' },
189
+ { code: '<div aria-activedescendant={`descending`} />' },
190
+ { code: '<div aria-activedescendant="none" />' },
191
+ { code: '<div aria-activedescendant={"none"} />' },
192
+ { code: '<div aria-activedescendant={`none`} />' },
193
+ { code: '<div aria-activedescendant="other" />' },
194
+ { code: '<div aria-activedescendant={"other"} />' },
195
+ { code: '<div aria-activedescendant={`other`} />' },
196
+ { code: '<div aria-activedescendant={foo} />' },
197
+ { code: '<div aria-activedescendant={foo.bar} />' },
198
+ { code: '<div aria-activedescendant={null} />' },
199
+ { code: '<div aria-activedescendant={undefined} />' },
200
+
201
+ // IDLIST
202
+ { code: '<div aria-labelledby="additions" />' },
203
+ { code: '<div aria-labelledby={"additions"} />' },
204
+ { code: '<div aria-labelledby={`additions`} />' },
205
+ { code: '<div aria-labelledby="additions removals" />' },
206
+ { code: '<div aria-labelledby="additions additions" />' },
207
+ { code: '<div aria-labelledby={"additions removals"} />' },
208
+ { code: '<div aria-labelledby={`additions removals`} />' },
209
+ { code: '<div aria-labelledby="additions removals text" />' },
210
+ { code: '<div aria-labelledby={"additions removals text"} />' },
211
+ { code: '<div aria-labelledby={`additions removals text`} />' },
212
+ { code: '<div aria-labelledby="additions removals text all" />' },
213
+ { code: '<div aria-labelledby={"additions removals text all"} />' },
214
+ { code: '<div aria-labelledby={`removals additions text all`} />' },
215
+ { code: '<div aria-labelledby={foo} />' },
216
+ { code: '<div aria-labelledby={foo.bar} />' },
217
+ { code: '<div aria-labelledby={null} />' },
218
+ { code: '<div aria-labelledby={undefined} />' },
219
+ )).map(parserOptionsMapper),
220
+ invalid: parsers.all([].concat(
221
+ // BOOLEAN
222
+ { code: '<div aria-hidden="yes" />', errors: [errorMessage('aria-hidden')] },
223
+ { code: '<div aria-hidden="no" />', errors: [errorMessage('aria-hidden')] },
224
+ { code: '<div aria-hidden={1234} />', errors: [errorMessage('aria-hidden')] },
225
+ {
226
+ code: '<div aria-hidden={`${abc}`} />',
227
+ errors: [errorMessage('aria-hidden')],
228
+ },
229
+
230
+ // STRING
231
+ { code: '<div aria-label />', errors: [errorMessage('aria-label')] },
232
+ { code: '<div aria-label={true} />', errors: [errorMessage('aria-label')] },
233
+ { code: '<div aria-label={false} />', errors: [errorMessage('aria-label')] },
234
+ { code: '<div aria-label={1234} />', errors: [errorMessage('aria-label')] },
235
+ { code: '<div aria-label={!true} />', errors: [errorMessage('aria-label')] },
236
+
237
+ // TRISTATE
238
+ { code: '<div aria-checked="yes" />', errors: [errorMessage('aria-checked')] },
239
+ { code: '<div aria-checked="no" />', errors: [errorMessage('aria-checked')] },
240
+ { code: '<div aria-checked={1234} />', errors: [errorMessage('aria-checked')] },
241
+ {
242
+ code: '<div aria-checked={`${abc}`} />',
243
+ errors: [errorMessage('aria-checked')],
244
+ },
245
+
246
+ // INTEGER
247
+ { code: '<div aria-level="yes" />', errors: [errorMessage('aria-level')] },
248
+ { code: '<div aria-level="no" />', errors: [errorMessage('aria-level')] },
249
+ { code: '<div aria-level={`abc`} />', errors: [errorMessage('aria-level')] },
250
+ { code: '<div aria-level={true} />', errors: [errorMessage('aria-level')] },
251
+ { code: '<div aria-level />', errors: [errorMessage('aria-level')] },
252
+ { code: '<div aria-level={"false"} />', errors: [errorMessage('aria-level')] },
253
+ { code: '<div aria-level={!"false"} />', errors: [errorMessage('aria-level')] },
254
+
255
+ // NUMBER
256
+ { code: '<div aria-valuemax="yes" />', errors: [errorMessage('aria-valuemax')] },
257
+ { code: '<div aria-valuemax="no" />', errors: [errorMessage('aria-valuemax')] },
258
+ {
259
+ code: '<div aria-valuemax={`abc`} />',
260
+ errors: [errorMessage('aria-valuemax')],
261
+ },
262
+ {
263
+ code: '<div aria-valuemax={true} />',
264
+ errors: [errorMessage('aria-valuemax')],
265
+ },
266
+ { code: '<div aria-valuemax />', errors: [errorMessage('aria-valuemax')] },
267
+ {
268
+ code: '<div aria-valuemax={"false"} />',
269
+ errors: [errorMessage('aria-valuemax')],
270
+ },
271
+ {
272
+ code: '<div aria-valuemax={!"false"} />',
273
+ errors: [errorMessage('aria-valuemax')],
274
+ },
275
+
276
+ // TOKEN
277
+ { code: '<div aria-sort="" />', errors: [errorMessage('aria-sort')] },
278
+ { code: '<div aria-sort="descnding" />', errors: [errorMessage('aria-sort')] },
279
+ { code: '<div aria-sort />', errors: [errorMessage('aria-sort')] },
280
+ { code: '<div aria-sort={true} />', errors: [errorMessage('aria-sort')] },
281
+ { code: '<div aria-sort={"false"} />', errors: [errorMessage('aria-sort')] },
282
+ {
283
+ code: '<div aria-sort="ascending descending" />',
284
+ errors: [errorMessage('aria-sort')],
285
+ },
286
+
287
+ // TOKENLIST
288
+ { code: '<div aria-relevant="" />', errors: [errorMessage('aria-relevant')] },
289
+ {
290
+ code: '<div aria-relevant="foobar" />',
291
+ errors: [errorMessage('aria-relevant')],
292
+ },
293
+ { code: '<div aria-relevant />', errors: [errorMessage('aria-relevant')] },
294
+ {
295
+ code: '<div aria-relevant={true} />',
296
+ errors: [errorMessage('aria-relevant')],
297
+ },
298
+ {
299
+ code: '<div aria-relevant={"false"} />',
300
+ errors: [errorMessage('aria-relevant')],
301
+ },
302
+ {
303
+ code: '<div aria-relevant="additions removalss" />',
304
+ errors: [errorMessage('aria-relevant')],
305
+ },
306
+ {
307
+ code: '<div aria-relevant="additions removalss " />',
308
+ errors: [errorMessage('aria-relevant')],
309
+ },
310
+ )).map(parserOptionsMapper),
311
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @fileoverview Enforce aria role attribute is valid.
3
+ * @author Ethan Cohen
4
+ */
5
+
6
+ // -----------------------------------------------------------------------------
7
+ // Requirements
8
+ // -----------------------------------------------------------------------------
9
+
10
+ import { roles } from 'aria-query';
11
+ import { RuleTester } from 'eslint';
12
+ import parserOptionsMapper from '../../__util__/parserOptionsMapper';
13
+ import parsers from '../../__util__/helpers/parsers';
14
+ import rule from '../../../src/rules/aria-role';
15
+
16
+ // -----------------------------------------------------------------------------
17
+ // Tests
18
+ // -----------------------------------------------------------------------------
19
+
20
+ const ruleTester = new RuleTester();
21
+
22
+ const errorMessage = {
23
+ message: 'Elements with ARIA roles must use a valid, non-abstract ARIA role.',
24
+ type: 'JSXAttribute',
25
+ };
26
+
27
+ const roleKeys = roles.keys();
28
+
29
+ const validRoles = roleKeys.filter((role) => roles.get(role).abstract === false);
30
+ const invalidRoles = roleKeys.filter((role) => roles.get(role).abstract === true);
31
+
32
+ const createTests = (roleNames) => roleNames.map((role) => ({
33
+ code: `<div role="${role.toLowerCase()}" />`,
34
+ }));
35
+
36
+ const validTests = createTests(validRoles);
37
+ const invalidTests = createTests(invalidRoles).map((test) => {
38
+ const invalidTest = { ...test };
39
+ invalidTest.errors = [errorMessage];
40
+ return invalidTest;
41
+ });
42
+
43
+ const allowedInvalidRoles = [{
44
+ allowedInvalidRoles: ['invalid-role', 'other-invalid-role'],
45
+ }];
46
+
47
+ const ignoreNonDOMSchema = [{
48
+ ignoreNonDOM: true,
49
+ }];
50
+
51
+ const customDivSettings = {
52
+ 'jsx-a11y': {
53
+ polymorphicPropName: 'asChild',
54
+ components: {
55
+ Div: 'div',
56
+ },
57
+ },
58
+ };
59
+
60
+ ruleTester.run('aria-role', rule, {
61
+ valid: parsers.all([].concat(
62
+ // Variables should pass, as we are only testing literals.
63
+ { code: '<div />' },
64
+ { code: '<div></div>' },
65
+ { code: '<div role={role} />' },
66
+ { code: '<div role={role || "button"} />' },
67
+ { code: '<div role={role || "foobar"} />' },
68
+ { code: '<div role="tabpanel row" />' },
69
+ { code: '<div role="switch" />' },
70
+ { code: '<div role="doc-abstract" />' },
71
+ { code: '<div role="doc-appendix doc-bibliography" />' },
72
+ { code: '<Bar baz />' },
73
+ { code: '<img role="invalid-role" />', options: allowedInvalidRoles },
74
+ { code: '<img role="invalid-role tabpanel" />', options: allowedInvalidRoles },
75
+ { code: '<img role="invalid-role other-invalid-role" />', options: allowedInvalidRoles },
76
+ { code: '<Foo role="bar" />', options: ignoreNonDOMSchema },
77
+ { code: '<fakeDOM role="bar" />', options: ignoreNonDOMSchema },
78
+ { code: '<img role="presentation" />', options: ignoreNonDOMSchema },
79
+ {
80
+ code: '<Div role="button" />',
81
+ settings: customDivSettings,
82
+ },
83
+ {
84
+ code: '<Box asChild="div" role="button" />',
85
+ settings: customDivSettings,
86
+ },
87
+ { code: '<svg role="graphics-document document" />' },
88
+ { code: '<svg role="img" />' },
89
+ )).concat(validTests).map(parserOptionsMapper),
90
+
91
+ invalid: parsers.all([].concat(
92
+ { code: '<div role="foobar" />', errors: [errorMessage] },
93
+ { code: '<div role="datepicker"></div>', errors: [errorMessage] },
94
+ { code: '<div role="range"></div>', errors: [errorMessage] },
95
+ { code: '<div role="Button"></div>', errors: [errorMessage] },
96
+ { code: '<div role=""></div>', errors: [errorMessage] },
97
+ { code: '<div role="tabpanel row foobar"></div>', errors: [errorMessage] },
98
+ { code: '<div role="tabpanel row range"></div>', errors: [errorMessage] },
99
+ { code: '<div role="doc-endnotes range"></div>', errors: [errorMessage] },
100
+ { code: '<div role />', errors: [errorMessage] },
101
+ { code: '<div role="unknown-invalid-role" />', errors: [errorMessage], options: allowedInvalidRoles },
102
+ { code: '<div role={null}></div>', errors: [errorMessage] },
103
+ { code: '<Foo role="datepicker" />', errors: [errorMessage] },
104
+ { code: '<Foo role="Button" />', errors: [errorMessage] },
105
+ { code: '<Div role="Button" />', errors: [errorMessage], settings: customDivSettings },
106
+ {
107
+ code: '<Div role="Button" />',
108
+ errors: [errorMessage],
109
+ options: ignoreNonDOMSchema,
110
+ settings: customDivSettings,
111
+ },
112
+ {
113
+ code: '<Box asChild="div" role="Button" />',
114
+ settings: customDivSettings,
115
+ errors: [errorMessage],
116
+ },
117
+ )).concat(invalidTests).map(parserOptionsMapper),
118
+ });
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @fileoverview Enforce that elements that do not support ARIA roles,
3
+ * states and properties do not have those attributes.
4
+ * @author Ethan Cohen
5
+ */
6
+
7
+ // -----------------------------------------------------------------------------
8
+ // Requirements
9
+ // -----------------------------------------------------------------------------
10
+
11
+ import { dom } from 'aria-query';
12
+ import { RuleTester } from 'eslint';
13
+ import parserOptionsMapper from '../../__util__/parserOptionsMapper';
14
+ import parsers from '../../__util__/helpers/parsers';
15
+ import rule from '../../../src/rules/aria-unsupported-elements';
16
+
17
+ // -----------------------------------------------------------------------------
18
+ // Tests
19
+ // -----------------------------------------------------------------------------
20
+
21
+ const ruleTester = new RuleTester();
22
+
23
+ const errorMessage = (invalidProp) => ({
24
+ message: `This element does not support ARIA roles, states and properties. \
25
+ Try removing the prop '${invalidProp}'.`,
26
+ type: 'JSXOpeningElement',
27
+ });
28
+
29
+ const domElements = dom.keys();
30
+ // Generate valid test cases
31
+ const roleValidityTests = domElements.map((element) => {
32
+ const isReserved = dom.get(element).reserved || false;
33
+ const role = isReserved ? '' : 'role';
34
+
35
+ return {
36
+ code: `<${element} ${role} />`,
37
+ };
38
+ });
39
+
40
+ const ariaValidityTests = domElements.map((element) => {
41
+ const isReserved = dom.get(element).reserved || false;
42
+ const aria = isReserved ? '' : 'aria-hidden';
43
+
44
+ return {
45
+ code: `<${element} ${aria} />`,
46
+ };
47
+ }).concat({
48
+ code: '<fake aria-hidden />',
49
+ errors: [errorMessage('aria-hidden')],
50
+ });
51
+
52
+ // Generate invalid test cases.
53
+ const invalidRoleValidityTests = domElements
54
+ .filter((element) => dom.get(element).reserved)
55
+ .map((reservedElem) => ({
56
+ code: `<${reservedElem} role {...props} />`,
57
+ errors: [errorMessage('role')],
58
+ })).concat({
59
+ code: '<Meta aria-hidden />',
60
+ errors: [errorMessage('aria-hidden')],
61
+ settings: { 'jsx-a11y': { components: { Meta: 'meta' } } },
62
+ });
63
+
64
+ const invalidAriaValidityTests = domElements
65
+ .filter((element) => dom.get(element).reserved)
66
+ .map((reservedElem) => ({
67
+ code: `<${reservedElem} aria-hidden aria-role="none" {...props} />`,
68
+ errors: [errorMessage('aria-hidden')],
69
+ }));
70
+
71
+ ruleTester.run('aria-unsupported-elements', rule, {
72
+ valid: parsers.all([].concat(roleValidityTests, ariaValidityTests)).map(parserOptionsMapper),
73
+ invalid: parsers.all([].concat(invalidRoleValidityTests, invalidAriaValidityTests))
74
+ .map(parserOptionsMapper),
75
+ });
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @fileoverview Ensure autocomplete attribute is correct.
3
+ * @author Wilco Fiers
4
+ */
5
+
6
+ // -----------------------------------------------------------------------------
7
+ // Requirements
8
+ // -----------------------------------------------------------------------------
9
+
10
+ import { RuleTester } from 'eslint';
11
+ import parserOptionsMapper from '../../__util__/parserOptionsMapper';
12
+ import { axeFailMessage } from '../../__util__/axeMapping';
13
+ import parsers from '../../__util__/helpers/parsers';
14
+ import rule from '../../../src/rules/autocomplete-valid';
15
+
16
+ // -----------------------------------------------------------------------------
17
+ // Tests
18
+ // -----------------------------------------------------------------------------
19
+
20
+ const ruleTester = new RuleTester();
21
+
22
+ const invalidAutocomplete = [{
23
+ message: axeFailMessage('autocomplete-valid'),
24
+ type: 'JSXOpeningElement',
25
+ }];
26
+
27
+ const inappropriateAutocomplete = [{
28
+ message: axeFailMessage('autocomplete-appropriate'),
29
+ type: 'JSXOpeningElement',
30
+ }];
31
+
32
+ const componentsSettings = {
33
+ 'jsx-a11y': {
34
+ components: {
35
+ Input: 'input',
36
+ },
37
+ },
38
+ };
39
+
40
+ ruleTester.run('autocomplete-valid', rule, {
41
+ valid: parsers.all([].concat(
42
+ // INAPPLICABLE
43
+ { code: '<input type="text" />;' },
44
+ // // PASSED AUTOCOMPLETE
45
+ { code: '<input type="text" autocomplete="name" />;' },
46
+ { code: '<input type="text" autocomplete="" />;' },
47
+ { code: '<input type="text" autocomplete="off" />;' },
48
+ { code: '<input type="text" autocomplete="on" />;' },
49
+ { code: '<input type="text" autocomplete="billing family-name" />;' },
50
+ { code: '<input type="text" autocomplete="section-blue shipping street-address" />;' },
51
+ { code: '<input type="text" autocomplete="section-somewhere shipping work email" />;' },
52
+ { code: '<input type="text" autocomplete />;' },
53
+ { code: '<input type="text" autocomplete={autocompl} />;' },
54
+ { code: '<input type="text" autocomplete={autocompl || "name"} />;' },
55
+ { code: '<input type="text" autocomplete={autocompl || "foo"} />;' },
56
+ { code: '<Foo autocomplete="bar"></Foo>;' },
57
+ { code: '<input type={isEmail ? "email" : "text"} autocomplete="none" />;' },
58
+ { code: '<Input type="text" autocomplete="name" />', settings: componentsSettings },
59
+ { code: '<Input type="text" autocomplete="baz" />' },
60
+
61
+ // PASSED "autocomplete-appropriate"
62
+ // see also: https://github.com/dequelabs/axe-core/issues/2912
63
+ { code: '<input type="date" autocomplete="email" />;', errors: inappropriateAutocomplete },
64
+ { code: '<input type="number" autocomplete="url" />;', errors: inappropriateAutocomplete },
65
+ { code: '<input type="month" autocomplete="tel" />;', errors: inappropriateAutocomplete },
66
+ { code: '<Foo type="month" autocomplete="tel"></Foo>;', errors: inappropriateAutocomplete, options: [{ inputComponents: ['Foo'] }] },
67
+ )).map(parserOptionsMapper),
68
+ invalid: parsers.all([].concat(
69
+ // FAILED "autocomplete-valid"
70
+ { code: '<input type="text" autocomplete="foo" />;', errors: invalidAutocomplete },
71
+ { code: '<input type="text" autocomplete="name invalid" />;', errors: invalidAutocomplete },
72
+ { code: '<input type="text" autocomplete="invalid name" />;', errors: invalidAutocomplete },
73
+ { code: '<input type="text" autocomplete="home url" />;', errors: invalidAutocomplete },
74
+ { code: '<Bar autocomplete="baz"></Bar>;', errors: invalidAutocomplete, options: [{ inputComponents: ['Bar'] }] },
75
+ { code: '<Input type="text" autocomplete="baz" />', errors: invalidAutocomplete, settings: componentsSettings },
76
+ )).map(parserOptionsMapper),
77
+ });