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 +21 -0
- package/README.md +218 -0
- package/dist/index.cjs +513 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +485 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
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
|