@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,77 @@
1
+ /**
2
+ * @fileoverview Enforce a clickable non-interactive element has at least 1 keyboard event listener.
3
+ * @author Ethan Cohen
4
+ */
5
+
6
+ // -----------------------------------------------------------------------------
7
+ // Requirements
8
+ // -----------------------------------------------------------------------------
9
+
10
+ import { RuleTester } from 'eslint';
11
+ import parserOptionsMapper from '../../__util__/parserOptionsMapper';
12
+ import parsers from '../../__util__/helpers/parsers';
13
+ import rule from '../../../src/rules/click-events-have-key-events';
14
+
15
+ // -----------------------------------------------------------------------------
16
+ // Tests
17
+ // -----------------------------------------------------------------------------
18
+
19
+ const ruleTester = new RuleTester();
20
+
21
+ const errorMessage = 'Visible, non-interactive elements with click handlers must have at least one keyboard listener.';
22
+
23
+ const expectedError = {
24
+ message: errorMessage,
25
+ type: 'JSXOpeningElement',
26
+ };
27
+
28
+ ruleTester.run('click-events-have-key-events', rule, {
29
+ valid: parsers.all([].concat(
30
+ { code: '<div onClick={() => void 0} onKeyDown={foo}/>;' },
31
+ { code: '<div onClick={() => void 0} onKeyUp={foo} />;' },
32
+ { code: '<div onClick={() => void 0} onKeyPress={foo}/>;' },
33
+ { code: '<div onClick={() => void 0} onKeyDown={foo} onKeyUp={bar} />;' },
34
+ { code: '<div onClick={() => void 0} onKeyDown={foo} {...props} />;' },
35
+ { code: '<div className="foo" />;' },
36
+ { code: '<div onClick={() => void 0} aria-hidden />;' },
37
+ { code: '<div onClick={() => void 0} aria-hidden={true} />;' },
38
+ { code: '<div onClick={() => void 0} aria-hidden={false} onKeyDown={foo} />;' },
39
+ {
40
+ code: '<div onClick={() => void 0} onKeyDown={foo} aria-hidden={undefined} />;',
41
+ },
42
+ { code: '<input type="text" onClick={() => void 0} />' },
43
+ { code: '<input onClick={() => void 0} />' },
44
+ { code: '<button onClick={() => void 0} className="foo" />' },
45
+ { code: '<option onClick={() => void 0} className="foo" />' },
46
+ { code: '<select onClick={() => void 0} className="foo" />' },
47
+ { code: '<textarea onClick={() => void 0} className="foo" />' },
48
+ { code: '<a onClick={() => void 0} href="http://x.y.z" />' },
49
+ { code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex="0" />' },
50
+ { code: '<input onClick={() => void 0} type="hidden" />;' },
51
+ { code: '<div onClick={() => void 0} role="presentation" />;' },
52
+ { code: '<div onClick={() => void 0} role="none" />;' },
53
+ { code: '<TestComponent onClick={doFoo} />' },
54
+ { code: '<Button onClick={doFoo} />' },
55
+ { code: '<Footer onClick={doFoo} />' },
56
+ )).map(parserOptionsMapper),
57
+ invalid: parsers.all([].concat(
58
+ { code: '<div onClick={() => void 0} />;', errors: [expectedError] },
59
+ {
60
+ code: '<div onClick={() => void 0} role={undefined} />;',
61
+ errors: [expectedError],
62
+ },
63
+ { code: '<div onClick={() => void 0} {...props} />;', errors: [expectedError] },
64
+ { code: '<section onClick={() => void 0} />;', errors: [expectedError] },
65
+ { code: '<main onClick={() => void 0} />;', errors: [expectedError] },
66
+ { code: '<article onClick={() => void 0} />;', errors: [expectedError] },
67
+ { code: '<header onClick={() => void 0} />;', errors: [expectedError] },
68
+ { code: '<footer onClick={() => void 0} />;', errors: [expectedError] },
69
+ {
70
+ code: '<div onClick={() => void 0} aria-hidden={false} />;',
71
+ errors: [expectedError],
72
+ },
73
+ { code: '<a onClick={() => void 0} />', errors: [expectedError] },
74
+ { code: '<a tabIndex="0" onClick={() => void 0} />', errors: [expectedError] },
75
+ { code: '<Footer onClick={doFoo} />', errors: [expectedError], settings: { 'jsx-a11y': { components: { Footer: 'footer' } } } },
76
+ )).map(parserOptionsMapper),
77
+ });
@@ -0,0 +1,327 @@
1
+ /**
2
+ * @fileoverview Control elements must be associated with a text label
3
+ * @author jessebeach
4
+ */
5
+
6
+ // -----------------------------------------------------------------------------
7
+ // Requirements
8
+ // -----------------------------------------------------------------------------
9
+
10
+ import { RuleTester } from 'eslint';
11
+ import { configs } from '../../../src/index';
12
+ import parserOptionsMapper from '../../__util__/parserOptionsMapper';
13
+ import parsers from '../../__util__/helpers/parsers';
14
+ import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
15
+ import rule from '../../../src/rules/control-has-associated-label';
16
+
17
+ // -----------------------------------------------------------------------------
18
+ // Tests
19
+ // -----------------------------------------------------------------------------
20
+
21
+ const ruleTester = new RuleTester();
22
+
23
+ const ruleName = 'jsx-a11y/control-has-associated-label';
24
+
25
+ const expectedError = {
26
+ message: 'A control must be associated with a text label.',
27
+ type: 'JSXOpeningElement',
28
+ };
29
+
30
+ const alwaysValid = [
31
+ // Custom Control Components
32
+ { code: '<CustomControl><span><span>Save</span></span></CustomControl>', options: [{ depth: 3, controlComponents: ['CustomControl'] }] },
33
+ { code: '<CustomControl><span><span label="Save"></span></span></CustomControl>', options: [{ depth: 3, controlComponents: ['CustomControl'], labelAttributes: ['label'] }] },
34
+ { code: '<CustomControl>Save</CustomControl>', settings: { 'jsx-a11y': { components: { CustomControl: 'button' } } } },
35
+ // Interactive Elements
36
+ { code: '<button>Save</button>' },
37
+ { code: '<button><span>Save</span></button>' },
38
+ { code: '<button><span><span>Save</span></span></button>', options: [{ depth: 3 }] },
39
+ { code: '<button><span><span><span><span><span><span><span><span>Save</span></span></span></span></span></span></span></span></button>', options: [{ depth: 9 }] },
40
+ { code: '<button><img alt="Save" /></button>' },
41
+ { code: '<button aria-label="Save" />' },
42
+ { code: '<button><span aria-label="Save" /></button>' },
43
+ { code: '<button aria-labelledby="js_1" />' },
44
+ { code: '<button><span aria-labelledby="js_1" /></button>' },
45
+ { code: '<button>{sureWhyNot}</button>' },
46
+ { code: '<button><span><span label="Save"></span></span></button>', options: [{ depth: 3, labelAttributes: ['label'] }] },
47
+ { code: '<a href="#">Save</a>' },
48
+ { code: '<area href="#">Save</area>' },
49
+ { code: '<link>Save</link>' },
50
+ { code: '<menuitem>Save</menuitem>' },
51
+ { code: '<option>Save</option>' },
52
+ { code: '<th>Save</th>' },
53
+ // Interactive Roles
54
+ { code: '<div role="button">Save</div>' },
55
+ { code: '<div role="checkbox">Save</div>' },
56
+ { code: '<div role="columnheader">Save</div>' },
57
+ { code: '<div role="combobox">Save</div>' },
58
+ { code: '<div role="gridcell">Save</div>' },
59
+ { code: '<div role="link">Save</div>' },
60
+ { code: '<div role="menuitem">Save</div>' },
61
+ { code: '<div role="menuitemcheckbox">Save</div>' },
62
+ { code: '<div role="menuitemradio">Save</div>' },
63
+ { code: '<div role="option">Save</div>' },
64
+ { code: '<div role="progressbar">Save</div>' },
65
+ { code: '<div role="radio">Save</div>' },
66
+ { code: '<div role="rowheader">Save</div>' },
67
+ { code: '<div role="searchbox">Save</div>' },
68
+ { code: '<div role="slider">Save</div>' },
69
+ { code: '<div role="spinbutton">Save</div>' },
70
+ { code: '<div role="switch">Save</div>' },
71
+ { code: '<div role="tab">Save</div>' },
72
+ { code: '<div role="textbox">Save</div>' },
73
+ { code: '<div role="treeitem">Save</div>' },
74
+ { code: '<div role="button" aria-label="Save" />' },
75
+ { code: '<div role="checkbox" aria-label="Save" />' },
76
+ { code: '<div role="columnheader" aria-label="Save" />' },
77
+ { code: '<div role="combobox" aria-label="Save" />' },
78
+ { code: '<div role="gridcell" aria-label="Save" />' },
79
+ { code: '<div role="link" aria-label="Save" />' },
80
+ { code: '<div role="menuitem" aria-label="Save" />' },
81
+ { code: '<div role="menuitemcheckbox" aria-label="Save" />' },
82
+ { code: '<div role="menuitemradio" aria-label="Save" />' },
83
+ { code: '<div role="option" aria-label="Save" />' },
84
+ { code: '<div role="progressbar" aria-label="Save" />' },
85
+ { code: '<div role="radio" aria-label="Save" />' },
86
+ { code: '<div role="rowheader" aria-label="Save" />' },
87
+ { code: '<div role="searchbox" aria-label="Save" />' },
88
+ { code: '<div role="slider" aria-label="Save" />' },
89
+ { code: '<div role="spinbutton" aria-label="Save" />' },
90
+ { code: '<div role="switch" aria-label="Save" />' },
91
+ { code: '<div role="tab" aria-label="Save" />' },
92
+ { code: '<div role="textbox" aria-label="Save" />' },
93
+ { code: '<div role="treeitem" aria-label="Save" />' },
94
+ { code: '<div role="button" aria-labelledby="js_1" />' },
95
+ { code: '<div role="checkbox" aria-labelledby="js_1" />' },
96
+ { code: '<div role="columnheader" aria-labelledby="js_1" />' },
97
+ { code: '<div role="combobox" aria-labelledby="js_1" />' },
98
+ { code: '<div role="gridcell" aria-labelledby="Save" />' },
99
+ { code: '<div role="link" aria-labelledby="js_1" />' },
100
+ { code: '<div role="menuitem" aria-labelledby="js_1" />' },
101
+ { code: '<div role="menuitemcheckbox" aria-labelledby="js_1" />' },
102
+ { code: '<div role="menuitemradio" aria-labelledby="js_1" />' },
103
+ { code: '<div role="option" aria-labelledby="js_1" />' },
104
+ { code: '<div role="progressbar" aria-labelledby="js_1" />' },
105
+ { code: '<div role="radio" aria-labelledby="js_1" />' },
106
+ { code: '<div role="rowheader" aria-labelledby="js_1" />' },
107
+ { code: '<div role="searchbox" aria-labelledby="js_1" />' },
108
+ { code: '<div role="slider" aria-labelledby="js_1" />' },
109
+ { code: '<div role="spinbutton" aria-labelledby="js_1" />' },
110
+ { code: '<div role="switch" aria-labelledby="js_1" />' },
111
+ { code: '<div role="tab" aria-labelledby="js_1" />' },
112
+ { code: '<div role="textbox" aria-labelledby="js_1" />' },
113
+ { code: '<div role="treeitem" aria-labelledby="js_1" />' },
114
+ // Non-interactive Elements
115
+ { code: '<abbr />' },
116
+ { code: '<article />' },
117
+ { code: '<blockquote />' },
118
+ { code: '<br />' },
119
+ { code: '<caption />' },
120
+ { code: '<dd />' },
121
+ { code: '<details />' },
122
+ { code: '<dfn />' },
123
+ { code: '<dialog />' },
124
+ { code: '<dir />' },
125
+ { code: '<dl />' },
126
+ { code: '<dt />' },
127
+ { code: '<fieldset />' },
128
+ { code: '<figcaption />' },
129
+ { code: '<figure />' },
130
+ { code: '<footer />' },
131
+ { code: '<form />' },
132
+ { code: '<frame />' },
133
+ { code: '<h1 />' },
134
+ { code: '<h2 />' },
135
+ { code: '<h3 />' },
136
+ { code: '<h4 />' },
137
+ { code: '<h5 />' },
138
+ { code: '<h6 />' },
139
+ { code: '<hr />' },
140
+ { code: '<iframe />' },
141
+ { code: '<img />' },
142
+ { code: '<label />' },
143
+ { code: '<legend />' },
144
+ { code: '<li />' },
145
+ { code: '<link />' },
146
+ { code: '<main />' },
147
+ { code: '<mark />' },
148
+ { code: '<marquee />' },
149
+ { code: '<menu />' },
150
+ { code: '<meter />' },
151
+ { code: '<nav />' },
152
+ { code: '<ol />' },
153
+ { code: '<p />' },
154
+ { code: '<pre />' },
155
+ { code: '<progress />' },
156
+ { code: '<ruby />' },
157
+ { code: '<section />' },
158
+ { code: '<table />' },
159
+ { code: '<tbody />' },
160
+ { code: '<tfoot />' },
161
+ { code: '<thead />' },
162
+ { code: '<time />' },
163
+ { code: '<ul />' },
164
+ // Non-interactive Roles
165
+ { code: '<div role="alert" />' },
166
+ { code: '<div role="alertdialog" />' },
167
+ { code: '<div role="application" />' },
168
+ { code: '<div role="article" />' },
169
+ { code: '<div role="banner" />' },
170
+ { code: '<div role="cell" />' },
171
+ { code: '<div role="complementary" />' },
172
+ { code: '<div role="contentinfo" />' },
173
+ { code: '<div role="definition" />' },
174
+ { code: '<div role="dialog" />' },
175
+ { code: '<div role="directory" />' },
176
+ { code: '<div role="document" />' },
177
+ { code: '<div role="feed" />' },
178
+ { code: '<div role="figure" />' },
179
+ { code: '<div role="form" />' },
180
+ { code: '<div role="group" />' },
181
+ { code: '<div role="heading" />' },
182
+ { code: '<div role="img" />' },
183
+ { code: '<div role="list" />' },
184
+ { code: '<div role="listitem" />' },
185
+ { code: '<div role="log" />' },
186
+ { code: '<div role="main" />' },
187
+ { code: '<div role="marquee" />' },
188
+ { code: '<div role="math" />' },
189
+ { code: '<div role="navigation" />' },
190
+ { code: '<div role="none" />' },
191
+ { code: '<div role="note" />' },
192
+ { code: '<div role="presentation" />' },
193
+ { code: '<div role="region" />' },
194
+ { code: '<div role="rowgroup" />' },
195
+ { code: '<div role="search" />' },
196
+ { code: '<div role="separator" />' },
197
+ { code: '<div role="status" />' },
198
+ { code: '<div role="table" />' },
199
+ { code: '<div role="tabpanel" />' },
200
+ { code: '<div role="term" />' },
201
+ { code: '<div role="timer" />' },
202
+ { code: '<div role="tooltip" />' },
203
+ // Via config
204
+ // Inputs. Ignore them because they might get a label from a wrapping label element.
205
+ { code: '<input />' },
206
+ { code: '<input type="button" />' },
207
+ { code: '<input type="checkbox" />' },
208
+ { code: '<input type="color" />' },
209
+ { code: '<input type="date" />' },
210
+ { code: '<input type="datetime" />' },
211
+ { code: '<input type="email" />' },
212
+ { code: '<input type="file" />' },
213
+ { code: '<input type="hidden" />' },
214
+ { code: '<input type="hidden" name="bot-field"/>' },
215
+ { code: '<input type="hidden" name="form-name" value="Contact Form"/>' },
216
+ { code: '<input type="image" />' },
217
+ { code: '<input type="month" />' },
218
+ { code: '<input type="number" />' },
219
+ { code: '<input type="password" />' },
220
+ { code: '<input type="radio" />' },
221
+ { code: '<input type="range" />' },
222
+ { code: '<input type="reset" />' },
223
+ { code: '<input type="search" />' },
224
+ { code: '<input type="submit" />' },
225
+ { code: '<input type="tel" />' },
226
+ { code: '<input type="text" />' },
227
+ { code: '<label>Foo <input type="text" /></label>' },
228
+ { code: '<input name={field.name} id="foo" type="text" value={field.value} disabled={isDisabled} onChange={changeText(field.onChange, field.name)} onBlur={field.onBlur} />' },
229
+ { code: '<input type="time" />' },
230
+ { code: '<input type="url" />' },
231
+ { code: '<input type="week" />' },
232
+ // Marginal interactive elements. It is difficult to insist that these
233
+ // elements contain a text label.
234
+ { code: '<audio />' },
235
+ { code: '<canvas />' },
236
+ { code: '<embed />' },
237
+ { code: '<textarea />' },
238
+ { code: '<tr />' },
239
+ { code: '<video />' },
240
+ // Interactive roles to ignore
241
+ { code: '<div role="grid" />' },
242
+ { code: '<div role="listbox" />' },
243
+ { code: '<div role="menu" />' },
244
+ { code: '<div role="menubar" />' },
245
+ { code: '<div role="radiogroup" />' },
246
+ { code: '<div role="row" />' },
247
+ { code: '<div role="tablist" />' },
248
+ { code: '<div role="toolbar" />' },
249
+ { code: '<div role="tree" />' },
250
+ { code: '<div role="treegrid" />' },
251
+ ];
252
+ const neverValid = [
253
+ { code: '<button />', errors: [expectedError] },
254
+ { code: '<button><span /></button>', errors: [expectedError] },
255
+ { code: '<button><img /></button>', errors: [expectedError] },
256
+ { code: '<button><span title="This is not a real label" /></button>', errors: [expectedError] },
257
+ { code: '<button><span><span><span>Save</span></span></span></button>', options: [{ depth: 3 }], errors: [expectedError] },
258
+ { code: '<CustomControl><span><span></span></span></CustomControl>', options: [{ depth: 3, controlComponents: ['CustomControl'] }], errors: [expectedError] },
259
+ { code: '<CustomControl></CustomControl>', errors: [expectedError], settings: { 'jsx-a11y': { components: { CustomControl: 'button' } } } },
260
+ { code: '<a href="#" />', errors: [expectedError] },
261
+ { code: '<area href="#" />', errors: [expectedError] },
262
+ { code: '<menuitem />', errors: [expectedError] },
263
+ { code: '<option />', errors: [expectedError] },
264
+ { code: '<th />', errors: [expectedError] },
265
+ { code: '<td />', errors: [expectedError] },
266
+ // Interactive Roles
267
+ { code: '<div role="button" />', errors: [expectedError] },
268
+ { code: '<div role="checkbox" />', errors: [expectedError] },
269
+ { code: '<div role="columnheader" />', errors: [expectedError] },
270
+ { code: '<div role="combobox" />', errors: [expectedError] },
271
+ { code: '<div role="link" />', errors: [expectedError] },
272
+ { code: '<div role="gridcell" />', errors: [expectedError] },
273
+ { code: '<div role="menuitem" />', errors: [expectedError] },
274
+ { code: '<div role="menuitemcheckbox" />', errors: [expectedError] },
275
+ { code: '<div role="menuitemradio" />', errors: [expectedError] },
276
+ { code: '<div role="option" />', errors: [expectedError] },
277
+ { code: '<div role="progressbar" />', errors: [expectedError] },
278
+ { code: '<div role="radio" />', errors: [expectedError] },
279
+ { code: '<div role="rowheader" />', errors: [expectedError] },
280
+ { code: '<div role="scrollbar" />', errors: [expectedError] },
281
+ { code: '<div role="searchbox" />', errors: [expectedError] },
282
+ { code: '<div role="slider" />', errors: [expectedError] },
283
+ { code: '<div role="spinbutton" />', errors: [expectedError] },
284
+ { code: '<div role="switch" />', errors: [expectedError] },
285
+ { code: '<div role="tab" />', errors: [expectedError] },
286
+ { code: '<div role="textbox" />', errors: [expectedError] },
287
+ ];
288
+
289
+ const recommendedOptions = (configs.recommended.rules[ruleName][1] || {});
290
+ ruleTester.run(`${ruleName}:recommended`, rule, {
291
+ valid: parsers.all([].concat(
292
+ ...alwaysValid,
293
+ ))
294
+ .map(ruleOptionsMapperFactory(recommendedOptions))
295
+ .map(parserOptionsMapper),
296
+ invalid: parsers.all([].concat(
297
+ ...neverValid,
298
+ ))
299
+ .map(ruleOptionsMapperFactory(recommendedOptions))
300
+ .map(parserOptionsMapper),
301
+ });
302
+
303
+ const strictOptions = (configs.strict.rules[ruleName][1] || {});
304
+ ruleTester.run(`${ruleName}:strict`, rule, {
305
+ valid: parsers.all([].concat(
306
+ ...alwaysValid,
307
+ ))
308
+ .map(ruleOptionsMapperFactory(strictOptions))
309
+ .map(parserOptionsMapper),
310
+ invalid: parsers.all([].concat(
311
+ ...neverValid,
312
+ ))
313
+ .map(ruleOptionsMapperFactory(strictOptions))
314
+ .map(parserOptionsMapper),
315
+ });
316
+
317
+ ruleTester.run(`${ruleName}:no-config`, rule, {
318
+ valid: parsers.all([].concat(
319
+ { code: '<input type="hidden" />' },
320
+ { code: '<input type="text" aria-hidden="true" />' },
321
+ ))
322
+ .map(parserOptionsMapper),
323
+ invalid: parsers.all([].concat(
324
+ { code: '<input type="text" />', errors: [expectedError] },
325
+ ))
326
+ .map(parserOptionsMapper),
327
+ });
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @fileoverview Enforce heading (h1, h2, etc) elements contain accessible content.
3
+ * @author Ethan Cohen
4
+ */
5
+
6
+ // -----------------------------------------------------------------------------
7
+ // Requirements
8
+ // -----------------------------------------------------------------------------
9
+
10
+ import { RuleTester } from 'eslint';
11
+ import parserOptionsMapper from '../../__util__/parserOptionsMapper';
12
+ import parsers from '../../__util__/helpers/parsers';
13
+ import rule from '../../../src/rules/heading-has-content';
14
+
15
+ // -----------------------------------------------------------------------------
16
+ // Tests
17
+ // -----------------------------------------------------------------------------
18
+
19
+ const ruleTester = new RuleTester();
20
+
21
+ const expectedError = {
22
+ message: 'Headings must have content and the content must be accessible by a screen reader.',
23
+ type: 'JSXOpeningElement',
24
+ };
25
+
26
+ const components = [{
27
+ components: ['Heading', 'Title'],
28
+ }];
29
+
30
+ const componentsSettings = {
31
+ 'jsx-a11y': {
32
+ components: {
33
+ CustomInput: 'input',
34
+ Title: 'h1',
35
+ Heading: 'h2',
36
+ },
37
+ },
38
+ };
39
+
40
+ ruleTester.run('heading-has-content', rule, {
41
+ valid: parsers.all([].concat(
42
+ // DEFAULT ELEMENT TESTS
43
+ { code: '<div />;' },
44
+ { code: '<h1>Foo</h1>' },
45
+ { code: '<h2>Foo</h2>' },
46
+ { code: '<h3>Foo</h3>' },
47
+ { code: '<h4>Foo</h4>' },
48
+ { code: '<h5>Foo</h5>' },
49
+ { code: '<h6>Foo</h6>' },
50
+ { code: '<h6>123</h6>' },
51
+ { code: '<h1><Bar /></h1>' },
52
+ { code: '<h1>{foo}</h1>' },
53
+ { code: '<h1>{foo.bar}</h1>' },
54
+ { code: '<h1 dangerouslySetInnerHTML={{ __html: "foo" }} />' },
55
+ { code: '<h1 children={children} />' },
56
+ // CUSTOM ELEMENT TESTS FOR COMPONENTS OPTION
57
+ { code: '<Heading>Foo</Heading>', options: components },
58
+ { code: '<Title>Foo</Title>', options: components },
59
+ { code: '<Heading><Bar /></Heading>', options: components },
60
+ { code: '<Heading>{foo}</Heading>', options: components },
61
+ { code: '<Heading>{foo.bar}</Heading>', options: components },
62
+ { code: '<Heading dangerouslySetInnerHTML={{ __html: "foo" }} />', options: components },
63
+ { code: '<Heading children={children} />', options: components },
64
+ { code: '<h1 aria-hidden />' },
65
+ // CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS
66
+ { code: '<Heading>Foo</Heading>', settings: componentsSettings },
67
+ { code: '<h1><CustomInput type="hidden" /></h1>' },
68
+ )).map(parserOptionsMapper),
69
+ invalid: parsers.all([].concat(
70
+ // DEFAULT ELEMENT TESTS
71
+ { code: '<h1 />', errors: [expectedError] },
72
+ { code: '<h1><Bar aria-hidden /></h1>', errors: [expectedError] },
73
+ { code: '<h1>{undefined}</h1>', errors: [expectedError] },
74
+ { code: '<h1><input type="hidden" /></h1>', errors: [expectedError] },
75
+
76
+ // CUSTOM ELEMENT TESTS FOR COMPONENTS OPTION
77
+ { code: '<Heading />', errors: [expectedError], options: components },
78
+ { code: '<Heading><Bar aria-hidden /></Heading>', errors: [expectedError], options: components },
79
+ { code: '<Heading>{undefined}</Heading>', errors: [expectedError], options: components },
80
+
81
+ // CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS
82
+ { code: '<Heading />', errors: [expectedError], settings: componentsSettings },
83
+ { code: '<h1><CustomInput type="hidden" /></h1>', errors: [expectedError], settings: componentsSettings },
84
+ )).map(parserOptionsMapper),
85
+ });
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @fileoverview Enforce html element has lang prop.
3
+ * @author Ethan Cohen
4
+ */
5
+
6
+ // -----------------------------------------------------------------------------
7
+ // Requirements
8
+ // -----------------------------------------------------------------------------
9
+
10
+ import { RuleTester } from 'eslint';
11
+ import parserOptionsMapper from '../../__util__/parserOptionsMapper';
12
+ import parsers from '../../__util__/helpers/parsers';
13
+ import rule from '../../../src/rules/html-has-lang';
14
+
15
+ // -----------------------------------------------------------------------------
16
+ // Tests
17
+ // -----------------------------------------------------------------------------
18
+
19
+ const ruleTester = new RuleTester();
20
+
21
+ const expectedError = {
22
+ message: '<html> elements must have the lang prop.',
23
+ type: 'JSXOpeningElement',
24
+ };
25
+
26
+ ruleTester.run('html-has-lang', rule, {
27
+ valid: parsers.all([].concat(
28
+ { code: '<div />;' },
29
+ { code: '<html lang="en" />' },
30
+ { code: '<html lang="en-US" />' },
31
+ { code: '<html lang={foo} />' },
32
+ { code: '<html lang />' },
33
+ { code: '<HTML />' },
34
+ { code: '<HTMLTop lang="en" />', errors: [expectedError], settings: { 'jsx-a11y': { components: { HTMLTop: 'html' } } } },
35
+ )).map(parserOptionsMapper),
36
+ invalid: parsers.all([].concat(
37
+ { code: '<html />', errors: [expectedError] },
38
+ { code: '<html {...props} />', errors: [expectedError] },
39
+ { code: '<html lang={undefined} />', errors: [expectedError] },
40
+ { code: '<HTMLTop />', errors: [expectedError], settings: { 'jsx-a11y': { components: { HTMLTop: 'html' } } } },
41
+ )).map(parserOptionsMapper),
42
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @fileoverview Enforce iframe elements have a title attribute.
3
+ * @author Ethan Cohen
4
+ */
5
+
6
+ // -----------------------------------------------------------------------------
7
+ // Requirements
8
+ // -----------------------------------------------------------------------------
9
+
10
+ import { RuleTester } from 'eslint';
11
+ import parserOptionsMapper from '../../__util__/parserOptionsMapper';
12
+ import parsers from '../../__util__/helpers/parsers';
13
+ import rule from '../../../src/rules/iframe-has-title';
14
+
15
+ // -----------------------------------------------------------------------------
16
+ // Tests
17
+ // -----------------------------------------------------------------------------
18
+
19
+ const ruleTester = new RuleTester();
20
+
21
+ const expectedError = {
22
+ message: '<iframe> elements must have a unique title property.',
23
+ type: 'JSXOpeningElement',
24
+ };
25
+
26
+ const componentsSettings = {
27
+ 'jsx-a11y': {
28
+ components: {
29
+ FooComponent: 'iframe',
30
+ },
31
+ },
32
+ };
33
+
34
+ ruleTester.run('html-has-lang', rule, {
35
+ valid: parsers.all([].concat(
36
+ { code: '<div />;' },
37
+ { code: '<iframe title="Unique title" />' },
38
+ { code: '<iframe title={foo} />' },
39
+ { code: '<FooComponent />' },
40
+ { code: '<FooComponent title="Unique title" />', settings: componentsSettings },
41
+ )).map(parserOptionsMapper),
42
+ invalid: parsers.all([].concat(
43
+ { code: '<iframe />', errors: [expectedError] },
44
+ { code: '<iframe {...props} />', errors: [expectedError] },
45
+ { code: '<iframe title={undefined} />', errors: [expectedError] },
46
+ { code: '<iframe title="" />', errors: [expectedError] },
47
+ { code: '<iframe title={false} />', errors: [expectedError] },
48
+ { code: '<iframe title={true} />', errors: [expectedError] },
49
+ { code: "<iframe title={''} />", errors: [expectedError] },
50
+ { code: '<iframe title={``} />', errors: [expectedError] },
51
+ { code: '<iframe title={""} />', errors: [expectedError] },
52
+ { code: '<iframe title={42} />', errors: [expectedError] },
53
+ { code: '<FooComponent />', errors: [expectedError], settings: componentsSettings },
54
+ )).map(parserOptionsMapper),
55
+ });