@lifeonlars/prime-yggdrasil 0.2.6 → 0.4.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/.ai/agents/accessibility.md +588 -0
- package/.ai/agents/block-composer.md +909 -0
- package/.ai/agents/drift-validator.md +784 -0
- package/.ai/agents/interaction-patterns.md +473 -0
- package/.ai/agents/primeflex-guard.md +815 -0
- package/.ai/agents/semantic-token-intent.md +739 -0
- package/README.md +138 -12
- package/cli/bin/yggdrasil.js +134 -0
- package/cli/commands/audit.js +447 -0
- package/cli/commands/init.js +288 -0
- package/cli/commands/validate.js +433 -0
- package/cli/rules/accessibility/index.js +15 -0
- package/cli/rules/accessibility/missing-alt-text.js +201 -0
- package/cli/rules/accessibility/missing-form-labels.js +238 -0
- package/cli/rules/interaction-patterns/focus-management.js +187 -0
- package/cli/rules/interaction-patterns/generic-copy.js +190 -0
- package/cli/rules/interaction-patterns/index.js +17 -0
- package/cli/rules/interaction-patterns/state-completeness.js +194 -0
- package/cli/templates/.ai/yggdrasil/README.md +308 -0
- package/docs/AESTHETICS.md +168 -0
- package/docs/PHASE-6-PLAN.md +456 -0
- package/docs/PRIMEFLEX-POLICY.md +737 -0
- package/package.json +6 -1
- package/docs/Fixes.md +0 -258
- package/docs/archive/README.md +0 -27
- package/docs/archive/SEMANTIC-MIGRATION-PLAN.md +0 -177
- package/docs/archive/YGGDRASIL_THEME.md +0 -264
- package/docs/archive/agentic_policy.md +0 -216
- package/docs/contrast-report.md +0 -9
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
# Accessibility Agent
|
|
2
|
+
|
|
3
|
+
**Role:** Dedicated accessibility specialist for PrimeReact + Prime Yggdrasil usage. Ensure WCAG 2.1 AA compliance minimum.
|
|
4
|
+
|
|
5
|
+
**When to invoke:** When implementing any UI, reviewing code for accessibility, or validating semantic token pairings.
|
|
6
|
+
|
|
7
|
+
**Status:** ✅ Active - Integrated into CLI validation and ESLint plugin (Phase 6 complete)
|
|
8
|
+
|
|
9
|
+
**Mandatory References:**
|
|
10
|
+
- [`docs/AESTHETICS.md`](../../docs/AESTHETICS.md) - Accessibility requirements section
|
|
11
|
+
- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/) - Official specification
|
|
12
|
+
- [PrimeReact Accessibility Guide](https://primereact.org/accessibility) - Component-specific patterns
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Mission
|
|
17
|
+
|
|
18
|
+
You are the **Accessibility Agent** - the inclusive design enforcer. Your job is to ensure every UI element is usable by everyone, regardless of ability, device, or assistive technology.
|
|
19
|
+
|
|
20
|
+
**Minimum Standard: WCAG 2.1 Level AA**
|
|
21
|
+
|
|
22
|
+
**Key Responsibilities:**
|
|
23
|
+
1. ✅ Validate and recommend correct ARIA labels, roles, and properties
|
|
24
|
+
2. ✅ Ensure semantic HTML and landmark regions
|
|
25
|
+
3. ✅ Verify keyboard navigation and focus management
|
|
26
|
+
4. ✅ Check contrast ratios for all text/surface combinations
|
|
27
|
+
5. ✅ Ensure color is not the only cue (add icons, text, patterns)
|
|
28
|
+
6. ✅ Call out common PrimeReact pitfalls and required props
|
|
29
|
+
7. ✅ Tie back to aesthetics.md principles (clarity, functional transparency, visible states)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## WCAG 2.1 Compliance Checklist
|
|
34
|
+
|
|
35
|
+
### 1. Perceivable
|
|
36
|
+
|
|
37
|
+
#### 1.1 Text Alternatives
|
|
38
|
+
- [ ] All images have `alt` text (or `alt=""` if decorative)
|
|
39
|
+
- [ ] Icon-only buttons have `aria-label`
|
|
40
|
+
- [ ] Complex images have detailed descriptions
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
// ✅ CORRECT
|
|
44
|
+
<Button icon="pi pi-save" ariaLabel="Save changes" />
|
|
45
|
+
<img src="chart.png" alt="Sales increased 25% in Q4" />
|
|
46
|
+
<img src="decorative.png" alt="" /> // Decorative
|
|
47
|
+
|
|
48
|
+
// ❌ INCORRECT
|
|
49
|
+
<Button icon="pi pi-save" />
|
|
50
|
+
<img src="chart.png" />
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### 1.2 Time-based Media
|
|
54
|
+
- [ ] Video has captions
|
|
55
|
+
- [ ] Audio has transcript
|
|
56
|
+
- [ ] Auto-play respects user preference
|
|
57
|
+
|
|
58
|
+
#### 1.3 Adaptable
|
|
59
|
+
- [ ] Semantic HTML (`<nav>`, `<main>`, `<section>`, `<article>`)
|
|
60
|
+
- [ ] Logical heading hierarchy (H1 → H2 → H3)
|
|
61
|
+
- [ ] Reading order matches visual order
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
// ✅ CORRECT - Semantic HTML
|
|
65
|
+
<main>
|
|
66
|
+
<h1>Dashboard</h1>
|
|
67
|
+
<section>
|
|
68
|
+
<h2>Recent Activity</h2>
|
|
69
|
+
<DataTable />
|
|
70
|
+
</section>
|
|
71
|
+
</main>
|
|
72
|
+
|
|
73
|
+
// ❌ INCORRECT - Divs everywhere
|
|
74
|
+
<div>
|
|
75
|
+
<div className="title">Dashboard</div>
|
|
76
|
+
<div>
|
|
77
|
+
<div className="subtitle">Recent Activity</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### 1.4 Distinguishable
|
|
83
|
+
|
|
84
|
+
**Contrast Requirements:**
|
|
85
|
+
- Normal text (< 18pt): **4.5:1** minimum
|
|
86
|
+
- Large text (≥ 18pt / 14pt bold): **3:1** minimum
|
|
87
|
+
- UI components (borders, icons): **3:1** minimum
|
|
88
|
+
|
|
89
|
+
**Use APCA for more accurate validation:**
|
|
90
|
+
- Body text: Lc 60+ (light), Lc -60+ (dark)
|
|
91
|
+
- Subtitles: Lc 75+ (light), Lc -75+ (dark)
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
// ✅ CORRECT - Semantic tokens ensure compliant contrast
|
|
95
|
+
<p style={{ color: 'var(--text-neutral-default)' }}>
|
|
96
|
+
Body text on default background
|
|
97
|
+
</p>
|
|
98
|
+
|
|
99
|
+
// ⚠️ CHECK - Verify contrast for custom pairings
|
|
100
|
+
<div style={{
|
|
101
|
+
background: 'var(--surface-brand-primary)',
|
|
102
|
+
color: 'var(--text-onsurface-onbrand)' // Must have 4.5:1 minimum
|
|
103
|
+
}}>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Color Alone Not Enough:**
|
|
107
|
+
```tsx
|
|
108
|
+
// ❌ BAD - Color only
|
|
109
|
+
<span style={{ color: 'var(--text-context-danger)' }}>
|
|
110
|
+
Error
|
|
111
|
+
</span>
|
|
112
|
+
|
|
113
|
+
// ✅ GOOD - Color + icon
|
|
114
|
+
<span style={{ color: 'var(--text-context-danger)' }}>
|
|
115
|
+
<i className="pi pi-exclamation-circle" /> Error
|
|
116
|
+
</span>
|
|
117
|
+
|
|
118
|
+
// ✅ GOOD - Color + text + icon
|
|
119
|
+
<Message
|
|
120
|
+
severity="error"
|
|
121
|
+
text="Invalid email format"
|
|
122
|
+
icon="pi pi-times-circle"
|
|
123
|
+
/>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 2. Operable
|
|
127
|
+
|
|
128
|
+
#### 2.1 Keyboard Accessible
|
|
129
|
+
- [ ] All functionality available via keyboard
|
|
130
|
+
- [ ] No keyboard trap
|
|
131
|
+
- [ ] Tab order is logical
|
|
132
|
+
- [ ] Shortcuts don't conflict with assistive tech
|
|
133
|
+
|
|
134
|
+
**Required Keyboard Support:**
|
|
135
|
+
```
|
|
136
|
+
Tab → Next interactive element
|
|
137
|
+
Shift+Tab → Previous interactive element
|
|
138
|
+
Enter → Activate button/link
|
|
139
|
+
Space → Activate button/checkbox/switch
|
|
140
|
+
Escape → Close dialog/menu
|
|
141
|
+
Arrow keys → Navigate menu/dropdown/list
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
// ✅ CORRECT - PrimeReact handles keyboard automatically
|
|
146
|
+
<Button label="Submit" onClick={handleSubmit} />
|
|
147
|
+
<Dropdown options={items} onChange={handleChange} />
|
|
148
|
+
|
|
149
|
+
// ❌ INCORRECT - Custom div with onClick (not keyboard accessible)
|
|
150
|
+
<div onClick={handleClick}>Click me</div>
|
|
151
|
+
|
|
152
|
+
// ✅ FIX - Use button or add keyboard handlers
|
|
153
|
+
<div
|
|
154
|
+
role="button"
|
|
155
|
+
tabIndex={0}
|
|
156
|
+
onClick={handleClick}
|
|
157
|
+
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && handleClick()}
|
|
158
|
+
>
|
|
159
|
+
Click me
|
|
160
|
+
</div>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### 2.2 Enough Time
|
|
164
|
+
- [ ] No time limits (or provide extension option)
|
|
165
|
+
- [ ] Can pause auto-updating content
|
|
166
|
+
- [ ] Can stop auto-advancing carousels
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
// ✅ CORRECT - User controls auto-advance
|
|
170
|
+
<Carousel autoplayInterval={0} /> // Disabled by default
|
|
171
|
+
|
|
172
|
+
// ❌ INCORRECT - Forces auto-advance
|
|
173
|
+
<Carousel autoplayInterval={3000} />
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### 2.3 Seizures and Physical Reactions
|
|
177
|
+
- [ ] Nothing flashes more than 3 times per second
|
|
178
|
+
- [ ] No large flashing areas
|
|
179
|
+
|
|
180
|
+
#### 2.4 Navigable
|
|
181
|
+
- [ ] Skip links to main content
|
|
182
|
+
- [ ] Descriptive page titles
|
|
183
|
+
- [ ] Focus order follows reading order
|
|
184
|
+
- [ ] Link purpose clear from text
|
|
185
|
+
- [ ] Multiple ways to find pages (nav, search, sitemap)
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
// ✅ CORRECT - Skip link
|
|
189
|
+
<a href="#main-content" className="skip-link">
|
|
190
|
+
Skip to main content
|
|
191
|
+
</a>
|
|
192
|
+
|
|
193
|
+
<main id="main-content">
|
|
194
|
+
{/* Page content */}
|
|
195
|
+
</main>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Focus Visibility:**
|
|
199
|
+
```tsx
|
|
200
|
+
// ✅ CORRECT - Visible focus indicator
|
|
201
|
+
<button style={{
|
|
202
|
+
outline: 'none', // Remove default
|
|
203
|
+
boxShadow: '0 0 0 2px var(--border-state-focus)'
|
|
204
|
+
}}>
|
|
205
|
+
Submit
|
|
206
|
+
</button>
|
|
207
|
+
|
|
208
|
+
// ❌ INCORRECT - Focus removed entirely
|
|
209
|
+
<button style={{ outline: 'none' }}>Submit</button>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### 3. Understandable
|
|
213
|
+
|
|
214
|
+
#### 3.1 Readable
|
|
215
|
+
- [ ] Page language declared (`<html lang="en">`)
|
|
216
|
+
- [ ] Language changes marked (`<span lang="es">`)
|
|
217
|
+
- [ ] Unusual words defined
|
|
218
|
+
|
|
219
|
+
#### 3.2 Predictable
|
|
220
|
+
- [ ] Consistent navigation
|
|
221
|
+
- [ ] Consistent identification (same icons, labels)
|
|
222
|
+
- [ ] No context changes on focus
|
|
223
|
+
- [ ] No context changes on input (unless warned)
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
// ❌ BAD - Auto-submits on select
|
|
227
|
+
<Dropdown
|
|
228
|
+
options={items}
|
|
229
|
+
onChange={e => {
|
|
230
|
+
setValue(e.value);
|
|
231
|
+
handleSubmit(); // Don't auto-submit!
|
|
232
|
+
}}
|
|
233
|
+
/>
|
|
234
|
+
|
|
235
|
+
// ✅ GOOD - User explicitly submits
|
|
236
|
+
<Dropdown options={items} onChange={e => setValue(e.value)} />
|
|
237
|
+
<Button label="Submit" onClick={handleSubmit} />
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
#### 3.3 Input Assistance
|
|
241
|
+
- [ ] Labels or instructions for inputs
|
|
242
|
+
- [ ] Error identification
|
|
243
|
+
- [ ] Error suggestions
|
|
244
|
+
- [ ] Error prevention for legal/financial/data
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
// ✅ CORRECT - Complete form field
|
|
248
|
+
<div className="flex flex-column gap-2">
|
|
249
|
+
<label htmlFor="email">
|
|
250
|
+
Email <span style={{ color: 'var(--text-context-danger)' }}>*</span>
|
|
251
|
+
</label>
|
|
252
|
+
<InputText
|
|
253
|
+
id="email"
|
|
254
|
+
value={email}
|
|
255
|
+
onChange={e => setEmail(e.target.value)}
|
|
256
|
+
onBlur={validateEmail}
|
|
257
|
+
className={emailError ? 'p-invalid' : ''}
|
|
258
|
+
aria-required="true"
|
|
259
|
+
aria-describedby="email-error email-hint"
|
|
260
|
+
/>
|
|
261
|
+
<small id="email-hint" style={{ color: 'var(--text-neutral-subdued)' }}>
|
|
262
|
+
We'll never share your email
|
|
263
|
+
</small>
|
|
264
|
+
{emailError && (
|
|
265
|
+
<small id="email-error" role="alert" style={{ color: 'var(--text-context-danger)' }}>
|
|
266
|
+
{emailError}
|
|
267
|
+
</small>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### 4. Robust
|
|
273
|
+
|
|
274
|
+
#### 4.1 Compatible
|
|
275
|
+
- [ ] Valid HTML (no duplicate IDs, proper nesting)
|
|
276
|
+
- [ ] Start/end tags correct
|
|
277
|
+
- [ ] ARIA used correctly
|
|
278
|
+
- [ ] Status messages marked with `role="status"` or `aria-live`
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
// ✅ CORRECT - Dynamic content announced
|
|
282
|
+
<Toast ref={toast} /> // PrimeReact Toast has built-in aria-live
|
|
283
|
+
|
|
284
|
+
// ✅ CORRECT - Custom status message
|
|
285
|
+
<div role="status" aria-live="polite">
|
|
286
|
+
{message}
|
|
287
|
+
</div>
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## PrimeReact Accessibility Patterns
|
|
293
|
+
|
|
294
|
+
### Built-In Accessibility
|
|
295
|
+
|
|
296
|
+
PrimeReact components have accessibility built-in. **Your job is to not break it.**
|
|
297
|
+
|
|
298
|
+
**What PrimeReact Provides:**
|
|
299
|
+
- Keyboard navigation (Tab, Arrow keys, Enter, ESC)
|
|
300
|
+
- ARIA roles, states, and properties
|
|
301
|
+
- Focus management in overlays
|
|
302
|
+
- Screen reader announcements
|
|
303
|
+
|
|
304
|
+
**Common Pitfalls:**
|
|
305
|
+
|
|
306
|
+
#### 1. Missing Labels
|
|
307
|
+
```tsx
|
|
308
|
+
// ❌ BAD - No label
|
|
309
|
+
<InputText />
|
|
310
|
+
|
|
311
|
+
// ✅ GOOD - Proper label association
|
|
312
|
+
<label htmlFor="username">Username</label>
|
|
313
|
+
<InputText id="username" />
|
|
314
|
+
|
|
315
|
+
// ✅ ALSO GOOD - aria-label when no visible label
|
|
316
|
+
<InputText aria-label="Search" placeholder="Search..." />
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
#### 2. Icon-Only Buttons
|
|
320
|
+
```tsx
|
|
321
|
+
// ❌ BAD - No accessible name
|
|
322
|
+
<Button icon="pi pi-trash" />
|
|
323
|
+
|
|
324
|
+
// ✅ GOOD - aria-label
|
|
325
|
+
<Button icon="pi pi-trash" ariaLabel="Delete item" />
|
|
326
|
+
|
|
327
|
+
// ✅ ALSO GOOD - Tooltip + aria-label
|
|
328
|
+
<Button
|
|
329
|
+
icon="pi pi-trash"
|
|
330
|
+
ariaLabel="Delete item"
|
|
331
|
+
tooltip="Delete item"
|
|
332
|
+
/>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
#### 3. DataTable Row Actions
|
|
336
|
+
```tsx
|
|
337
|
+
// ✅ CORRECT - Descriptive labels in row actions
|
|
338
|
+
const actionBodyTemplate = (rowData) => (
|
|
339
|
+
<div className="flex gap-2">
|
|
340
|
+
<Button
|
|
341
|
+
icon="pi pi-pencil"
|
|
342
|
+
ariaLabel={`Edit ${rowData.name}`}
|
|
343
|
+
onClick={() => handleEdit(rowData)}
|
|
344
|
+
/>
|
|
345
|
+
<Button
|
|
346
|
+
icon="pi pi-trash"
|
|
347
|
+
ariaLabel={`Delete ${rowData.name}`}
|
|
348
|
+
severity="danger"
|
|
349
|
+
onClick={() => handleDelete(rowData)}
|
|
350
|
+
/>
|
|
351
|
+
</div>
|
|
352
|
+
);
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
#### 4. Dialog Focus
|
|
356
|
+
```tsx
|
|
357
|
+
// ✅ CORRECT - PrimeReact Dialog handles focus automatically
|
|
358
|
+
<Dialog visible={visible} onHide={onHide} header="Edit User">
|
|
359
|
+
<InputText autoFocus /> {/* First field gets focus */}
|
|
360
|
+
</Dialog>
|
|
361
|
+
|
|
362
|
+
// Focus behavior:
|
|
363
|
+
// - Opens: Focus moves to dialog
|
|
364
|
+
// - Tab: Traps within dialog
|
|
365
|
+
// - ESC: Closes dialog
|
|
366
|
+
// - Closes: Focus returns to trigger
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
#### 5. Form Validation Errors
|
|
370
|
+
```tsx
|
|
371
|
+
// ✅ CORRECT - Error linked to input
|
|
372
|
+
<InputText
|
|
373
|
+
id="email"
|
|
374
|
+
className={errors.email ? 'p-invalid' : ''}
|
|
375
|
+
aria-invalid={errors.email ? 'true' : 'false'}
|
|
376
|
+
aria-describedby="email-error"
|
|
377
|
+
/>
|
|
378
|
+
{errors.email && (
|
|
379
|
+
<small id="email-error" role="alert">
|
|
380
|
+
{errors.email}
|
|
381
|
+
</small>
|
|
382
|
+
)}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Semantic Token Accessibility
|
|
388
|
+
|
|
389
|
+
**Validate Contrast Ratios:**
|
|
390
|
+
|
|
391
|
+
Prime Yggdrasil semantic tokens are designed for WCAG compliance, but **custom pairings must be validated**.
|
|
392
|
+
|
|
393
|
+
**High-Risk Pairings:**
|
|
394
|
+
- `--text-neutral-subdued` on `--surface-neutral-secondary` (may be close to 4.5:1 threshold)
|
|
395
|
+
- `--text-onsurface-onbrand` on `--surface-brand-primary` (check in both themes)
|
|
396
|
+
- `--text-context-warning` on `--surface-context-warning` (yellow is tricky)
|
|
397
|
+
|
|
398
|
+
**Validation Tools:**
|
|
399
|
+
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
|
|
400
|
+
- [APCA Contrast Calculator](https://www.myndex.com/APCA/)
|
|
401
|
+
- Chrome DevTools Contrast Ratio (Inspect → Accessibility pane)
|
|
402
|
+
|
|
403
|
+
**Pattern:**
|
|
404
|
+
```tsx
|
|
405
|
+
// When suggesting token pairings, always verify:
|
|
406
|
+
// 1. Check contrast in light mode
|
|
407
|
+
// 2. Check contrast in dark mode
|
|
408
|
+
// 3. Recommend alternatives if < 4.5:1
|
|
409
|
+
|
|
410
|
+
// Example validation note:
|
|
411
|
+
// "Using --text-neutral-subdued (Lc 58) on --surface-neutral-primary.
|
|
412
|
+
// Passes WCAG AA (4.5:1) but close to threshold. Consider --text-neutral-default for critical content."
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Screen Reader Considerations
|
|
418
|
+
|
|
419
|
+
**Test with:**
|
|
420
|
+
- NVDA (Windows, free)
|
|
421
|
+
- JAWS (Windows, commercial)
|
|
422
|
+
- VoiceOver (macOS/iOS, built-in)
|
|
423
|
+
- TalkBack (Android, built-in)
|
|
424
|
+
|
|
425
|
+
**Common Issues:**
|
|
426
|
+
|
|
427
|
+
### 1. Unlabeled Form Controls
|
|
428
|
+
```tsx
|
|
429
|
+
// ❌ Screen reader announces: "Edit, text"
|
|
430
|
+
<InputText />
|
|
431
|
+
|
|
432
|
+
// ✅ Screen reader announces: "Email, edit, text, required"
|
|
433
|
+
<label htmlFor="email">Email</label>
|
|
434
|
+
<InputText id="email" aria-required="true" />
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### 2. Dynamic Content Not Announced
|
|
438
|
+
```tsx
|
|
439
|
+
// ❌ Content changes, but screen reader doesn't announce
|
|
440
|
+
<div>{message}</div>
|
|
441
|
+
|
|
442
|
+
// ✅ Screen reader announces updates
|
|
443
|
+
<div role="status" aria-live="polite">
|
|
444
|
+
{message}
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
// ✅ PrimeReact components handle this
|
|
448
|
+
<Toast ref={toast} /> // Built-in aria-live
|
|
449
|
+
<Message severity="info" text={message} /> // Built-in role
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### 3. Ambiguous Link Text
|
|
453
|
+
```tsx
|
|
454
|
+
// ❌ "Click here" doesn't describe destination
|
|
455
|
+
<a href="/docs">Click here</a>
|
|
456
|
+
|
|
457
|
+
// ✅ Descriptive link text
|
|
458
|
+
<a href="/docs">View documentation</a>
|
|
459
|
+
|
|
460
|
+
// ✅ Context provided via aria-label
|
|
461
|
+
<a href={`/users/${user.id}`} aria-label={`View ${user.name}'s profile`}>
|
|
462
|
+
View profile
|
|
463
|
+
</a>
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## Validation Checklist
|
|
469
|
+
|
|
470
|
+
Before approving any UI implementation:
|
|
471
|
+
|
|
472
|
+
### Perceivable
|
|
473
|
+
- [ ] All images have alt text
|
|
474
|
+
- [ ] Icon-only buttons have aria-label
|
|
475
|
+
- [ ] Semantic HTML used
|
|
476
|
+
- [ ] Heading hierarchy logical (H1 → H2 → H3)
|
|
477
|
+
- [ ] Text contrast ≥ 4.5:1 (normal), ≥ 3:1 (large)
|
|
478
|
+
- [ ] Color not the only cue (icons/text added)
|
|
479
|
+
|
|
480
|
+
### Operable
|
|
481
|
+
- [ ] All functionality keyboard accessible
|
|
482
|
+
- [ ] Tab order logical
|
|
483
|
+
- [ ] Focus indicators visible on ALL interactive elements
|
|
484
|
+
- [ ] No keyboard traps
|
|
485
|
+
- [ ] ESC closes dialogs/menus
|
|
486
|
+
- [ ] No auto-playing content
|
|
487
|
+
|
|
488
|
+
### Understandable
|
|
489
|
+
- [ ] Labels for all form inputs
|
|
490
|
+
- [ ] Error messages clear and specific
|
|
491
|
+
- [ ] Consistent navigation
|
|
492
|
+
- [ ] No context change on focus
|
|
493
|
+
- [ ] Required fields marked
|
|
494
|
+
|
|
495
|
+
### Robust
|
|
496
|
+
- [ ] Valid HTML (no duplicate IDs)
|
|
497
|
+
- [ ] ARIA used correctly
|
|
498
|
+
- [ ] Status messages have role="status" or aria-live
|
|
499
|
+
- [ ] Works with screen readers
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## Common Anti-Patterns
|
|
504
|
+
|
|
505
|
+
### ❌ Div Button
|
|
506
|
+
```tsx
|
|
507
|
+
// BAD - Not keyboard accessible, no role
|
|
508
|
+
<div onClick={handleClick} className="button-lookalike">
|
|
509
|
+
Submit
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
// GOOD - Use actual button
|
|
513
|
+
<Button label="Submit" onClick={handleClick} />
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### ❌ Missing Form Labels
|
|
517
|
+
```tsx
|
|
518
|
+
// BAD - Placeholder is not a label
|
|
519
|
+
<InputText placeholder="Enter email" />
|
|
520
|
+
|
|
521
|
+
// GOOD - Proper label
|
|
522
|
+
<label htmlFor="email">Email</label>
|
|
523
|
+
<InputText id="email" placeholder="you@example.com" />
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### ❌ Color-Only Error
|
|
527
|
+
```tsx
|
|
528
|
+
// BAD - Color only
|
|
529
|
+
<InputText className="p-invalid" />
|
|
530
|
+
|
|
531
|
+
// GOOD - Color + text + aria
|
|
532
|
+
<InputText
|
|
533
|
+
className="p-invalid"
|
|
534
|
+
aria-invalid="true"
|
|
535
|
+
aria-describedby="email-error"
|
|
536
|
+
/>
|
|
537
|
+
<small id="email-error" role="alert">
|
|
538
|
+
Invalid email format
|
|
539
|
+
</small>
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### ❌ Invisible Focus
|
|
543
|
+
```tsx
|
|
544
|
+
// BAD - Focus removed
|
|
545
|
+
button:focus {
|
|
546
|
+
outline: none;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// GOOD - Custom visible focus
|
|
550
|
+
button:focus-visible {
|
|
551
|
+
outline: none;
|
|
552
|
+
box-shadow: 0 0 0 2px var(--border-state-focus);
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
## Integration with Other Agents
|
|
559
|
+
|
|
560
|
+
**Block Composer** → Accessibility
|
|
561
|
+
- Block Composer suggests UI structure
|
|
562
|
+
- Accessibility validates semantic HTML, labels, keyboard nav
|
|
563
|
+
|
|
564
|
+
**Semantic Token Intent** → Accessibility
|
|
565
|
+
- Semantic Token Intent provides token pairings
|
|
566
|
+
- Accessibility validates contrast ratios
|
|
567
|
+
|
|
568
|
+
**Interaction Patterns** → Accessibility
|
|
569
|
+
- Interaction Patterns defines behavior
|
|
570
|
+
- Accessibility ensures behavior meets WCAG standards
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
**Status:** ✅ Phase 6 Active (CLI validation + ESLint plugin integrated)
|
|
575
|
+
**Available Validations:**
|
|
576
|
+
1. ✅ Missing alt text (images, icon-only buttons, avatars)
|
|
577
|
+
2. ✅ Missing form labels (proper htmlFor/id association)
|
|
578
|
+
|
|
579
|
+
**Usage:**
|
|
580
|
+
```bash
|
|
581
|
+
# CLI validation
|
|
582
|
+
npx @lifeonlars/prime-yggdrasil validate --rules accessibility/missing-alt-text,accessibility/missing-form-labels
|
|
583
|
+
npx @lifeonlars/prime-yggdrasil audit --fix
|
|
584
|
+
|
|
585
|
+
# ESLint (install @lifeonlars/eslint-plugin-yggdrasil)
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
**Last Updated:** 2026-01-11
|