eslint-plugin-react-a11y 2.1.0

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 (83) hide show
  1. package/AGENTS.md +110 -0
  2. package/CHANGELOG.md +37 -0
  3. package/LICENSE +22 -0
  4. package/README.md +213 -0
  5. package/package.json +79 -0
  6. package/src/index.d.ts +40 -0
  7. package/src/index.js +279 -0
  8. package/src/rules/alt-text.d.ts +23 -0
  9. package/src/rules/alt-text.js +205 -0
  10. package/src/rules/anchor-ambiguous-text.d.ts +20 -0
  11. package/src/rules/anchor-ambiguous-text.js +169 -0
  12. package/src/rules/anchor-has-content.d.ts +20 -0
  13. package/src/rules/anchor-has-content.js +85 -0
  14. package/src/rules/anchor-is-valid.d.ts +23 -0
  15. package/src/rules/anchor-is-valid.js +100 -0
  16. package/src/rules/aria-activedescendant-has-tabindex.d.ts +15 -0
  17. package/src/rules/aria-activedescendant-has-tabindex.js +63 -0
  18. package/src/rules/aria-props.d.ts +15 -0
  19. package/src/rules/aria-props.js +51 -0
  20. package/src/rules/aria-role.d.ts +21 -0
  21. package/src/rules/aria-role.js +72 -0
  22. package/src/rules/aria-unsupported-elements.d.ts +15 -0
  23. package/src/rules/aria-unsupported-elements.js +54 -0
  24. package/src/rules/autocomplete-valid.d.ts +20 -0
  25. package/src/rules/autocomplete-valid.js +79 -0
  26. package/src/rules/click-events-have-key-events.d.ts +15 -0
  27. package/src/rules/click-events-have-key-events.js +60 -0
  28. package/src/rules/control-has-associated-label.d.ts +24 -0
  29. package/src/rules/control-has-associated-label.js +178 -0
  30. package/src/rules/heading-has-content.d.ts +20 -0
  31. package/src/rules/heading-has-content.js +72 -0
  32. package/src/rules/html-has-lang.d.ts +15 -0
  33. package/src/rules/html-has-lang.js +50 -0
  34. package/src/rules/iframe-has-title.d.ts +15 -0
  35. package/src/rules/iframe-has-title.js +51 -0
  36. package/src/rules/img-redundant-alt.d.ts +21 -0
  37. package/src/rules/img-redundant-alt.js +85 -0
  38. package/src/rules/interactive-supports-focus.d.ts +20 -0
  39. package/src/rules/interactive-supports-focus.js +81 -0
  40. package/src/rules/label-has-associated-control.d.ts +24 -0
  41. package/src/rules/label-has-associated-control.js +93 -0
  42. package/src/rules/lang.d.ts +15 -0
  43. package/src/rules/lang.js +53 -0
  44. package/src/rules/media-has-caption.d.ts +22 -0
  45. package/src/rules/media-has-caption.js +84 -0
  46. package/src/rules/mouse-events-have-key-events.d.ts +17 -0
  47. package/src/rules/mouse-events-have-key-events.js +62 -0
  48. package/src/rules/no-access-key.d.ts +15 -0
  49. package/src/rules/no-access-key.js +43 -0
  50. package/src/rules/no-aria-hidden-on-focusable.d.ts +15 -0
  51. package/src/rules/no-aria-hidden-on-focusable.js +67 -0
  52. package/src/rules/no-autofocus.d.ts +20 -0
  53. package/src/rules/no-autofocus.js +59 -0
  54. package/src/rules/no-distracting-elements.d.ts +20 -0
  55. package/src/rules/no-distracting-elements.js +64 -0
  56. package/src/rules/no-interactive-element-to-noninteractive-role.d.ts +18 -0
  57. package/src/rules/no-interactive-element-to-noninteractive-role.js +86 -0
  58. package/src/rules/no-keyboard-inaccessible-elements.d.ts +24 -0
  59. package/src/rules/no-keyboard-inaccessible-elements.js +136 -0
  60. package/src/rules/no-missing-aria-labels.d.ts +24 -0
  61. package/src/rules/no-missing-aria-labels.js +131 -0
  62. package/src/rules/no-noninteractive-element-interactions.d.ts +20 -0
  63. package/src/rules/no-noninteractive-element-interactions.js +78 -0
  64. package/src/rules/no-noninteractive-element-to-interactive-role.d.ts +18 -0
  65. package/src/rules/no-noninteractive-element-to-interactive-role.js +95 -0
  66. package/src/rules/no-noninteractive-tabindex.d.ts +22 -0
  67. package/src/rules/no-noninteractive-tabindex.js +172 -0
  68. package/src/rules/no-redundant-roles.d.ts +24 -0
  69. package/src/rules/no-redundant-roles.js +115 -0
  70. package/src/rules/no-static-element-interactions.d.ts +20 -0
  71. package/src/rules/no-static-element-interactions.js +72 -0
  72. package/src/rules/prefer-tag-over-role.d.ts +15 -0
  73. package/src/rules/prefer-tag-over-role.js +101 -0
  74. package/src/rules/role-has-required-aria-props.d.ts +15 -0
  75. package/src/rules/role-has-required-aria-props.js +65 -0
  76. package/src/rules/role-supports-aria-props.d.ts +15 -0
  77. package/src/rules/role-supports-aria-props.js +115 -0
  78. package/src/rules/scope.d.ts +15 -0
  79. package/src/rules/scope.js +49 -0
  80. package/src/rules/tabindex-no-positive.d.ts +15 -0
  81. package/src/rules/tabindex-no-positive.js +55 -0
  82. package/src/types/index.d.ts +208 -0
  83. package/src/types/index.js +7 -0
