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.
- package/AGENTS.md +110 -0
- package/CHANGELOG.md +37 -0
- package/LICENSE +22 -0
- package/README.md +213 -0
- package/package.json +79 -0
- package/src/index.d.ts +40 -0
- package/src/index.js +279 -0
- package/src/rules/alt-text.d.ts +23 -0
- package/src/rules/alt-text.js +205 -0
- package/src/rules/anchor-ambiguous-text.d.ts +20 -0
- package/src/rules/anchor-ambiguous-text.js +169 -0
- package/src/rules/anchor-has-content.d.ts +20 -0
- package/src/rules/anchor-has-content.js +85 -0
- package/src/rules/anchor-is-valid.d.ts +23 -0
- package/src/rules/anchor-is-valid.js +100 -0
- package/src/rules/aria-activedescendant-has-tabindex.d.ts +15 -0
- package/src/rules/aria-activedescendant-has-tabindex.js +63 -0
- package/src/rules/aria-props.d.ts +15 -0
- package/src/rules/aria-props.js +51 -0
- package/src/rules/aria-role.d.ts +21 -0
- package/src/rules/aria-role.js +72 -0
- package/src/rules/aria-unsupported-elements.d.ts +15 -0
- package/src/rules/aria-unsupported-elements.js +54 -0
- package/src/rules/autocomplete-valid.d.ts +20 -0
- package/src/rules/autocomplete-valid.js +79 -0
- package/src/rules/click-events-have-key-events.d.ts +15 -0
- package/src/rules/click-events-have-key-events.js +60 -0
- package/src/rules/control-has-associated-label.d.ts +24 -0
- package/src/rules/control-has-associated-label.js +178 -0
- package/src/rules/heading-has-content.d.ts +20 -0
- package/src/rules/heading-has-content.js +72 -0
- package/src/rules/html-has-lang.d.ts +15 -0
- package/src/rules/html-has-lang.js +50 -0
- package/src/rules/iframe-has-title.d.ts +15 -0
- package/src/rules/iframe-has-title.js +51 -0
- package/src/rules/img-redundant-alt.d.ts +21 -0
- package/src/rules/img-redundant-alt.js +85 -0
- package/src/rules/interactive-supports-focus.d.ts +20 -0
- package/src/rules/interactive-supports-focus.js +81 -0
- package/src/rules/label-has-associated-control.d.ts +24 -0
- package/src/rules/label-has-associated-control.js +93 -0
- package/src/rules/lang.d.ts +15 -0
- package/src/rules/lang.js +53 -0
- package/src/rules/media-has-caption.d.ts +22 -0
- package/src/rules/media-has-caption.js +84 -0
- package/src/rules/mouse-events-have-key-events.d.ts +17 -0
- package/src/rules/mouse-events-have-key-events.js +62 -0
- package/src/rules/no-access-key.d.ts +15 -0
- package/src/rules/no-access-key.js +43 -0
- package/src/rules/no-aria-hidden-on-focusable.d.ts +15 -0
- package/src/rules/no-aria-hidden-on-focusable.js +67 -0
- package/src/rules/no-autofocus.d.ts +20 -0
- package/src/rules/no-autofocus.js +59 -0
- package/src/rules/no-distracting-elements.d.ts +20 -0
- package/src/rules/no-distracting-elements.js +64 -0
- package/src/rules/no-interactive-element-to-noninteractive-role.d.ts +18 -0
- package/src/rules/no-interactive-element-to-noninteractive-role.js +86 -0
- package/src/rules/no-keyboard-inaccessible-elements.d.ts +24 -0
- package/src/rules/no-keyboard-inaccessible-elements.js +136 -0
- package/src/rules/no-missing-aria-labels.d.ts +24 -0
- package/src/rules/no-missing-aria-labels.js +131 -0
- package/src/rules/no-noninteractive-element-interactions.d.ts +20 -0
- package/src/rules/no-noninteractive-element-interactions.js +78 -0
- package/src/rules/no-noninteractive-element-to-interactive-role.d.ts +18 -0
- package/src/rules/no-noninteractive-element-to-interactive-role.js +95 -0
- package/src/rules/no-noninteractive-tabindex.d.ts +22 -0
- package/src/rules/no-noninteractive-tabindex.js +172 -0
- package/src/rules/no-redundant-roles.d.ts +24 -0
- package/src/rules/no-redundant-roles.js +115 -0
- package/src/rules/no-static-element-interactions.d.ts +20 -0
- package/src/rules/no-static-element-interactions.js +72 -0
- package/src/rules/prefer-tag-over-role.d.ts +15 -0
- package/src/rules/prefer-tag-over-role.js +101 -0
- package/src/rules/role-has-required-aria-props.d.ts +15 -0
- package/src/rules/role-has-required-aria-props.js +65 -0
- package/src/rules/role-supports-aria-props.d.ts +15 -0
- package/src/rules/role-supports-aria-props.js +115 -0
- package/src/rules/scope.d.ts +15 -0
- package/src/rules/scope.js +49 -0
- package/src/rules/tabindex-no-positive.d.ts +15 -0
- package/src/rules/tabindex-no-positive.js +55 -0
- package/src/types/index.d.ts +208 -0
- 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 {};
|