eslint-plugin-aria-state-validator 1.0.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.ko.md +382 -0
- package/README.md +360 -0
- package/index.js +37 -0
- package/lib/rules/state-dependent-aria-validator.js +385 -0
- package/lib/rules/static-aria-validator.js +358 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# eslint-plugin-aria-state-validator
|
|
2
|
+
|
|
3
|
+
Comprehensive ESLint plugin for ARIA accessibility validation:
|
|
4
|
+
- **Dynamic State Validation**: Validates that ARIA state attributes are properly bound to component state
|
|
5
|
+
- **Static ARIA Validation**: Validates static ARIA attributes for correctness (spelling, values, conflicts)
|
|
6
|
+
|
|
7
|
+
Catches both runtime and static accessibility errors at build time.
|
|
8
|
+
|
|
9
|
+
## Problem Statement: The "Runtime Gap" in Static Analysis
|
|
10
|
+
|
|
11
|
+
Existing accessibility tools like `eslint-plugin-jsx-a11y` validate the syntactic correctness of ARIA attributes (e.g., ensuring `aria-expanded` uses `'true'` or `'false'`), but they **cannot verify** whether these attributes are correctly connected to the component's JavaScript state and dynamically updated at runtime.
|
|
12
|
+
|
|
13
|
+
### Issues This Causes:
|
|
14
|
+
|
|
15
|
+
- Toggle buttons with `aria-expanded="false"` hardcoded, never changing when opened
|
|
16
|
+
- Boolean values directly bound to ARIA attributes: `aria-expanded={isOpen}` (incorrect - should be string)
|
|
17
|
+
- Static ARIA states on interactive elements that should be dynamic
|
|
18
|
+
- Screen readers receiving inaccurate information about component states
|
|
19
|
+
|
|
20
|
+
## Core Concept: State-Dependent ARIA Validation
|
|
21
|
+
|
|
22
|
+
This plugin uses AST (Abstract Syntax Tree) analysis to **statically verify** that ARIA state attributes are logically connected to component state (State/Props) and will update dynamically at runtime.
|
|
23
|
+
|
|
24
|
+
### Key Differentiator
|
|
25
|
+
|
|
26
|
+
Goes beyond syntax checking to infer component **dynamic behavior**, proactively catching runtime accessibility errors during build/CI phase.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install --save-dev eslint-plugin-aria-state-validator
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
### ESLint Flat Config (ESLint 9+)
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
// eslint.config.js
|
|
40
|
+
import ariaStateValidator from 'eslint-plugin-aria-state-validator';
|
|
41
|
+
|
|
42
|
+
export default [
|
|
43
|
+
{
|
|
44
|
+
plugins: {
|
|
45
|
+
'aria-state-validator': ariaStateValidator
|
|
46
|
+
},
|
|
47
|
+
rules: {
|
|
48
|
+
'aria-state-validator/state-dependent-aria-validator': 'warn'
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
];
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Legacy ESLintRC Config
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"plugins": ["aria-state-validator"],
|
|
59
|
+
"rules": {
|
|
60
|
+
"aria-state-validator/state-dependent-aria-validator": "warn"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Two Rules for Comprehensive ARIA Validation
|
|
66
|
+
|
|
67
|
+
This plugin provides two complementary rules:
|
|
68
|
+
|
|
69
|
+
### Rule 1: `state-dependent-aria-validator` (Dynamic State Validation)
|
|
70
|
+
|
|
71
|
+
Validates that ARIA **state** attributes are properly bound to component state and update dynamically.
|
|
72
|
+
|
|
73
|
+
### Rule 2: `static-aria-validator` (Static ARIA Validation)
|
|
74
|
+
|
|
75
|
+
Validates the **correctness** of ARIA attributes when they are used:
|
|
76
|
+
- Valid ARIA property names (catches typos)
|
|
77
|
+
- Correct values for boolean and enum ARIA attributes
|
|
78
|
+
- No conflicting ARIA attributes
|
|
79
|
+
- No redundant roles on native HTML elements
|
|
80
|
+
|
|
81
|
+
**Note:** This plugin validates **how** ARIA attributes are used, not **whether** they should be used. For enforcing the presence of accessibility features, use [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y).
|
|
82
|
+
|
|
83
|
+
## Features & Validation Patterns
|
|
84
|
+
|
|
85
|
+
### 1. Boolean Value Direct Binding Detection
|
|
86
|
+
|
|
87
|
+
**Detects:** Direct binding of Boolean values to ARIA attributes
|
|
88
|
+
|
|
89
|
+
```jsx
|
|
90
|
+
// ❌ Error: Boolean value directly bound
|
|
91
|
+
<button aria-expanded={isOpen}>Toggle</button>
|
|
92
|
+
|
|
93
|
+
// ✅ Correct: Explicit string conversion
|
|
94
|
+
<button aria-expanded={isOpen ? 'true' : 'false'}>Toggle</button>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Auto-fix Available:** `eslint --fix` will automatically convert to proper string format
|
|
98
|
+
|
|
99
|
+
### 2. Static Value + Interactive Handler Detection
|
|
100
|
+
|
|
101
|
+
**Detects:** Static ARIA state values on elements with event handlers
|
|
102
|
+
|
|
103
|
+
```jsx
|
|
104
|
+
// ❌ Error: Has onClick but aria-expanded is static
|
|
105
|
+
<button onClick={handleClick} aria-expanded="false">
|
|
106
|
+
Toggle
|
|
107
|
+
</button>
|
|
108
|
+
|
|
109
|
+
// ✅ Correct: Dynamic state binding
|
|
110
|
+
<button onClick={handleClick} aria-expanded={isOpen ? 'true' : 'false'}>
|
|
111
|
+
Toggle
|
|
112
|
+
</button>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Smart Auto-fix:** When the handler contains state variable references (e.g., `() => setExpanded(!expanded)`), the plugin automatically extracts the variable name and binds it to the ARIA attribute:
|
|
116
|
+
|
|
117
|
+
```jsx
|
|
118
|
+
// ❌ Before auto-fix
|
|
119
|
+
<div onClick={() => setExpanded(!expanded)} aria-expanded="false">
|
|
120
|
+
Click to expand
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
// ✅ After auto-fix (automatically detects 'expanded' variable)
|
|
124
|
+
<div onClick={() => setExpanded(!expanded)} aria-expanded={expanded ? 'true' : 'false'}>
|
|
125
|
+
Click to expand
|
|
126
|
+
</div>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 3. Role-Based State Association Validation
|
|
130
|
+
|
|
131
|
+
**Detects:** Interactive roles with static ARIA states
|
|
132
|
+
|
|
133
|
+
```jsx
|
|
134
|
+
// ❌ Error: role="button" with static aria-pressed
|
|
135
|
+
<div role="button" aria-pressed="false">Button</div>
|
|
136
|
+
|
|
137
|
+
// ✅ Correct: Dynamic state binding
|
|
138
|
+
<div role="button" aria-pressed={isPressed ? 'true' : 'false'}>Button</div>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Static ARIA Validation Patterns
|
|
144
|
+
|
|
145
|
+
### 4. Invalid ARIA Property Detection
|
|
146
|
+
|
|
147
|
+
**Detects:** Typos in ARIA attribute names
|
|
148
|
+
|
|
149
|
+
```jsx
|
|
150
|
+
// ❌ Error: Typo in aria-label
|
|
151
|
+
<button aria-labell="Close">X</button>
|
|
152
|
+
|
|
153
|
+
// ✅ Correct: Proper spelling
|
|
154
|
+
<button aria-label="Close">X</button>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 5. Empty Required Values
|
|
158
|
+
|
|
159
|
+
**Detects:** ARIA attributes that require non-empty values
|
|
160
|
+
|
|
161
|
+
```jsx
|
|
162
|
+
// ❌ Error: Empty aria-label
|
|
163
|
+
<div aria-label="">Content</div>
|
|
164
|
+
|
|
165
|
+
// ✅ Correct: Non-empty value
|
|
166
|
+
<div aria-label="Description">Content</div>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### 6. Invalid Boolean Values
|
|
170
|
+
|
|
171
|
+
**Detects:** Incorrect boolean-like values
|
|
172
|
+
|
|
173
|
+
```jsx
|
|
174
|
+
// ❌ Error: Invalid boolean value
|
|
175
|
+
<div aria-hidden="yes">Content</div>
|
|
176
|
+
<div aria-disabled="1">Content</div>
|
|
177
|
+
|
|
178
|
+
// ✅ Correct: Use "true" or "false"
|
|
179
|
+
<div aria-hidden="true">Content</div>
|
|
180
|
+
<div aria-disabled="false">Content</div>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 7. Invalid Enum Values
|
|
184
|
+
|
|
185
|
+
**Detects:** Invalid values for enum-type ARIA attributes
|
|
186
|
+
|
|
187
|
+
```jsx
|
|
188
|
+
// ❌ Error: Invalid aria-live value
|
|
189
|
+
<div aria-live="loud">Live region</div>
|
|
190
|
+
|
|
191
|
+
// ✅ Correct: Use valid enum value
|
|
192
|
+
<div aria-live="polite">Live region</div>
|
|
193
|
+
|
|
194
|
+
// Valid values: "off", "polite", "assertive"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 8. Conflicting ARIA Attributes
|
|
198
|
+
|
|
199
|
+
**Detects:** aria-label and aria-labelledby used together
|
|
200
|
+
|
|
201
|
+
```jsx
|
|
202
|
+
// ❌ Error: Both attributes present (aria-labelledby takes precedence)
|
|
203
|
+
<div aria-label="Label" aria-labelledby="other-id">Content</div>
|
|
204
|
+
|
|
205
|
+
// ✅ Correct: Use only one
|
|
206
|
+
<div aria-labelledby="other-id">Content</div>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 9. Redundant Roles
|
|
210
|
+
|
|
211
|
+
**Detects:** Explicit roles that match native HTML semantics
|
|
212
|
+
|
|
213
|
+
```jsx
|
|
214
|
+
// ❌ Error: Redundant role
|
|
215
|
+
<button role="button">Click me</button>
|
|
216
|
+
<nav role="navigation">Nav</nav>
|
|
217
|
+
<a href="#" role="link">Link</a>
|
|
218
|
+
|
|
219
|
+
// ✅ Correct: Use native semantics
|
|
220
|
+
<button>Click me</button>
|
|
221
|
+
<nav>Nav</nav>
|
|
222
|
+
<a href="#">Link</a>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Supported ARIA Attributes (Dynamic Validation)
|
|
228
|
+
|
|
229
|
+
The plugin validates these dynamic ARIA state attributes:
|
|
230
|
+
|
|
231
|
+
- `aria-expanded` - Toggle buttons, expandable elements
|
|
232
|
+
- `aria-selected` - Tabs, selectable options
|
|
233
|
+
- `aria-checked` - Checkboxes, radio buttons
|
|
234
|
+
- `aria-pressed` - Toggle buttons
|
|
235
|
+
- `aria-hidden` - Dynamically shown/hidden elements
|
|
236
|
+
- `aria-disabled` - Dynamically enabled/disabled elements
|
|
237
|
+
- `aria-modal` - Dialogs, modals
|
|
238
|
+
- `aria-current` - Current active item indicators
|
|
239
|
+
|
|
240
|
+
## Supported Interactive Roles
|
|
241
|
+
|
|
242
|
+
Elements with these roles are expected to have dynamic ARIA states:
|
|
243
|
+
|
|
244
|
+
- `button`, `tab`, `checkbox`, `radio`, `switch`
|
|
245
|
+
- `menuitem`, `menuitemcheckbox`, `menuitemradio`
|
|
246
|
+
- `option`, `treeitem`
|
|
247
|
+
- `dialog`, `alertdialog`
|
|
248
|
+
|
|
249
|
+
## Examples
|
|
250
|
+
|
|
251
|
+
### Valid Patterns ✅
|
|
252
|
+
|
|
253
|
+
```jsx
|
|
254
|
+
// Ternary operator for string conversion
|
|
255
|
+
<button aria-expanded={isOpen ? 'true' : 'false'}>Toggle</button>
|
|
256
|
+
|
|
257
|
+
// String() function
|
|
258
|
+
<button aria-pressed={String(isPressed)}>Toggle</button>
|
|
259
|
+
|
|
260
|
+
// .toString() method
|
|
261
|
+
<div aria-hidden={isHidden.toString()}>Content</div>
|
|
262
|
+
|
|
263
|
+
// Inverted boolean with ternary
|
|
264
|
+
<button aria-expanded={!isOpen ? 'true' : 'false'}>Toggle</button>
|
|
265
|
+
|
|
266
|
+
// Static ARIA on non-interactive element (no handler)
|
|
267
|
+
<div aria-label="static label">Content</div>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Invalid Patterns ❌
|
|
271
|
+
|
|
272
|
+
```jsx
|
|
273
|
+
// Direct boolean binding
|
|
274
|
+
<button aria-expanded={isOpen}>Toggle</button>
|
|
275
|
+
// Fix: aria-expanded={isOpen ? 'true' : 'false'}
|
|
276
|
+
|
|
277
|
+
// Logical expression without string conversion
|
|
278
|
+
<button aria-expanded={foo && bar}>Toggle</button>
|
|
279
|
+
// Fix: aria-expanded={foo && bar ? 'true' : 'false'}
|
|
280
|
+
|
|
281
|
+
// Static value with event handler
|
|
282
|
+
<button onClick={handleClick} aria-expanded="false">Toggle</button>
|
|
283
|
+
// Fix: Use dynamic state binding
|
|
284
|
+
|
|
285
|
+
// Interactive role with static ARIA
|
|
286
|
+
<div role="tab" aria-selected="true">Tab</div>
|
|
287
|
+
// Fix: Use dynamic state binding
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Technical Implementation
|
|
291
|
+
|
|
292
|
+
### Technology Stack
|
|
293
|
+
|
|
294
|
+
- **Base:** ESLint plugin (Node.js environment)
|
|
295
|
+
- **Parser:** `@babel/eslint-parser` or TypeScript parser for JSX/TSX AST parsing
|
|
296
|
+
- **Analysis:** Visitor pattern on JSX elements
|
|
297
|
+
|
|
298
|
+
### Analysis Logic
|
|
299
|
+
|
|
300
|
+
1. **JSXElement Traversal:** Identifies `role` and `aria-*` attributes
|
|
301
|
+
2. **Scope Tracking:** Uses `context.getScope()` to trace variables bound to ARIA attributes
|
|
302
|
+
3. **State Inference:** Determines if values derive from `useState` hooks or props
|
|
303
|
+
4. **Pattern Validation:** Checks for proper string conversion patterns
|
|
304
|
+
|
|
305
|
+
## Expected Benefits
|
|
306
|
+
|
|
307
|
+
### 1. Accessibility Quality Improvement
|
|
308
|
+
|
|
309
|
+
Catch dynamic ARIA errors at build/CI phase instead of runtime
|
|
310
|
+
|
|
311
|
+
### 2. Developer Productivity
|
|
312
|
+
|
|
313
|
+
Auto-fix repetitive ARIA state binding errors with `eslint --fix`
|
|
314
|
+
|
|
315
|
+
### 3. Standardization
|
|
316
|
+
|
|
317
|
+
Enforce consistent dynamic ARIA usage patterns across projects
|
|
318
|
+
|
|
319
|
+
## Development
|
|
320
|
+
|
|
321
|
+
### Running Tests
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
npm test
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Building
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
npm run build # If applicable
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Contributing
|
|
334
|
+
|
|
335
|
+
Contributions welcome! Please feel free to submit issues or pull requests.
|
|
336
|
+
|
|
337
|
+
## License
|
|
338
|
+
|
|
339
|
+
ISC
|
|
340
|
+
|
|
341
|
+
## Keywords
|
|
342
|
+
|
|
343
|
+
- eslint
|
|
344
|
+
- eslint-plugin
|
|
345
|
+
- accessibility
|
|
346
|
+
- a11y
|
|
347
|
+
- aria
|
|
348
|
+
- jsx
|
|
349
|
+
- react
|
|
350
|
+
- state-validation
|
|
351
|
+
- dynamic-attributes
|
|
352
|
+
|
|
353
|
+
## Related Projects
|
|
354
|
+
|
|
355
|
+
- [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) - Comprehensive JSX accessibility checking
|
|
356
|
+
- [axe-core](https://github.com/dequelabs/axe-core) - Runtime accessibility testing engine
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
**Note:** This plugin focuses on static analysis of ARIA state binding patterns. For comprehensive accessibility testing, combine with runtime testing tools like axe-core.
|
package/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const stateDependentAriaValidator = require('./lib/rules/state-dependent-aria-validator');
|
|
2
|
+
const staticAriaValidator = require('./lib/rules/static-aria-validator');
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
rules: {
|
|
6
|
+
'state-dependent-aria-validator': stateDependentAriaValidator,
|
|
7
|
+
'static-aria-validator': staticAriaValidator
|
|
8
|
+
},
|
|
9
|
+
configs: {
|
|
10
|
+
recommended: {
|
|
11
|
+
plugins: ['aria-state-validator'],
|
|
12
|
+
rules: {
|
|
13
|
+
'aria-state-validator/state-dependent-aria-validator': 'warn',
|
|
14
|
+
'aria-state-validator/static-aria-validator': 'warn'
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
strict: {
|
|
18
|
+
plugins: ['aria-state-validator'],
|
|
19
|
+
rules: {
|
|
20
|
+
'aria-state-validator/state-dependent-aria-validator': 'error',
|
|
21
|
+
'aria-state-validator/static-aria-validator': 'error'
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
'dynamic-only': {
|
|
25
|
+
plugins: ['aria-state-validator'],
|
|
26
|
+
rules: {
|
|
27
|
+
'aria-state-validator/state-dependent-aria-validator': 'warn'
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
'static-only': {
|
|
31
|
+
plugins: ['aria-state-validator'],
|
|
32
|
+
rules: {
|
|
33
|
+
'aria-state-validator/static-aria-validator': 'warn'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|