package/src/index.js ADDED
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2025 Ofri Peretz
4
+ * Licensed under the MIT License. Use of this source code is governed by the
5
+ * MIT license that can be found in the LICENSE file.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.configs = exports.plugin = exports.rules = void 0;
9
+ const tslib_1 = require("tslib");
10
+ // Anchor rules
11
+ const anchor_ambiguous_text_1 = require("./rules/anchor-ambiguous-text");
12
+ const anchor_has_content_1 = require("./rules/anchor-has-content");
13
+ const anchor_is_valid_1 = require("./rules/anchor-is-valid");
14
+ // ARIA rules
15
+ const aria_activedescendant_has_tabindex_1 = require("./rules/aria-activedescendant-has-tabindex");
16
+ const aria_props_1 = require("./rules/aria-props");
17
+ const aria_role_1 = require("./rules/aria-role");
18
+ const aria_unsupported_elements_1 = require("./rules/aria-unsupported-elements");
19
+ // Form & Input rules
20
+ const autocomplete_valid_1 = require("./rules/autocomplete-valid");
21
+ const control_has_associated_label_1 = require("./rules/control-has-associated-label");
22
+ const label_has_associated_control_1 = require("./rules/label-has-associated-control");
23
+ // Event rules
24
+ const click_events_have_key_events_1 = require("./rules/click-events-have-key-events");
25
+ const mouse_events_have_key_events_1 = require("./rules/mouse-events-have-key-events");
26
+ // Content rules
27
+ const heading_has_content_1 = require("./rules/heading-has-content");
28
+ const html_has_lang_1 = require("./rules/html-has-lang");
29
+ const iframe_has_title_1 = require("./rules/iframe-has-title");
30
+ const lang_1 = require("./rules/lang");
31
+ const media_has_caption_1 = require("./rules/media-has-caption");
32
+ // Image rules
33
+ const img_redundant_alt_1 = require("./rules/img-redundant-alt");
34
+ const alt_text_1 = require("./rules/alt-text");
35
+ // Interactive element rules
36
+ const interactive_supports_focus_1 = require("./rules/interactive-supports-focus");
37
+ const no_interactive_element_to_noninteractive_role_1 = require("./rules/no-interactive-element-to-noninteractive-role");
38
+ const no_noninteractive_element_interactions_1 = require("./rules/no-noninteractive-element-interactions");
39
+ const no_noninteractive_element_to_interactive_role_1 = require("./rules/no-noninteractive-element-to-interactive-role");
40
+ const no_noninteractive_tabindex_1 = require("./rules/no-noninteractive-tabindex");
41
+ const no_static_element_interactions_1 = require("./rules/no-static-element-interactions");
42
+ // Focus & Navigation rules
43
+ const no_access_key_1 = require("./rules/no-access-key");
44
+ const no_aria_hidden_on_focusable_1 = require("./rules/no-aria-hidden-on-focusable");
45
+ const no_autofocus_1 = require("./rules/no-autofocus");
46
+ const no_keyboard_inaccessible_elements_1 = require("./rules/no-keyboard-inaccessible-elements");
47
+ const tabindex_no_positive_1 = require("./rules/tabindex-no-positive");
48
+ // Visual & Distraction rules
49
+ const no_distracting_elements_1 = require("./rules/no-distracting-elements");
50
+ const no_missing_aria_labels_1 = require("./rules/no-missing-aria-labels");
51
+ const no_redundant_roles_1 = require("./rules/no-redundant-roles");
52
+ // Role rules
53
+ const role_has_required_aria_props_1 = require("./rules/role-has-required-aria-props");
54
+ const role_supports_aria_props_1 = require("./rules/role-supports-aria-props");
55
+ const prefer_tag_over_role_1 = require("./rules/prefer-tag-over-role");
56
+ // Scope rule
57
+ const scope_1 = require("./rules/scope");
58
+ /**
59
+ * Collection of all accessibility ESLint rules
60
+ */
61
+ exports.rules = {
62
+ // Anchor rules
63
+ 'anchor-ambiguous-text': anchor_ambiguous_text_1.anchorAmbiguousText,
64
+ 'anchor-has-content': anchor_has_content_1.anchorHasContent,
65
+ 'anchor-is-valid': anchor_is_valid_1.anchorIsValid,
66
+ // ARIA rules
67
+ 'aria-activedescendant-has-tabindex': aria_activedescendant_has_tabindex_1.ariaActivedescendantHasTabindex,
68
+ 'aria-props': aria_props_1.ariaProps,
69
+ 'aria-role': aria_role_1.ariaRole,
70
+ 'aria-unsupported-elements': aria_unsupported_elements_1.ariaUnsupportedElements,
71
+ // Form & Input rules
72
+ 'autocomplete-valid': autocomplete_valid_1.autocompleteValid,
73
+ 'control-has-associated-label': control_has_associated_label_1.controlHasAssociatedLabel,
74
+ 'label-has-associated-control': label_has_associated_control_1.labelHasAssociatedControl,
75
+ // Event rules
76
+ 'click-events-have-key-events': click_events_have_key_events_1.clickEventsHaveKeyEvents,
77
+ 'mouse-events-have-key-events': mouse_events_have_key_events_1.mouseEventsHaveKeyEvents,
78
+ // Content rules
79
+ 'heading-has-content': heading_has_content_1.headingHasContent,
80
+ 'html-has-lang': html_has_lang_1.htmlHasLang,
81
+ 'iframe-has-title': iframe_has_title_1.iframeHasTitle,
82
+ 'lang': lang_1.lang,
83
+ 'media-has-caption': media_has_caption_1.mediaHasCaption,
84
+ // Image rules
85
+ 'img-redundant-alt': img_redundant_alt_1.imgRedundantAlt,
86
+ 'alt-text': alt_text_1.altText,
87
+ // Interactive element rules
88
+ 'interactive-supports-focus': interactive_supports_focus_1.interactiveSupportsFocus,
89
+ 'no-interactive-element-to-noninteractive-role': no_interactive_element_to_noninteractive_role_1.noInteractiveElementToNoninteractiveRole,
90
+ 'no-noninteractive-element-interactions': no_noninteractive_element_interactions_1.noNoninteractiveElementInteractions,
91
+ 'no-noninteractive-element-to-interactive-role': no_noninteractive_element_to_interactive_role_1.noNoninteractiveElementToInteractiveRole,
92
+ 'no-noninteractive-tabindex': no_noninteractive_tabindex_1.noNoninteractiveTabindex,
93
+ 'no-static-element-interactions': no_static_element_interactions_1.noStaticElementInteractions,
94
+ // Focus & Navigation rules
95
+ 'no-access-key': no_access_key_1.noAccessKey,
96
+ 'no-aria-hidden-on-focusable': no_aria_hidden_on_focusable_1.noAriaHiddenOnFocusable,
97
+ 'no-autofocus': no_autofocus_1.noAutofocus,
98
+ 'no-keyboard-inaccessible-elements': no_keyboard_inaccessible_elements_1.noKeyboardInaccessibleElements,
99
+ 'tabindex-no-positive': tabindex_no_positive_1.tabindexNoPositive,
100
+ // Visual & Distraction rules
101
+ 'no-distracting-elements': no_distracting_elements_1.noDistractingElements,
102
+ 'no-missing-aria-labels': no_missing_aria_labels_1.noMissingAriaLabels,
103
+ 'no-redundant-roles': no_redundant_roles_1.noRedundantRoles,
104
+ // Role rules
105
+ 'role-has-required-aria-props': role_has_required_aria_props_1.roleHasRequiredAriaProps,
106
+ 'role-supports-aria-props': role_supports_aria_props_1.roleSupportsAriaProps,
107
+ 'prefer-tag-over-role': prefer_tag_over_role_1.preferTagOverRole,
108
+ // Scope rule
109
+ 'scope': scope_1.scope,
110
+ };
111
+ /**
112
+ * ESLint Plugin object
113
+ */
114
+ exports.plugin = {
115
+ meta: {
116
+ name: 'eslint-plugin-react-a11y',
117
+ version: '1.0.0',
118
+ },
119
+ rules: exports.rules,
120
+ };
121
+ /**
122
+ * Preset configurations for accessibility rules
123
+ */
124
+ exports.configs = {
125
+ /**
126
+ * Recommended accessibility configuration
127
+ *
128
+ * Enables critical accessibility rules with sensible severity levels:
129
+ * - WCAG Level A violations as errors
130
+ * - WCAG Level AA/AAA as warnings
131
+ */
132
+ recommended: {
133
+ plugins: {
134
+ 'react-a11y': exports.plugin,
135
+ },
136
+ rules: {
137
+ // WCAG 2.1 Level A - Critical (errors)
138
+ 'react-a11y/alt-text': 'error',
139
+ 'react-a11y/anchor-has-content': 'error',
140
+ 'react-a11y/aria-props': 'error',
141
+ 'react-a11y/aria-role': 'error',
142
+ 'react-a11y/aria-unsupported-elements': 'error',
143
+ 'react-a11y/role-has-required-aria-props': 'error',
144
+ 'react-a11y/role-supports-aria-props': 'error',
145
+ 'react-a11y/html-has-lang': 'error',
146
+ 'react-a11y/lang': 'error',
147
+ 'react-a11y/heading-has-content': 'error',
148
+ 'react-a11y/iframe-has-title': 'error',
149
+ 'react-a11y/no-distracting-elements': 'error',
150
+ 'react-a11y/scope': 'error',
151
+ 'react-a11y/no-aria-hidden-on-focusable': 'error',
152
+ // WCAG 2.1 Level A - Keyboard accessibility
153
+ 'react-a11y/click-events-have-key-events': 'error',
154
+ 'react-a11y/no-keyboard-inaccessible-elements': 'error',
155
+ 'react-a11y/interactive-supports-focus': 'error',
156
+ 'react-a11y/no-noninteractive-element-interactions': 'warn',
157
+ 'react-a11y/no-static-element-interactions': 'warn',
158
+ // WCAG 2.1 Level AA - Important (warnings)
159
+ 'react-a11y/anchor-is-valid': 'warn',
160
+ 'react-a11y/anchor-ambiguous-text': 'warn',
161
+ 'react-a11y/autocomplete-valid': 'warn',
162
+ 'react-a11y/control-has-associated-label': 'warn',
163
+ 'react-a11y/label-has-associated-control': 'warn',
164
+ 'react-a11y/media-has-caption': 'warn',
165
+ 'react-a11y/mouse-events-have-key-events': 'warn',
166
+ 'react-a11y/no-access-key': 'warn',
167
+ 'react-a11y/no-autofocus': 'warn',
168
+ 'react-a11y/no-redundant-roles': 'warn',
169
+ 'react-a11y/tabindex-no-positive': 'warn',
170
+ 'react-a11y/aria-activedescendant-has-tabindex': 'warn',
171
+ // Best practices (warnings)
172
+ 'react-a11y/img-redundant-alt': 'warn',
173
+ 'react-a11y/no-missing-aria-labels': 'warn',
174
+ 'react-a11y/prefer-tag-over-role': 'warn',
175
+ 'react-a11y/no-interactive-element-to-noninteractive-role': 'warn',
176
+ 'react-a11y/no-noninteractive-element-to-interactive-role': 'warn',
177
+ 'react-a11y/no-noninteractive-tabindex': 'warn',
178
+ },
179
+ },
180
+ /**
181
+ * Strict accessibility configuration
182
+ *
183
+ * All accessibility rules set to 'error' for maximum WCAG compliance
184
+ */
185
+ strict: {
186
+ plugins: {
187
+ 'react-a11y': exports.plugin,
188
+ },
189
+ rules: Object.fromEntries(Object.keys(exports.rules).map(ruleName => [`react-a11y/${ruleName}`, 'error'])),
190
+ },
191
+ /**
192
+ * WCAG 2.1 Level A configuration
193
+ *
194
+ * Only rules required for WCAG 2.1 Level A compliance
195
+ */
196
+ 'wcag-a': {
197
+ plugins: {
198
+ 'react-a11y': exports.plugin,
199
+ },
200
+ rules: {
201
+ // 1.1.1 Non-text Content
202
+ 'react-a11y/alt-text': 'error',
203
+ // 1.3.1 Info and Relationships
204
+ 'react-a11y/heading-has-content': 'error',
205
+ 'react-a11y/scope': 'error',
206
+ 'react-a11y/role-has-required-aria-props': 'error',
207
+ // 2.1.1 Keyboard
208
+ 'react-a11y/click-events-have-key-events': 'error',
209
+ 'react-a11y/no-keyboard-inaccessible-elements': 'error',
210
+ 'react-a11y/interactive-supports-focus': 'error',
211
+ // 2.4.4 Link Purpose
212
+ 'react-a11y/anchor-has-content': 'error',
213
+ // 3.1.1 Language of Page
214
+ 'react-a11y/html-has-lang': 'error',
215
+ 'react-a11y/lang': 'error',
216
+ // 4.1.1 Parsing
217
+ 'react-a11y/aria-props': 'error',
218
+ 'react-a11y/aria-role': 'error',
219
+ 'react-a11y/aria-unsupported-elements': 'error',
220
+ // 4.1.2 Name, Role, Value
221
+ 'react-a11y/role-supports-aria-props': 'error',
222
+ 'react-a11y/iframe-has-title': 'error',
223
+ // 2.3.1 Three Flashes or Below Threshold
224
+ 'react-a11y/no-distracting-elements': 'error',
225
+ },
226
+ },
227
+ /**
228
+ * WCAG 2.1 Level AA configuration
229
+ *
230
+ * Includes Level A + additional rules for Level AA compliance
231
+ */
232
+ 'wcag-aa': {
233
+ plugins: {
234
+ 'react-a11y': exports.plugin,
235
+ },
236
+ rules: {
237
+ // All Level A rules
238
+ 'react-a11y/alt-text': 'error',
239
+ 'react-a11y/heading-has-content': 'error',
240
+ 'react-a11y/scope': 'error',
241
+ 'react-a11y/role-has-required-aria-props': 'error',
242
+ 'react-a11y/click-events-have-key-events': 'error',
243
+ 'react-a11y/no-keyboard-inaccessible-elements': 'error',
244
+ 'react-a11y/interactive-supports-focus': 'error',
245
+ 'react-a11y/anchor-has-content': 'error',
246
+ 'react-a11y/html-has-lang': 'error',
247
+ 'react-a11y/lang': 'error',
248
+ 'react-a11y/aria-props': 'error',
249
+ 'react-a11y/aria-role': 'error',
250
+ 'react-a11y/aria-unsupported-elements': 'error',
251
+ 'react-a11y/role-supports-aria-props': 'error',
252
+ 'react-a11y/iframe-has-title': 'error',
253
+ 'react-a11y/no-distracting-elements': 'error',
254
+ // Level AA additions
255
+ // 1.2.4 Captions (Live) & 1.2.5 Audio Description
256
+ 'react-a11y/media-has-caption': 'error',
257
+ // 1.3.5 Identify Input Purpose
258
+ 'react-a11y/autocomplete-valid': 'error',
259
+ // 1.4.4 Resize text (no autofocus related)
260
+ 'react-a11y/no-autofocus': 'error',
261
+ // 2.4.6 Headings and Labels
262
+ 'react-a11y/control-has-associated-label': 'error',
263
+ 'react-a11y/label-has-associated-control': 'error',
264
+ // 2.4.7 Focus Visible
265
+ 'react-a11y/tabindex-no-positive': 'error',
266
+ 'react-a11y/no-aria-hidden-on-focusable': 'error',
267
+ // 3.2.1 On Focus
268
+ 'react-a11y/mouse-events-have-key-events': 'error',
269
+ },
270
+ },
271
+ };
272
+ /**
273
+ * Default export for ESLint plugin
274
+ */
275
+ exports.default = exports.plugin;
276
+ /**
277
+ * Re-export types (will be created in types/index.ts)
278
+ */
279
+ tslib_1.__exportStar(require("./types/index"), exports);
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Copyright (c) 2025 Ofri Peretz
3
+ * Licensed under the MIT License. Use of this source code is governed by the
4
+ * MIT license that can be found in the LICENSE file.
5
+ */
6
+ /**
7
+ * ESLint Rule: alt-text
8
+ * Enforce alt text on images with user impact context
9
+ * Matches jsx-a11y naming convention
10
+ */
11
+ import type { TSESLint } from '@interlace/eslint-devkit';
12
+ type MessageIds = 'missingAlt' | 'emptyAlt' | 'addDescriptiveAlt' | 'useEmptyAlt';
13
+ export interface Options {
14
+ /** Allow aria-label as alternative to alt text. Default: false */
15
+ allowAriaLabel?: boolean;
16
+ /** Allow aria-labelledby as alternative to alt text. Default: false */
17
+ allowAriaLabelledby?: boolean;
18
+ }
19
+ type RuleOptions = [Options?];
20
+ export declare const altText: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
21
+ name: string;
22
+ };
23
+ export {};
@@ -0,0 +1,205 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2025 Ofri Peretz
4
+ * Licensed under the MIT License. Use of this source code is governed by the
5
+ * MIT license that can be found in the LICENSE file.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.altText = void 0;
9
+ const eslint_devkit_1 = require("@interlace/eslint-devkit");
10
+ const eslint_devkit_2 = require("@interlace/eslint-devkit");
11
+ exports.altText = (0, eslint_devkit_2.createRule)({
12
+ name: 'alt-text',
13
+ meta: {
14
+ type: 'problem',
15
+ docs: {
16
+ description: 'Enforce alt text on images with accessibility impact context',
17
+ },
18
+ fixable: 'code',
19
+ hasSuggestions: true,
20
+ messages: {
21
+ // 🎯 Token optimization: 45% reduction (51→28 tokens) - image alt text improves accessibility
22
+ missingAlt: (0, eslint_devkit_1.formatLLMMessage)({
23
+ icon: eslint_devkit_1.MessageIcons.ACCESSIBILITY,
24
+ issueName: 'Image missing alt text',
25
+ cwe: 'CWE-252',
26
+ description: 'Image missing alt text',
27
+ severity: 'CRITICAL',
28
+ fix: 'Add alt="Descriptive text about image"',
29
+ documentationLink: 'https://www.w3.org/WAI/tutorials/images/',
30
+ }),
31
+ emptyAlt: (0, eslint_devkit_1.formatLLMMessage)({
32
+ icon: eslint_devkit_1.MessageIcons.ACCESSIBILITY,
33
+ issueName: 'Empty Alt Text',
34
+ description: 'Empty alt text detected',
35
+ severity: 'LOW',
36
+ fix: 'Consider: {{consideration}}',
37
+ documentationLink: 'https://www.w3.org/WAI/tutorials/images/decorative/',
38
+ }),
39
+ addDescriptiveAlt: (0, eslint_devkit_1.formatLLMMessage)({
40
+ icon: eslint_devkit_1.MessageIcons.INFO,
41
+ issueName: 'Add Descriptive Alt',
42
+ description: 'Add descriptive alt text',
43
+ severity: 'LOW',
44
+ fix: 'alt="Descriptive text about image content"',
45
+ documentationLink: 'https://www.w3.org/WAI/tutorials/images/informative/',
46
+ }),
47
+ useEmptyAlt: (0, eslint_devkit_1.formatLLMMessage)({
48
+ icon: eslint_devkit_1.MessageIcons.INFO,
49
+ issueName: 'Use Empty Alt',
50
+ description: 'Use empty alt for decorative images',
51
+ severity: 'LOW',
52
+ fix: 'alt="" (for decorative images only)',
53
+ documentationLink: 'https://www.w3.org/WAI/tutorials/images/decorative/',
54
+ }),
55
+ },
56
+ schema: [
57
+ {
58
+ type: 'object',
59
+ properties: {
60
+ allowAriaLabel: {
61
+ type: 'boolean',
62
+ default: false,
63
+ },
64
+ allowAriaLabelledby: {
65
+ type: 'boolean',
66
+ default: false,
67
+ },
68
+ },
69
+ additionalProperties: false,
70
+ },
71
+ ],
72
+ },
73
+ defaultOptions: [
74
+ {
75
+ allowAriaLabel: false,
76
+ allowAriaLabelledby: false,
77
+ },
78
+ ],
79
+ create(context) {
80
+ const options = context.options[0] || {};
81
+ const { allowAriaLabel = false, allowAriaLabelledby = false } = options || {};
82
+ /**
83
+ * Check if element has alt attribute
84
+ */
85
+ const hasAltAttribute = (node) => {
86
+ return node.attributes.some((attr) => attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier' && attr.name.name === 'alt');
87
+ };
88
+ /**
89
+ * Get alt attribute value
90
+ */
91
+ const getAltValue = (node) => {
92
+ const altAttr = node.attributes.find((attr) => attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier' && attr.name.name === 'alt');
93
+ if (!altAttr || altAttr.type !== 'JSXAttribute')
94
+ return null;
95
+ if (!altAttr.value)
96
+ return '';
97
+ if (altAttr.value.type === 'Literal' && typeof altAttr.value.value === 'string') {
98
+ return altAttr.value.value;
99
+ }
100
+ return null; // Dynamic value
101
+ };
102
+ /**
103
+ * Check for aria-label or aria-labelledby
104
+ */
105
+ const hasAriaLabel = (node) => {
106
+ if (!allowAriaLabel && !allowAriaLabelledby)
107
+ return false;
108
+ return node.attributes.some((attr) => {
109
+ if (attr.type !== 'JSXAttribute' || attr.name.type !== 'JSXIdentifier')
110
+ return false;
111
+ return ((allowAriaLabel && attr.name.name === 'aria-label') ||
112
+ (allowAriaLabelledby && attr.name.name === 'aria-labelledby'));
113
+ });
114
+ };
115
+ /**
116
+ * Extract context about the image
117
+ */
118
+ const getImageContext = (node) => {
119
+ const srcAttr = node.attributes.find((attr) => attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier' && attr.name.name === 'src');
120
+ let src;
121
+ if (srcAttr && srcAttr.type === 'JSXAttribute' && srcAttr.value?.type === 'Literal') {
122
+ src = String(srcAttr.value.value);
123
+ }
124
+ return {
125
+ src,
126
+ usage: 'Content image', // Could be enhanced to detect context
127
+ surroundingText: 'Unknown context',
128
+ };
129
+ };
130
+ /**
131
+ * Suggest alt text based on context
132
+ */
133
+ const suggestAltText = (imageContext) => {
134
+ const suggestions = [];
135
+ if (imageContext.src) {
136
+ // Extract filename-based suggestion
137
+ const filename = imageContext.src.split('/').pop()?.replace(/\.[^.]+$/, '');
138
+ if (filename) {
139
+ suggestions.push(filename.replace(/-|_/g, ' '));
140
+ }
141
+ }
142
+ suggestions.push('Descriptive text about the image');
143
+ suggestions.push('Product image showing [description]');
144
+ return suggestions;
145
+ };
146
+ return {
147
+ JSXOpeningElement(node) {
148
+ // Check if this is an img element
149
+ if (node.name.type !== 'JSXIdentifier' || node.name.name !== 'img') {
150
+ return;
151
+ }
152
+ // Check if has alt or aria-label
153
+ if (hasAltAttribute(node)) {
154
+ const altValue = getAltValue(node);
155
+ // If alt value is null, it means alt={undefined} or alt={variable} - still missing
156
+ if (altValue === null) {
157
+ // alt attribute exists but value is dynamic/undefined - treat as missing
158
+ }
159
+ else if (altValue === '') {
160
+ // Empty alt is valid for decorative images, but we can suggest verification
161
+ return;
162
+ }
163
+ else {
164
+ // Has valid alt text
165
+ return;
166
+ }
167
+ }
168
+ if (hasAriaLabel(node)) {
169
+ return; // Has aria-label as alternative
170
+ }
171
+ // Missing alt text
172
+ const imageContext = getImageContext(node);
173
+ const suggestions = suggestAltText(imageContext);
174
+ context.report({
175
+ node,
176
+ messageId: 'missingAlt',
177
+ data: {
178
+ affectedUsers: '8% of users',
179
+ wcagLevel: 'A',
180
+ suggestion: suggestions[0] || 'Descriptive text',
181
+ },
182
+ suggest: [
183
+ {
184
+ messageId: 'addDescriptiveAlt',
185
+ fix: (fixer) => {
186
+ // Add alt with placeholder
187
+ const lastAttr = node.attributes[node.attributes.length - 1];
188
+ const insertAfter = lastAttr || node.name;
189
+ return fixer.insertTextAfter(insertAfter, ' alt="TODO: Add descriptive text"');
190
+ },
191
+ },
192
+ {
193
+ messageId: 'useEmptyAlt',
194
+ fix: (fixer) => {
195
+ const lastAttr = node.attributes[node.attributes.length - 1];
196
+ const insertAfter = lastAttr || node.name;
197
+ return fixer.insertTextAfter(insertAfter, ' alt=""');
198
+ },
199
+ },
200
+ ],
201
+ });
202
+ },
203
+ };
204
+ },
205
+ });
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Copyright (c) 2025 Ofri Peretz
3
+ * Licensed under the MIT License. Use of this source code is governed by the
4
+ * MIT license that can be found in the LICENSE file.
5
+ */
6
+ /**
7
+ * ESLint Rule: anchor-ambiguous-text
8
+ * Enforce that anchor text is not ambiguous
9
+ *
10
+ * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/anchor-ambiguous-text.md
11
+ */
12
+ import type { TSESLint } from '@interlace/eslint-devkit';
13
+ type Options = {
14
+ words?: string[];
15
+ };
16
+ type RuleOptions = [Options?];
17
+ export declare const anchorAmbiguousText: TSESLint.RuleModule<"ambiguousText", RuleOptions, unknown, TSESLint.RuleListener> & {
18
+ name: string;
19
+ };
20
+ export {};