rails_accessibility_testing 1.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.
- checksums.yaml +7 -0
- data/ARCHITECTURE.md +307 -0
- data/CHANGELOG.md +81 -0
- data/CODE_OF_CONDUCT.md +125 -0
- data/CONTRIBUTING.md +225 -0
- data/GUIDES/continuous_integration.md +326 -0
- data/GUIDES/getting_started.md +205 -0
- data/GUIDES/working_with_designers_and_content_authors.md +398 -0
- data/GUIDES/writing_accessible_views_in_rails.md +412 -0
- data/LICENSE +22 -0
- data/README.md +350 -0
- data/docs_site/404.html +11 -0
- data/docs_site/Gemfile +11 -0
- data/docs_site/Makefile +14 -0
- data/docs_site/_config.yml +41 -0
- data/docs_site/_includes/header.html +13 -0
- data/docs_site/_layouts/default.html +130 -0
- data/docs_site/assets/main.scss +4 -0
- data/docs_site/ci_integration.md +76 -0
- data/docs_site/configuration.md +114 -0
- data/docs_site/contributing.md +69 -0
- data/docs_site/getting_started.md +57 -0
- data/docs_site/index.md +57 -0
- data/exe/rails_a11y +12 -0
- data/exe/rails_server_safe +41 -0
- data/lib/generators/rails_a11y/install/generator.rb +51 -0
- data/lib/rails_accessibility_testing/accessibility_helper.rb +701 -0
- data/lib/rails_accessibility_testing/change_detector.rb +114 -0
- data/lib/rails_accessibility_testing/checks/aria_landmarks_check.rb +33 -0
- data/lib/rails_accessibility_testing/checks/base_check.rb +156 -0
- data/lib/rails_accessibility_testing/checks/color_contrast_check.rb +56 -0
- data/lib/rails_accessibility_testing/checks/duplicate_ids_check.rb +49 -0
- data/lib/rails_accessibility_testing/checks/form_errors_check.rb +40 -0
- data/lib/rails_accessibility_testing/checks/form_labels_check.rb +62 -0
- data/lib/rails_accessibility_testing/checks/heading_hierarchy_check.rb +53 -0
- data/lib/rails_accessibility_testing/checks/image_alt_text_check.rb +52 -0
- data/lib/rails_accessibility_testing/checks/interactive_elements_check.rb +66 -0
- data/lib/rails_accessibility_testing/checks/keyboard_accessibility_check.rb +36 -0
- data/lib/rails_accessibility_testing/checks/skip_links_check.rb +24 -0
- data/lib/rails_accessibility_testing/checks/table_structure_check.rb +36 -0
- data/lib/rails_accessibility_testing/cli/command.rb +259 -0
- data/lib/rails_accessibility_testing/config/yaml_loader.rb +131 -0
- data/lib/rails_accessibility_testing/configuration.rb +30 -0
- data/lib/rails_accessibility_testing/engine/rule_engine.rb +97 -0
- data/lib/rails_accessibility_testing/engine/violation.rb +58 -0
- data/lib/rails_accessibility_testing/engine/violation_collector.rb +59 -0
- data/lib/rails_accessibility_testing/error_message_builder.rb +354 -0
- data/lib/rails_accessibility_testing/integration/minitest_integration.rb +74 -0
- data/lib/rails_accessibility_testing/rspec_integration.rb +58 -0
- data/lib/rails_accessibility_testing/shared_examples.rb +93 -0
- data/lib/rails_accessibility_testing/version.rb +4 -0
- data/lib/rails_accessibility_testing.rb +83 -0
- data/lib/tasks/accessibility.rake +28 -0
- metadata +218 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
# Writing Accessible Views in Rails
|
|
2
|
+
|
|
3
|
+
This guide shows you how to write accessible Rails views that pass Rails A11y checks and meet WCAG 2.1 AA standards.
|
|
4
|
+
|
|
5
|
+
## Core Principles
|
|
6
|
+
|
|
7
|
+
1. **Semantic HTML** - Use the right elements for the job
|
|
8
|
+
2. **Clear Structure** - Logical heading hierarchy and landmarks
|
|
9
|
+
3. **Accessible Forms** - Proper labels and error associations
|
|
10
|
+
4. **Descriptive Content** - Alt text, link text, and button labels
|
|
11
|
+
5. **Keyboard Navigation** - Everything works without a mouse
|
|
12
|
+
|
|
13
|
+
## Forms
|
|
14
|
+
|
|
15
|
+
### ✅ Good: Proper Labels
|
|
16
|
+
|
|
17
|
+
```erb
|
|
18
|
+
<%= form_with model: @user do |f| %>
|
|
19
|
+
<%= f.label :email, "Email Address" %>
|
|
20
|
+
<%= f.email_field :email %>
|
|
21
|
+
|
|
22
|
+
<%= f.label :password, "Password" %>
|
|
23
|
+
<%= f.password_field :password %>
|
|
24
|
+
<% end %>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### ❌ Bad: Missing Labels
|
|
28
|
+
|
|
29
|
+
```erb
|
|
30
|
+
<%= form_with model: @user do |f| %>
|
|
31
|
+
<%= f.email_field :email %> <!-- No label! -->
|
|
32
|
+
<%= f.password_field :password %> <!-- No label! -->
|
|
33
|
+
<% end %>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Alternative: aria-label
|
|
37
|
+
|
|
38
|
+
For icon-only inputs, use `aria-label`:
|
|
39
|
+
|
|
40
|
+
```erb
|
|
41
|
+
<%= f.search_field :query, aria: { label: "Search" } %>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Form Errors
|
|
45
|
+
|
|
46
|
+
Associate error messages with inputs:
|
|
47
|
+
|
|
48
|
+
```erb
|
|
49
|
+
<%= form_with model: @user do |f| %>
|
|
50
|
+
<%= f.label :email %>
|
|
51
|
+
<%= f.email_field :email,
|
|
52
|
+
aria: {
|
|
53
|
+
describedby: "email-error",
|
|
54
|
+
invalid: @user.errors[:email].any?
|
|
55
|
+
} %>
|
|
56
|
+
<% if @user.errors[:email].any? %>
|
|
57
|
+
<div id="email-error" class="error">
|
|
58
|
+
<%= @user.errors[:email].first %>
|
|
59
|
+
</div>
|
|
60
|
+
<% end %>
|
|
61
|
+
<% end %>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Images
|
|
65
|
+
|
|
66
|
+
### ✅ Good: Descriptive Alt Text
|
|
67
|
+
|
|
68
|
+
```erb
|
|
69
|
+
<%= image_tag "logo.png", alt: "Company Logo" %>
|
|
70
|
+
|
|
71
|
+
<!-- For decorative images -->
|
|
72
|
+
<%= image_tag "decoration.png", alt: "" %>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### ❌ Bad: Missing Alt
|
|
76
|
+
|
|
77
|
+
```erb
|
|
78
|
+
<%= image_tag "logo.png" %> <!-- Missing alt! -->
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### When to Use Empty Alt
|
|
82
|
+
|
|
83
|
+
Use `alt=""` only for purely decorative images:
|
|
84
|
+
|
|
85
|
+
```erb
|
|
86
|
+
<!-- Decorative border -->
|
|
87
|
+
<%= image_tag "border.png", alt: "" %>
|
|
88
|
+
|
|
89
|
+
<!-- Spacer image -->
|
|
90
|
+
<%= image_tag "spacer.gif", alt: "" %>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Links and Buttons
|
|
94
|
+
|
|
95
|
+
### ✅ Good: Descriptive Text
|
|
96
|
+
|
|
97
|
+
```erb
|
|
98
|
+
<%= link_to "Read More", article_path(@article) %>
|
|
99
|
+
|
|
100
|
+
<%= button_to "Submit", submit_path, method: :post %>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### ❌ Bad: Generic or Missing Text
|
|
104
|
+
|
|
105
|
+
```erb
|
|
106
|
+
<%= link_to "Click here", article_path(@article) %> <!-- Generic! -->
|
|
107
|
+
<%= link_to article_path(@article) do %>
|
|
108
|
+
<i class="icon"></i> <!-- No accessible name! -->
|
|
109
|
+
<% end %>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Icon-Only Links/Buttons
|
|
113
|
+
|
|
114
|
+
For icon-only interactive elements, use `aria-label`:
|
|
115
|
+
|
|
116
|
+
```erb
|
|
117
|
+
<%= link_to article_path(@article), aria: { label: "Read article" } do %>
|
|
118
|
+
<i class="icon-read"></i>
|
|
119
|
+
<% end %>
|
|
120
|
+
|
|
121
|
+
<%= button_tag type: "submit", aria: { label: "Close dialog" } do %>
|
|
122
|
+
<i class="icon-close"></i>
|
|
123
|
+
<% end %>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Visually Hidden Text
|
|
127
|
+
|
|
128
|
+
Alternative approach using CSS:
|
|
129
|
+
|
|
130
|
+
```erb
|
|
131
|
+
<%= link_to article_path(@article) do %>
|
|
132
|
+
<i class="icon-read"></i>
|
|
133
|
+
<span class="visually-hidden">Read article</span>
|
|
134
|
+
<% end %>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```css
|
|
138
|
+
.visually-hidden {
|
|
139
|
+
position: absolute;
|
|
140
|
+
width: 1px;
|
|
141
|
+
height: 1px;
|
|
142
|
+
padding: 0;
|
|
143
|
+
margin: -1px;
|
|
144
|
+
overflow: hidden;
|
|
145
|
+
clip: rect(0, 0, 0, 0);
|
|
146
|
+
white-space: nowrap;
|
|
147
|
+
border-width: 0;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Headings
|
|
152
|
+
|
|
153
|
+
### ✅ Good: Proper Hierarchy
|
|
154
|
+
|
|
155
|
+
```erb
|
|
156
|
+
<h1>Page Title</h1>
|
|
157
|
+
<h2>Section Title</h2>
|
|
158
|
+
<h3>Subsection Title</h3>
|
|
159
|
+
<h2>Another Section</h2>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### ❌ Bad: Skipped Levels
|
|
163
|
+
|
|
164
|
+
```erb
|
|
165
|
+
<h1>Page Title</h1>
|
|
166
|
+
<h3>Subsection</h3> <!-- Skipped h2! -->
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### ❌ Bad: Multiple H1s
|
|
170
|
+
|
|
171
|
+
```erb
|
|
172
|
+
<h1>Main Title</h1>
|
|
173
|
+
<h1>Another Title</h1> <!-- Should be h2! -->
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Rails Helper
|
|
177
|
+
|
|
178
|
+
Use a helper to manage heading levels:
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
# app/helpers/application_helper.rb
|
|
182
|
+
def heading(text, level: 2)
|
|
183
|
+
content_tag("h#{level}", text)
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Landmarks
|
|
188
|
+
|
|
189
|
+
### ✅ Good: Semantic Structure
|
|
190
|
+
|
|
191
|
+
```erb
|
|
192
|
+
<body>
|
|
193
|
+
<header>
|
|
194
|
+
<nav>
|
|
195
|
+
<!-- Navigation -->
|
|
196
|
+
</nav>
|
|
197
|
+
</header>
|
|
198
|
+
|
|
199
|
+
<main>
|
|
200
|
+
<%= yield %>
|
|
201
|
+
</main>
|
|
202
|
+
|
|
203
|
+
<footer>
|
|
204
|
+
<!-- Footer content -->
|
|
205
|
+
</footer>
|
|
206
|
+
</body>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### ARIA Landmarks
|
|
210
|
+
|
|
211
|
+
If you can't use semantic HTML:
|
|
212
|
+
|
|
213
|
+
```erb
|
|
214
|
+
<div role="main">
|
|
215
|
+
<%= yield %>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<div role="navigation">
|
|
219
|
+
<!-- Navigation -->
|
|
220
|
+
</div>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Tables
|
|
224
|
+
|
|
225
|
+
### ✅ Good: Proper Headers
|
|
226
|
+
|
|
227
|
+
```erb
|
|
228
|
+
<table>
|
|
229
|
+
<thead>
|
|
230
|
+
<tr>
|
|
231
|
+
<th>Name</th>
|
|
232
|
+
<th>Email</th>
|
|
233
|
+
<th>Role</th>
|
|
234
|
+
</tr>
|
|
235
|
+
</thead>
|
|
236
|
+
<tbody>
|
|
237
|
+
<% @users.each do |user| %>
|
|
238
|
+
<tr>
|
|
239
|
+
<td><%= user.name %></td>
|
|
240
|
+
<td><%= user.email %></td>
|
|
241
|
+
<td><%= user.role %></td>
|
|
242
|
+
</tr>
|
|
243
|
+
<% end %>
|
|
244
|
+
</tbody>
|
|
245
|
+
</table>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### With Caption
|
|
249
|
+
|
|
250
|
+
```erb
|
|
251
|
+
<table>
|
|
252
|
+
<caption>User Directory</caption>
|
|
253
|
+
<thead>
|
|
254
|
+
<!-- ... -->
|
|
255
|
+
</thead>
|
|
256
|
+
</table>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Skip Links
|
|
260
|
+
|
|
261
|
+
Add skip navigation links:
|
|
262
|
+
|
|
263
|
+
```erb
|
|
264
|
+
<a href="#main-content" class="skip-link">Skip to main content</a>
|
|
265
|
+
|
|
266
|
+
<header>
|
|
267
|
+
<!-- Navigation -->
|
|
268
|
+
</header>
|
|
269
|
+
|
|
270
|
+
<main id="main-content">
|
|
271
|
+
<%= yield %>
|
|
272
|
+
</main>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
```css
|
|
276
|
+
.skip-link {
|
|
277
|
+
position: absolute;
|
|
278
|
+
top: -40px;
|
|
279
|
+
left: 0;
|
|
280
|
+
background: #000;
|
|
281
|
+
color: #fff;
|
|
282
|
+
padding: 8px;
|
|
283
|
+
text-decoration: none;
|
|
284
|
+
z-index: 100;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.skip-link:focus {
|
|
288
|
+
top: 0;
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Modals and Dialogs
|
|
293
|
+
|
|
294
|
+
### ✅ Good: Focusable Elements
|
|
295
|
+
|
|
296
|
+
```erb
|
|
297
|
+
<div role="dialog" aria-labelledby="modal-title">
|
|
298
|
+
<h2 id="modal-title">Confirm Action</h2>
|
|
299
|
+
<p>Are you sure?</p>
|
|
300
|
+
<button>Cancel</button>
|
|
301
|
+
<button>Confirm</button>
|
|
302
|
+
</div>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Focus Management
|
|
306
|
+
|
|
307
|
+
Use JavaScript to trap focus:
|
|
308
|
+
|
|
309
|
+
```javascript
|
|
310
|
+
// Trap focus in modal
|
|
311
|
+
const modal = document.querySelector('[role="dialog"]');
|
|
312
|
+
const focusableElements = modal.querySelectorAll(
|
|
313
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
314
|
+
);
|
|
315
|
+
const firstElement = focusableElements[0];
|
|
316
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
317
|
+
|
|
318
|
+
firstElement.focus();
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Color and Contrast
|
|
322
|
+
|
|
323
|
+
### ✅ Good: Sufficient Contrast
|
|
324
|
+
|
|
325
|
+
```erb
|
|
326
|
+
<!-- Dark text on light background -->
|
|
327
|
+
<p style="color: #000; background: #fff;">Readable text</p>
|
|
328
|
+
|
|
329
|
+
<!-- Light text on dark background -->
|
|
330
|
+
<p style="color: #fff; background: #000;">Readable text</p>
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### ❌ Bad: Low Contrast
|
|
334
|
+
|
|
335
|
+
```erb
|
|
336
|
+
<!-- Hard to read -->
|
|
337
|
+
<p style="color: #ccc; background: #fff;">Poor contrast</p>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Tools
|
|
341
|
+
|
|
342
|
+
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
|
|
343
|
+
- Browser DevTools (Chrome Lighthouse)
|
|
344
|
+
|
|
345
|
+
## Common Patterns
|
|
346
|
+
|
|
347
|
+
### Breadcrumbs
|
|
348
|
+
|
|
349
|
+
```erb
|
|
350
|
+
<nav aria-label="Breadcrumb">
|
|
351
|
+
<ol>
|
|
352
|
+
<li><%= link_to "Home", root_path %></li>
|
|
353
|
+
<li><%= link_to "Products", products_path %></li>
|
|
354
|
+
<li aria-current="page"><%= @product.name %></li>
|
|
355
|
+
</ol>
|
|
356
|
+
</nav>
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Error Messages
|
|
360
|
+
|
|
361
|
+
```erb
|
|
362
|
+
<% if @user.errors.any? %>
|
|
363
|
+
<div role="alert" aria-live="polite">
|
|
364
|
+
<h2>Please fix the following errors:</h2>
|
|
365
|
+
<ul>
|
|
366
|
+
<% @user.errors.full_messages.each do |message| %>
|
|
367
|
+
<li><%= message %></li>
|
|
368
|
+
<% end %>
|
|
369
|
+
</ul>
|
|
370
|
+
</div>
|
|
371
|
+
<% end %>
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Loading States
|
|
375
|
+
|
|
376
|
+
```erb
|
|
377
|
+
<button aria-busy="true" aria-label="Loading...">
|
|
378
|
+
<span class="spinner"></span>
|
|
379
|
+
<span class="visually-hidden">Loading</span>
|
|
380
|
+
</button>
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Testing Your Views
|
|
384
|
+
|
|
385
|
+
Run Rails A11y checks:
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
# Run all system tests
|
|
389
|
+
bundle exec rspec spec/system/
|
|
390
|
+
|
|
391
|
+
# Check specific routes
|
|
392
|
+
bundle exec rails_a11y check --routes home_path about_path
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## Resources
|
|
396
|
+
|
|
397
|
+
- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
|
|
398
|
+
- [WebAIM](https://webaim.org/) - Accessibility resources
|
|
399
|
+
- [A11y Project](https://www.a11yproject.com/) - Community-driven
|
|
400
|
+
- [MDN Accessibility](https://developer.mozilla.org/en-US/docs/Web/Accessibility)
|
|
401
|
+
|
|
402
|
+
## Next Steps
|
|
403
|
+
|
|
404
|
+
- **Run checks regularly** - Catch issues early
|
|
405
|
+
- **Review with screen readers** - Test with NVDA, JAWS, or VoiceOver
|
|
406
|
+
- **Keyboard testing** - Navigate without a mouse
|
|
407
|
+
- **Color blindness** - Test with color blindness simulators
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
**Remember:** Accessibility isn't optional—it's a requirement. These practices make your app usable for everyone.
|
|
412
|
+
|
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2025 Regan Maharjan
|
|
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.
|
|
22
|
+
|