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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 comento
|
|
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.ko.md
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# eslint-plugin-aria-state-validator
|
|
2
|
+
|
|
3
|
+
ARIA 접근성을 위한 포괄적인 ESLint 플러그인:
|
|
4
|
+
|
|
5
|
+
- **동적 상태 검증**: ARIA 상태 속성이 컴포넌트 상태와 올바르게 연결되었는지 검증
|
|
6
|
+
- **정적 ARIA 검증**: 정적 ARIA 속성의 정확성 검증 (철자, 값, 충돌)
|
|
7
|
+
|
|
8
|
+
빌드 타임에 런타임 및 정적 접근성 오류를 미리 감지합니다.
|
|
9
|
+
|
|
10
|
+
## 문제 제기: 정적 분석의 '런타임 공백'
|
|
11
|
+
|
|
12
|
+
`eslint-plugin-jsx-a11y` 같은 기존 접근성 도구들은 ARIA 속성의 문법적 정확성(예: `aria-expanded`가 `'true'` 또는 `'false'`를 사용하는지)은 검증하지만, 이 속성들이 컴포넌트의 JavaScript 상태와 올바르게 연결되어 런타임에 동적으로 업데이트되는지는 **검증할 수 없습니다**.
|
|
13
|
+
|
|
14
|
+
### 발생하는 문제:
|
|
15
|
+
|
|
16
|
+
- 토글 버튼의 `aria-expanded="false"`가 하드코딩되어 열려도 변경되지 않음
|
|
17
|
+
- ARIA 속성에 Boolean 값을 직접 바인딩: `aria-expanded={isOpen}` (오류 - 문자열이어야 함)
|
|
18
|
+
- 인터랙티브 요소의 정적 ARIA 상태
|
|
19
|
+
- 스크린 리더 사용자에게 부정확한 정보 전달
|
|
20
|
+
|
|
21
|
+
## 핵심 컨셉: 상태 의존 ARIA 검증
|
|
22
|
+
|
|
23
|
+
이 플러그인은 AST(추상 구문 트리) 분석을 통해 ARIA 상태 속성이 컴포넌트 상태(State/Props)와 논리적으로 연결되어 런타임에 업데이트될 것인지 **정적으로 검증**합니다.
|
|
24
|
+
|
|
25
|
+
### 차별점
|
|
26
|
+
|
|
27
|
+
단순 문법 검사를 넘어 컴포넌트의 **동적 동작**을 추론하여, 런타임에서만 발견되던 접근성 오류를 빌드/CI 단계에서 선제적으로 포착합니다.
|
|
28
|
+
|
|
29
|
+
## 설치
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install --save-dev eslint-plugin-aria-state-validator
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 사용법
|
|
36
|
+
|
|
37
|
+
### ESLint Flat Config (ESLint 9+)
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
// eslint.config.js
|
|
41
|
+
import ariaStateValidator from "eslint-plugin-aria-state-validator";
|
|
42
|
+
|
|
43
|
+
export default [
|
|
44
|
+
{
|
|
45
|
+
plugins: {
|
|
46
|
+
"aria-state-validator": ariaStateValidator,
|
|
47
|
+
},
|
|
48
|
+
rules: {
|
|
49
|
+
"aria-state-validator/state-dependent-aria-validator": "warn",
|
|
50
|
+
"aria-state-validator/static-aria-validator": "warn",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Legacy ESLintRC Config
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"plugins": ["aria-state-validator"],
|
|
61
|
+
"rules": {
|
|
62
|
+
"aria-state-validator/state-dependent-aria-validator": "warn",
|
|
63
|
+
"aria-state-validator/static-aria-validator": "warn"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 설정 프리셋
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
// 권장 설정 (경고)
|
|
72
|
+
...ariaStateValidator.configs.recommended
|
|
73
|
+
|
|
74
|
+
// 엄격한 설정 (에러)
|
|
75
|
+
...ariaStateValidator.configs.strict
|
|
76
|
+
|
|
77
|
+
// 동적 검증만
|
|
78
|
+
...ariaStateValidator.configs['dynamic-only']
|
|
79
|
+
|
|
80
|
+
// 정적 검증만
|
|
81
|
+
...ariaStateValidator.configs['static-only']
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 두 가지 규칙으로 포괄적인 ARIA 검증
|
|
85
|
+
|
|
86
|
+
이 플러그인은 두 가지 보완적인 규칙을 제공합니다:
|
|
87
|
+
|
|
88
|
+
### 규칙 1: `state-dependent-aria-validator` (동적 상태 검증)
|
|
89
|
+
|
|
90
|
+
ARIA **상태** 속성이 컴포넌트 상태와 올바르게 연결되어 동적으로 업데이트되는지 검증합니다.
|
|
91
|
+
|
|
92
|
+
### 규칙 2: `static-aria-validator` (정적 ARIA 검증)
|
|
93
|
+
|
|
94
|
+
사용된 ARIA 속성의 **정확성**을 검증합니다:
|
|
95
|
+
|
|
96
|
+
- 유효한 ARIA 속성 이름 (오타 감지)
|
|
97
|
+
- Boolean 및 Enum ARIA 속성의 올바른 값
|
|
98
|
+
- 충돌하는 ARIA 속성 감지
|
|
99
|
+
- 네이티브 HTML 요소의 중복 role 감지
|
|
100
|
+
|
|
101
|
+
**참고:** 이 플러그인은 ARIA 속성을 **어떻게** 사용하는지를 검증하며, **사용 여부**를 강제하지는 않습니다. 접근성 기능의 존재를 강제하려면 [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y)를 사용하세요.
|
|
102
|
+
|
|
103
|
+
## 기능 및 검증 패턴
|
|
104
|
+
|
|
105
|
+
## 동적 상태 검증 패턴
|
|
106
|
+
|
|
107
|
+
### 1. Boolean 값 직접 바인딩 감지
|
|
108
|
+
|
|
109
|
+
**감지 내용:** ARIA 속성에 Boolean 값을 직접 바인딩
|
|
110
|
+
|
|
111
|
+
```jsx
|
|
112
|
+
// ❌ 오류: Boolean 값 직접 바인딩
|
|
113
|
+
<button aria-expanded={isOpen}>토글</button>
|
|
114
|
+
|
|
115
|
+
// ✅ 올바름: 명시적 문자열 변환
|
|
116
|
+
<button aria-expanded={isOpen ? 'true' : 'false'}>토글</button>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**자동 수정 가능:** `eslint --fix`가 자동으로 올바른 문자열 형식으로 변환합니다
|
|
120
|
+
|
|
121
|
+
### 2. 정적 값 + 인터랙티브 핸들러 감지
|
|
122
|
+
|
|
123
|
+
**감지 내용:** 이벤트 핸들러가 있는 요소의 정적 ARIA 상태 값
|
|
124
|
+
|
|
125
|
+
```jsx
|
|
126
|
+
// ❌ 오류: onClick이 있지만 aria-expanded가 정적
|
|
127
|
+
<button onClick={handleClick} aria-expanded="false">
|
|
128
|
+
토글
|
|
129
|
+
</button>
|
|
130
|
+
|
|
131
|
+
// ✅ 올바름: 동적 상태 바인딩
|
|
132
|
+
<button onClick={handleClick} aria-expanded={isOpen ? 'true' : 'false'}>
|
|
133
|
+
토글
|
|
134
|
+
</button>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**스마트 자동 수정:** 핸들러에 상태 변수 참조가 있으면 (예: `() => setExpanded(!expanded)`), 플러그인이 자동으로 변수 이름을 추출하여 ARIA 속성에 바인딩합니다:
|
|
138
|
+
|
|
139
|
+
```jsx
|
|
140
|
+
// ❌ 자동 수정 전
|
|
141
|
+
<div onClick={() => setExpanded(!expanded)} aria-expanded="false">
|
|
142
|
+
클릭하여 확장
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
// ✅ 자동 수정 후 (자동으로 'expanded' 변수 감지)
|
|
146
|
+
<div onClick={() => setExpanded(!expanded)} aria-expanded={expanded ? 'true' : 'false'}>
|
|
147
|
+
클릭하여 확장
|
|
148
|
+
</div>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 3. 역할 기반 상태 연관성 검증
|
|
152
|
+
|
|
153
|
+
**감지 내용:** 정적 ARIA 상태를 가진 인터랙티브 역할
|
|
154
|
+
|
|
155
|
+
```jsx
|
|
156
|
+
// ❌ 오류: role="button"인데 aria-pressed가 정적
|
|
157
|
+
<div role="button" aria-pressed="false">버튼</div>
|
|
158
|
+
|
|
159
|
+
// ✅ 올바름: 동적 상태 바인딩
|
|
160
|
+
<div role="button" aria-pressed={isPressed ? 'true' : 'false'}>버튼</div>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 정적 ARIA 검증 패턴
|
|
166
|
+
|
|
167
|
+
### 4. 유효하지 않은 ARIA 속성 감지
|
|
168
|
+
|
|
169
|
+
**감지 내용:** ARIA 속성 이름의 오타
|
|
170
|
+
|
|
171
|
+
```jsx
|
|
172
|
+
// ❌ 오류: aria-label의 오타
|
|
173
|
+
<button aria-labell="닫기">X</button>
|
|
174
|
+
|
|
175
|
+
// ✅ 올바름: 올바른 철자
|
|
176
|
+
<button aria-label="닫기">X</button>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 5. 빈 필수 값
|
|
180
|
+
|
|
181
|
+
**감지 내용:** 비어있지 않은 값이 필요한 ARIA 속성
|
|
182
|
+
|
|
183
|
+
```jsx
|
|
184
|
+
// ❌ 오류: aria-label이 비어있음
|
|
185
|
+
<div aria-label="">내용</div>
|
|
186
|
+
|
|
187
|
+
// ✅ 올바름: 비어있지 않은 값
|
|
188
|
+
<div aria-label="설명">내용</div>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 6. 유효하지 않은 Boolean 값
|
|
192
|
+
|
|
193
|
+
**감지 내용:** 잘못된 boolean 형식 값
|
|
194
|
+
|
|
195
|
+
```jsx
|
|
196
|
+
// ❌ 오류: 유효하지 않은 boolean 값
|
|
197
|
+
<div aria-hidden="yes">내용</div>
|
|
198
|
+
<div aria-disabled="1">내용</div>
|
|
199
|
+
|
|
200
|
+
// ✅ 올바름: "true" 또는 "false" 사용
|
|
201
|
+
<div aria-hidden="true">내용</div>
|
|
202
|
+
<div aria-disabled="false">내용</div>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 7. 유효하지 않은 Enum 값
|
|
206
|
+
|
|
207
|
+
**감지 내용:** Enum 타입 ARIA 속성의 유효하지 않은 값
|
|
208
|
+
|
|
209
|
+
```jsx
|
|
210
|
+
// ❌ 오류: 유효하지 않은 aria-live 값
|
|
211
|
+
<div aria-live="loud">라이브 영역</div>
|
|
212
|
+
|
|
213
|
+
// ✅ 올바름: 유효한 enum 값 사용
|
|
214
|
+
<div aria-live="polite">라이브 영역</div>
|
|
215
|
+
|
|
216
|
+
// 유효한 값: "off", "polite", "assertive"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### 8. 충돌하는 ARIA 속성
|
|
220
|
+
|
|
221
|
+
**감지 내용:** aria-label과 aria-labelledby를 함께 사용
|
|
222
|
+
|
|
223
|
+
```jsx
|
|
224
|
+
// ❌ 오류: 두 속성 모두 존재 (aria-labelledby가 우선)
|
|
225
|
+
<div aria-label="레이블" aria-labelledby="other-id">내용</div>
|
|
226
|
+
|
|
227
|
+
// ✅ 올바름: 하나만 사용
|
|
228
|
+
<div aria-labelledby="other-id">내용</div>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### 9. 중복 Role
|
|
232
|
+
|
|
233
|
+
**감지 내용:** 네이티브 HTML 시맨틱과 일치하는 명시적 role
|
|
234
|
+
|
|
235
|
+
```jsx
|
|
236
|
+
// ❌ 오류: 중복 role
|
|
237
|
+
<button role="button">클릭</button>
|
|
238
|
+
<nav role="navigation">내비게이션</nav>
|
|
239
|
+
<a href="#" role="link">링크</a>
|
|
240
|
+
|
|
241
|
+
// ✅ 올바름: 네이티브 시맨틱 사용
|
|
242
|
+
<button>클릭</button>
|
|
243
|
+
<nav>내비게이션</nav>
|
|
244
|
+
<a href="#">링크</a>
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## 지원하는 ARIA 속성 (동적 검증)
|
|
250
|
+
|
|
251
|
+
플러그인이 검증하는 동적 ARIA 상태 속성:
|
|
252
|
+
|
|
253
|
+
- `aria-expanded` - 토글 버튼, 확장 가능한 요소
|
|
254
|
+
- `aria-selected` - 탭, 선택 가능한 옵션
|
|
255
|
+
- `aria-checked` - 체크박스, 라디오 버튼
|
|
256
|
+
- `aria-pressed` - 토글 버튼
|
|
257
|
+
- `aria-hidden` - 동적으로 표시/숨김되는 요소
|
|
258
|
+
- `aria-disabled` - 동적으로 활성화/비활성화되는 요소
|
|
259
|
+
- `aria-modal` - 다이얼로그, 모달
|
|
260
|
+
- `aria-current` - 현재 활성 항목 표시
|
|
261
|
+
|
|
262
|
+
## 지원하는 인터랙티브 역할
|
|
263
|
+
|
|
264
|
+
다음 역할을 가진 요소는 동적 ARIA 상태가 필요합니다:
|
|
265
|
+
|
|
266
|
+
- `button`, `tab`, `checkbox`, `radio`, `switch`
|
|
267
|
+
- `menuitem`, `menuitemcheckbox`, `menuitemradio`
|
|
268
|
+
- `option`, `treeitem`
|
|
269
|
+
- `dialog`, `alertdialog`
|
|
270
|
+
|
|
271
|
+
## 예제
|
|
272
|
+
|
|
273
|
+
### 올바른 패턴 ✅
|
|
274
|
+
|
|
275
|
+
```jsx
|
|
276
|
+
// 삼항 연산자로 문자열 변환
|
|
277
|
+
<button aria-expanded={isOpen ? 'true' : 'false'}>토글</button>
|
|
278
|
+
|
|
279
|
+
// String() 함수
|
|
280
|
+
<button aria-pressed={String(isPressed)}>토글</button>
|
|
281
|
+
|
|
282
|
+
// .toString() 메서드
|
|
283
|
+
<div aria-hidden={isHidden.toString()}>내용</div>
|
|
284
|
+
|
|
285
|
+
// 반전된 boolean을 삼항 연산자로
|
|
286
|
+
<button aria-expanded={!isOpen ? 'true' : 'false'}>토글</button>
|
|
287
|
+
|
|
288
|
+
// 비인터랙티브 요소의 정적 ARIA (핸들러 없음)
|
|
289
|
+
<div aria-label="정적 레이블">내용</div>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### 잘못된 패턴 ❌
|
|
293
|
+
|
|
294
|
+
```jsx
|
|
295
|
+
// Boolean 직접 바인딩
|
|
296
|
+
<button aria-expanded={isOpen}>토글</button>
|
|
297
|
+
// 수정: aria-expanded={isOpen ? 'true' : 'false'}
|
|
298
|
+
|
|
299
|
+
// 문자열 변환 없는 논리 표현식
|
|
300
|
+
<button aria-expanded={foo && bar}>토글</button>
|
|
301
|
+
// 수정: aria-expanded={foo && bar ? 'true' : 'false'}
|
|
302
|
+
|
|
303
|
+
// 이벤트 핸들러와 정적 값
|
|
304
|
+
<button onClick={handleClick} aria-expanded="false">토글</button>
|
|
305
|
+
// 수정: 동적 상태 바인딩 사용
|
|
306
|
+
|
|
307
|
+
// 인터랙티브 role과 정적 ARIA
|
|
308
|
+
<div role="tab" aria-selected="true">탭</div>
|
|
309
|
+
// 수정: 동적 상태 바인딩 사용
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## 기술 구현
|
|
313
|
+
|
|
314
|
+
### 기술 스택
|
|
315
|
+
|
|
316
|
+
- **기반:** ESLint 플러그인 (Node.js 환경)
|
|
317
|
+
- **파서:** JSX/TSX AST 파싱을 위한 `@babel/eslint-parser` 또는 TypeScript 파서
|
|
318
|
+
- **분석:** JSX 요소에 대한 Visitor 패턴
|
|
319
|
+
|
|
320
|
+
### 분석 로직
|
|
321
|
+
|
|
322
|
+
1. **JSXElement 순회:** `role` 및 `aria-*` 속성 식별
|
|
323
|
+
2. **스코프 추적:** `context.getScope()`를 사용하여 ARIA 속성에 바인딩된 변수 추적
|
|
324
|
+
3. **상태 추론:** 값이 `useState` 훅이나 props에서 파생되었는지 판단
|
|
325
|
+
4. **패턴 검증:** 올바른 문자열 변환 패턴 확인
|
|
326
|
+
|
|
327
|
+
## 기대 효과
|
|
328
|
+
|
|
329
|
+
### 1. 접근성 품질 향상
|
|
330
|
+
|
|
331
|
+
런타임에서만 발견되던 동적 ARIA 오류를 빌드/CI 단계에서 선제적으로 감지
|
|
332
|
+
|
|
333
|
+
### 2. 개발자 생산성
|
|
334
|
+
|
|
335
|
+
`eslint --fix`로 반복적인 ARIA 상태 바인딩 오류를 자동 수정
|
|
336
|
+
|
|
337
|
+
### 3. 표준화
|
|
338
|
+
|
|
339
|
+
프로젝트 전반에 걸쳐 일관된 동적 ARIA 사용 패턴 강제
|
|
340
|
+
|
|
341
|
+
## 개발
|
|
342
|
+
|
|
343
|
+
### 테스트 실행
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
npm test
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### 빌드
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
npm run build # 해당되는 경우
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## 기여
|
|
356
|
+
|
|
357
|
+
기여는 언제나 환영합니다! 이슈나 풀 리퀘스트를 자유롭게 제출해주세요.
|
|
358
|
+
|
|
359
|
+
## 라이선스
|
|
360
|
+
|
|
361
|
+
ISC
|
|
362
|
+
|
|
363
|
+
## 키워드
|
|
364
|
+
|
|
365
|
+
- eslint
|
|
366
|
+
- eslint-plugin
|
|
367
|
+
- accessibility
|
|
368
|
+
- a11y
|
|
369
|
+
- aria
|
|
370
|
+
- jsx
|
|
371
|
+
- react
|
|
372
|
+
- state-validation
|
|
373
|
+
- dynamic-attributes
|
|
374
|
+
|
|
375
|
+
## 관련 프로젝트
|
|
376
|
+
|
|
377
|
+
- [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) - 포괄적인 JSX 접근성 검사
|
|
378
|
+
- [axe-core](https://github.com/dequelabs/axe-core) - 런타임 접근성 테스팅 엔진
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
**참고:** 이 플러그인은 ARIA 상태 바인딩 패턴의 정적 분석에 집중합니다. 포괄적인 접근성 테스팅을 위해 axe-core 같은 런타임 테스팅 도구와 함께 사용하세요.
|