eslint-plugin-a11y-enforce 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Venkatesh Mukundan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # eslint-plugin-a11y-enforce
2
+
3
+ ESLint plugin that catches accessibility composition errors that element-level tools miss.
4
+
5
+ `eslint-plugin-jsx-a11y` checks individual elements: "does this img have alt text?" `a11y-enforce` checks how elements relate to each other: "does this trigger's `aria-haspopup` match its content's role?" "Is this accordion trigger inside a heading?"
6
+
7
+ Use both. They complement each other.
8
+
9
+ ## What this catches that jsx-a11y does not
10
+
11
+ | Pattern | jsx-a11y | a11y-enforce |
12
+ |---------|----------|--------------|
13
+ | `role="dialog"` without `aria-modal="true"` | No | Yes |
14
+ | `aria-haspopup` with invalid value | No | Yes |
15
+ | Interactive elements inside `role="tooltip"` | No | Yes |
16
+ | Accordion trigger not inside a heading | No | Yes |
17
+ | `role="menuitem"` on a `<button>` | No | Yes |
18
+ | `role="dialog"` without accessible name | No | Yes |
19
+ | `tabIndex={0}` without keyboard handler | No | Yes |
20
+ | Form input without accessible label | Unreliable | Yes |
21
+ | Radio buttons without grouping container | No | Yes |
22
+ | `tabIndex` greater than 0 | Warning | Error |
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ npm install --save-dev eslint-plugin-a11y-enforce
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### ESLint 9+ (flat config)
33
+
34
+ ```js
35
+ // eslint.config.js
36
+ import a11yEnforce from 'eslint-plugin-a11y-enforce';
37
+
38
+ export default [
39
+ a11yEnforce.configs.recommended,
40
+ ];
41
+ ```
42
+
43
+ ### ESLint 8 (legacy config)
44
+
45
+ ```json
46
+ {
47
+ "extends": ["plugin:a11y-enforce/legacy/recommended"]
48
+ }
49
+ ```
50
+
51
+ ## Rules
52
+
53
+ All rules are set to `error` in the recommended config.
54
+
55
+ ### Component pattern rules
56
+
57
+ These validate ARIA relationships in compound components like Dialog, Menu, Select, Accordion, and Tooltip. No other public ESLint plugin checks these patterns.
58
+
59
+ #### `dialog-requires-modal`
60
+
61
+ Elements with `role="dialog"` or `role="alertdialog"` must have `aria-modal="true"`. Without it, screen readers allow users to navigate outside the dialog using virtual cursor.
62
+
63
+ ```jsx
64
+ // Bad: screen reader can read content behind the dialog
65
+ <div role="dialog">Content</div>
66
+
67
+ // Good: navigation restricted to dialog
68
+ <div role="dialog" aria-modal="true" aria-labelledby="title">Content</div>
69
+ ```
70
+
71
+ #### `haspopup-role-match`
72
+
73
+ `aria-haspopup` must be one of: `menu`, `listbox`, `tree`, `grid`, `dialog`, `true`, `false`. Invalid values are silently treated as `false` by browsers, meaning the popup is never announced.
74
+
75
+ ```jsx
76
+ // Bad: "dropdown" is not a valid value
77
+ <button aria-haspopup="dropdown">Open</button>
78
+
79
+ // Good: matches the popup's role
80
+ <button aria-haspopup="menu">Actions</button>
81
+ ```
82
+
83
+ #### `tooltip-no-interactive`
84
+
85
+ `role="tooltip"` must not contain focusable elements (buttons, links, inputs, elements with `tabIndex >= 0`). Tooltips disappear on blur, making interactive content inside them unreachable by keyboard users.
86
+
87
+ ```jsx
88
+ // Bad: keyboard user cannot reach the link
89
+ <div role="tooltip"><a href="/help">Learn more</a></div>
90
+
91
+ // Good: text-only content
92
+ <div role="tooltip">Save your changes (Ctrl+S)</div>
93
+ ```
94
+
95
+ #### `accordion-trigger-heading`
96
+
97
+ A `<button>` with `aria-expanded` (accordion trigger) should be inside a heading element (`h1`-`h6` or `role="heading"`). Screen reader users navigate pages by headings. Without a heading wrapper, accordion sections are invisible to heading navigation.
98
+
99
+ ```jsx
100
+ // Bad: invisible to heading navigation
101
+ <div><button aria-expanded="true">Section</button></div>
102
+
103
+ // Good: discoverable via heading navigation
104
+ <h3><button aria-expanded="true">Section</button></h3>
105
+ ```
106
+
107
+ #### `menuitem-not-button`
108
+
109
+ `role="menuitem"` (including `menuitemcheckbox` and `menuitemradio`) should not be on `<button>` elements. Buttons have an implicit "button" role, causing some screen readers to double-announce: "button, menuitem, Edit."
110
+
111
+ ```jsx
112
+ // Bad: double announcement in some screen readers
113
+ <button role="menuitem">Edit</button>
114
+
115
+ // Good: single role, programmatically focusable
116
+ <div role="menuitem" tabIndex={-1}>Edit</div>
117
+ ```
118
+
119
+ #### `dialog-requires-title`
120
+
121
+ `role="dialog"` or `role="alertdialog"` must have `aria-labelledby` or `aria-label`. Without a name, screen readers announce "dialog" with no context about its purpose.
122
+
123
+ ```jsx
124
+ // Bad: "dialog" with no context
125
+ <div role="dialog" aria-modal="true">Are you sure?</div>
126
+
127
+ // Good: "Confirm deletion, dialog"
128
+ <div role="dialog" aria-modal="true" aria-labelledby="title">
129
+ <h2 id="title">Confirm deletion</h2>
130
+ </div>
131
+ ```
132
+
133
+ ### General interaction rules
134
+
135
+ These catch common accessibility mistakes in everyday React code. They fire on patterns every developer writes.
136
+
137
+ #### `focusable-has-interaction`
138
+
139
+ Elements with `tabIndex={0}` must have a keyboard event handler (`onKeyDown`, `onKeyUp`, or `onKeyPress`). Making an element focusable implies it's interactive. Without a keyboard handler, it's a dead end in the Tab sequence.
140
+
141
+ ```jsx
142
+ // Bad: reachable by Tab but inert
143
+ <div tabIndex={0}>Card</div>
144
+
145
+ // Good: keyboard interaction supported
146
+ <div tabIndex={0} onKeyDown={handleKeyDown}>Card</div>
147
+ ```
148
+
149
+ #### `input-requires-label`
150
+
151
+ `<input>`, `<select>`, and `<textarea>` must have an accessible label via `aria-label`, `aria-labelledby`, or `id` (implying a `<label htmlFor>` association). Placeholder text is not a label.
152
+
153
+ ```jsx
154
+ // Bad: screen reader says "edit text" with no context
155
+ <input type="text" placeholder="Enter name" />
156
+
157
+ // Good: screen reader says "Full name, edit text"
158
+ <input type="text" aria-label="Full name" />
159
+ ```
160
+
161
+ #### `radio-group-requires-grouping`
162
+
163
+ `<input type="radio">` must be inside a `<fieldset>` or an element with `role="radiogroup"`. Without grouping, screen readers announce each radio independently with no indication they form a set.
164
+
165
+ ```jsx
166
+ // Bad: "radio button, Red" then "radio button, Blue" (no relationship)
167
+ <div>
168
+ <input type="radio" name="color" value="red" /> Red
169
+ <input type="radio" name="color" value="blue" /> Blue
170
+ </div>
171
+
172
+ // Good: "Color, group. Radio button, Red. Radio button, Blue."
173
+ <fieldset>
174
+ <legend>Color</legend>
175
+ <input type="radio" name="color" value="red" /> Red
176
+ <input type="radio" name="color" value="blue" /> Blue
177
+ </fieldset>
178
+ ```
179
+
180
+ #### `no-positive-tabindex`
181
+
182
+ `tabIndex` must not be greater than 0. Positive values override the natural DOM tab order, creating unpredictable keyboard navigation. `jsx-a11y` has this as a warning. This plugin makes it an error because there is no legitimate use case.
183
+
184
+ ```jsx
185
+ // Bad: receives focus before all tabIndex={0} elements
186
+ <div tabIndex={5}>Out of order</div>
187
+
188
+ // Good: focusable in DOM order
189
+ <div tabIndex={0} onKeyDown={handleKey}>In order</div>
190
+ ```
191
+
192
+ ## Why these rules exist
193
+
194
+ Accessibility lawsuits in the US increased 37% in 2025, with over 5,000 federal cases filed. The European Accessibility Act started enforcement in June 2025. India's Supreme Court declared digital access a fundamental right, and SEBI mandated accessibility compliance for the financial sector with deadlines through 2026.
195
+
196
+ The most common issues cited in audits and lawsuits are WCAG 4.1.2 (Name, Role, Value) and 1.3.1 (Info and Relationships). These are exactly the composition errors this plugin catches: mismatched ARIA relationships, missing modal semantics, unlabeled dialogs, and broken focus patterns.
197
+
198
+ Your linter should catch these before they ship. `jsx-a11y` catches the element-level issues. `a11y-enforce` catches the composition-level issues.
199
+
200
+ ## Design decisions
201
+
202
+ - **Zero runtime dependencies.** The plugin uses only ESLint's built-in AST APIs.
203
+ - **ESLint 8 and 9 support.** Rules are version-agnostic. Only the config export format differs.
204
+ - **Educational error messages.** Every violation includes what is wrong, why it matters for users, and how to fix it.
205
+ - **Single recommended preset.** All 10 rules as errors. No strict/recommended split until real-world usage data justifies one.
206
+ - **Complements jsx-a11y.** No rule overlap. Install both.
207
+
208
+ ## Stats
209
+
210
+ - 10 rules (6 component pattern, 4 general interaction)
211
+ - 149 tests
212
+ - Zero runtime dependencies
213
+ - ESM + CJS dual output
214
+ - TypeScript source with full type safety
215
+
216
+ ## License
217
+
218
+ MIT