PluginProbe ʕ •ᴥ•ʔ
Web Accessibility Toolkit – Accessibility Checker & ARIA for WCAG, Section 508 & ADA Compliance / 1.6.5
Web Accessibility Toolkit – Accessibility Checker & ARIA for WCAG, Section 508 & ADA Compliance v1.6.5
trunk 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6
aria-accessibility-toolkit / frontend / class-frontend-checker.php
aria-accessibility-toolkit / frontend Last commit date
class-frontend-checker.php 1 month ago class-frontend.php 1 month ago index.php 11 months ago
class-frontend-checker.php
499 lines
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) exit;
3
4 class ARIAAT_Frontend_Checker {
5
6 public function __construct() {
7 add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] );
8 add_action( 'wp_footer', [ $this, 'render_checker_panel' ] );
9 add_action( 'wp_ajax_ariaat_apply_auto_fixes', [ $this, 'ajax_apply_auto_fixes' ] );
10 }
11
12 public function enqueue_assets() {
13
14 $settings = get_option( 'ariaat_general_settings', [] );
15
16 $show_checker = apply_filters(
17 'ariaat_show_frontend_checker',
18 ! empty( $settings['enable_frontend_checker'] ) && current_user_can( 'administrator' )
19 );
20
21 if ( $show_checker ) {
22
23 $v = ARIAATVERSION;
24 //$v = time();
25
26 wp_enqueue_style(
27 'ariaat-checker-css',
28 ARIAATURL . 'assets/css/ariaat-frontend-checker.css',
29 false,
30 $v
31 );
32
33 wp_enqueue_script(
34 'ariaat-checker-js',
35 ARIAATURL . 'assets/js/ariaat-frontend-checker.js',
36 [ 'jquery' ],
37 $v,
38 true
39 );
40
41 wp_localize_script(
42 'ariaat-checker-js',
43 'ARIAAT_ErrorMap',
44 $this->get_error_definitions()
45 );
46
47 wp_localize_script(
48 'ariaat-checker-js',
49 'ARIAAT_AutoFixes',
50 $this->get_free_auto_fix_definitions()
51 );
52
53 wp_localize_script(
54 'ariaat-checker-js',
55 'ARIAAT_CheckerAjax',
56 [
57 'ajaxurl' => admin_url( 'admin-ajax.php' ),
58 'nonce' => wp_create_nonce( 'ariaat_apply_auto_fixes' ),
59 ]
60 );
61 }
62 }
63
64 public function render_checker_panel() {
65
66 $settings = get_option( 'ariaat_general_settings', [] );
67
68 $show_checker = apply_filters(
69 'ariaat_show_frontend_checker',
70 ! empty( $settings['enable_frontend_checker'] ) && current_user_can( 'administrator' )
71 );
72
73 if ( $show_checker ) {
74 ?>
75 <div id="ariaat-checker-panel" class="ariaat-ignore">
76 <div id="ariaat-checker-header">
77 <div id="ariaat-draggable">
78 <svg fill="#ffffff" width="20" height="20" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
79 <title>draggable</title>
80 <rect x="10" y="6" width="4" height="4"/>
81 <rect x="18" y="6" width="4" height="4"/>
82 <rect x="10" y="14" width="4" height="4"/>
83 <rect x="18" y="14" width="4" height="4"/>
84 <rect x="10" y="22" width="4" height="4"/>
85 <rect x="18" y="22" width="4" height="4"/>
86 </svg>
87 Accessibility Checker
88 </div>
89
90 <div class="ariaat-checker-actions">
91 <button id="run-ariaat-scan" type="button">Scan Page</button>
92 </div>
93 </div>
94
95 <div id="ariaat-checker-overview"></div>
96 <div id="ariaat-checker-results"></div>
97 </div>
98 <?php
99 }
100 }
101
102 private function get_free_auto_fix_definitions() : array {
103 $definitions = $this->get_error_definitions();
104 $fixable = [];
105
106 foreach ( $definitions as $group => $items ) {
107 foreach ( $items as $code => $item ) {
108 if (
109 isset( $item['fix_type'], $item['fix_scope'], $item['fix_pro'] ) &&
110 'auto' === $item['fix_type'] &&
111 'safe' === $item['fix_scope'] &&
112 false === $item['fix_pro']
113 ) {
114 $fixable[ $group . '.' . $code ] = $item;
115 }
116 }
117 }
118
119 return $fixable;
120 }
121
122 public function ajax_apply_auto_fixes() : void {
123 if ( ! current_user_can( 'administrator' ) ) {
124 wp_send_json_error( [ 'message' => 'Permission denied.' ], 403 );
125 }
126
127 check_ajax_referer( 'ariaat_apply_auto_fixes', 'nonce' );
128
129 $setting_keys = isset( $_POST['setting_keys'] ) ? (array) $_POST['setting_keys'] : [];
130 $setting_keys = array_map( 'sanitize_key', $setting_keys );
131 $setting_keys = array_filter( array_unique( $setting_keys ) );
132
133 if ( empty( $setting_keys ) ) {
134 wp_send_json_error( [ 'message' => 'No settings provided.' ], 400 );
135 }
136
137 $allowed_keys = [
138 'focus_outline',
139 'fix_tabindex',
140 'make_viewport_scalable',
141 ];
142
143 $setting_keys = array_values( array_intersect( $setting_keys, $allowed_keys ) );
144
145 if ( empty( $setting_keys ) ) {
146 wp_send_json_error( [ 'message' => 'No valid settings provided.' ], 400 );
147 }
148
149 $settings = get_option( 'ariaat_general_settings', [] );
150
151 if ( ! is_array( $settings ) ) {
152 $settings = [];
153 }
154
155 foreach ( $setting_keys as $setting_key ) {
156 $settings[ $setting_key ] = '1';
157 }
158
159 update_option( 'ariaat_general_settings', $settings );
160
161 wp_send_json_success(
162 [
163 'message' => 'Fix settings saved.',
164 'setting_keys' => $setting_keys,
165 ]
166 );
167 }
168
169 private function get_error_definitions() {
170
171 $aria_url = admin_url('admin.php?page=ariaat&tab=aria');
172 $roles_url = admin_url('admin.php?page=ariaat&tab=roles');
173 $contrast_url = admin_url('admin.php?page=ariaat&tab=contrast');
174 $general_url = admin_url('admin.php?page=ariaat&tab=general');
175 $images_url = admin_url('admin.php?page=ariaat&tab=images');
176
177 $aria_link = sprintf('<a target="_blank" href="%s">ARIA settings</a>', $aria_url);
178 $roles_link = sprintf('<a target="_blank" href="%s">Roles settings</a>', $roles_url );
179 $contrast_link = sprintf('<a target="_blank" href="%s">Contrast settings</a>', $contrast_url );
180 $general_link = sprintf('<a target="_blank" href="%s">General settings</a>', $general_url );
181 $images_link = sprintf('<a target="_blank" href="%s">Images settings</a>', $images_url );
182
183 $errors = [
184 'aria' => [
185 'missing_label' => [
186 'desc' => 'Missing aria-label',
187 'long_desc' => "This can prevent screen readers from interpreting its purpose. Add a meaningful aria-label or use aria-labelledby within $aria_link.",
188 'show' => true,
189 'copy' => true,
190 'fix' => $aria_url,
191 'level' => 'A',
192 'wcag' => '1.3.1',
193 'wcag_label' => 'Info and Relationships',
194 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships.html',
195 'type' => 'error',
196 ],
197 'empty_attribute' => [
198 'desc' => 'Empty ARIA attribute',
199 'long_desc' => "An ARIA attribute was found with an empty value. ARIA attributes must have valid values to assist accessibility. Add a value within $aria_link.",
200 'show' => true,
201 'copy' => true,
202 'fix' => $aria_url,
203 'level' => 'A',
204 'wcag' => '4.1.2',
205 'wcag_label' => 'Name, Role, Value',
206 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html',
207 'type' => 'error',
208 ],
209 'broken_reference' => [
210 'desc' => 'Broken ARIA reference',
211 'long_desc' => "This element references another element via ARIA (e.g., aria-describedby), but the referenced ID was not found in the DOM. Double-check that the target ID exists and is unique.",
212 'show' => true,
213 'copy' => true,
214 'level' => 'A',
215 'wcag' => '4.1.2',
216 'wcag_label' => 'Name, Role, Value',
217 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html',
218 'type' => 'error',
219 ],
220 'empty_button' => [
221 'desc' => 'Empty button with no accessible text',
222 'long_desc' => "Buttons should contain text or an aria-label so screen readers can convey their purpose. Add an aria-label within $aria_link.",
223 'show' => true,
224 'copy' => true,
225 'fix' => $aria_url,
226 'level' => 'A',
227 'wcag' => '4.1.2',
228 'wcag_label' => 'Name, Role, Value',
229 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html',
230 'type' => 'error',
231 ],
232 ],
233 'roles' => [
234 'missing_role' => [
235 'desc' => 'Missing role or label on repeated landmark-type element',
236 'long_desc' => "This element is one of multiple landmark-type elements on the page, such as <code>&lt;section&gt;</code> or <code>&lt;aside&gt;</code>. When multiple instances of these elements are used as landmarks, each should be given a semantic <code>role</code> (e.g. <code>role=\"region\"</code> or <code>role=\"complementary\"</code>) and a descriptive label using <code>aria-label</code> or <code>aria-labelledby</code>. This helps screen reader users distinguish between different regions of the page. Use $roles_link to fix.",
237 'show' => true,
238 'copy' => true,
239 'fix' => $roles_url,
240 'level' => 'A',
241 'wcag' => '1.3.1',
242 'wcag_label' => 'Info and Relationships',
243 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships.html',
244 'type' => 'warning',
245 ],
246 'incorrect_role' => [
247 'desc' => 'Incorrect ARIA role',
248 'long_desc' => "This element has a <code>role</code> that doesn’t match its expected semantic purpose. For example, <code>&lt;main role=\"banner\"&gt;</code> is incorrect. Adjust the role to match the element’s purpose using $roles_link.",
249 'show' => true,
250 'copy' => true,
251 'fix' => $roles_url,
252 'level' => 'A',
253 'wcag' => '1.3.1',
254 'wcag_label' => 'Info and Relationships',
255 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships.html',
256 'type' => 'warning',
257 ],
258 'invalid_role' => [
259 'desc' => 'Invalid ARIA role',
260 'long_desc' => "This element is using a role that does not exist or is misspelled (e.g., <code>role=\"navigationn\"</code>). Roles must match valid ARIA specifications. Fix or remove the invalid role using $roles_link.",
261 'show' => true,
262 'copy' => true,
263 'fix' => $roles_url,
264 'level' => 'A',
265 'wcag' => '4.1.2',
266 'wcag_label' => 'Name, Role, Value',
267 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html',
268 'type' => 'error',
269 ],
270 'redundant_role' => [
271 'desc' => 'Redundant or conflicting ARIA role',
272 'long_desc' => "This element is using a role that is already implied by its HTML tag (e.g., <code>&lt;nav role=\"navigation\"&gt;</code>). Avoid redundant roles unless needed for clarification or assistive technology compatibility. Update or remove via $roles_link.",
273 'show' => true,
274 'copy' => true,
275 'fix' => $roles_url,
276 'level' => 'A',
277 'wcag' => '1.3.1',
278 'wcag_label' => 'Info and Relationships',
279 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships.html',
280 'type' => 'warning',
281 ],
282 ],
283 'contrast' => [
284 'low_contrast' => [
285 'desc' => 'Has low contrast',
286 'long_desc' => "This text has insufficient color contrast against the background. Aim for 4.5:1 or higher. You can manually override text and background colors using $contrast_link.",
287 'show' => true,
288 'copy' => true,
289 'fix' => $contrast_url,
290 'level' => 'AA',
291 'wcag' => '1.4.3',
292 'wcag_label' => 'Contrast (Minimum)',
293 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html',
294 'type' => 'error',
295 ],
296 ],
297 'headings' => [
298 'skipped_level' => [
299 'desc' => 'Skips heading level',
300 'long_desc' => "Heading levels should be used in order without skipping (e.g., h2 → h3, not h2 → h4). Update your theme/template structure accordingly.",
301 'show' => true,
302 'level' => 'A',
303 'wcag' => '2.4.10',
304 'wcag_label' => 'Section Headings',
305 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/section-headings.html',
306 'type' => 'warning',
307 ],
308 ],
309 'media' => [
310 'missing_alt' => [
311 'desc' => 'Missing alt attribute',
312 'long_desc' => "Images must include alt text for screen readers. Use role=\"presentation\" if decorative or add alt tags in $images_link.",
313 'show' => true,
314 'fix' => $images_url,
315 'level' => 'A',
316 'wcag' => '1.1.1',
317 'wcag_label' => 'Non-text Content',
318 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html',
319 'type' => 'error',
320 ],
321 'low_quality_alt' => [
322 'desc' => 'Low-quality alternative text',
323 'long_desc' => "This image uses alt text that contains unnecessary words like 'image', file extensions like '.jpg', or generic descriptors such as 'spacer' or 'arrow' or 'logo'. These terms do not help screen reader users. Rewrite the alt text to be accurate, unique, and meaningful, or leave it blank if the image is decorative. Learn more in the $images_link.",
324 'show' => true,
325 'fix' => $images_url,
326 'level' => 'AA',
327 'type' => 'warning',
328 'wcag' => '1.1.1',
329 'wcag_label' => 'Non-text Content',
330 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html',
331 ],
332 'non_text_fallback' => [
333 'desc' => 'Non-text element has no fallback content',
334 'long_desc' => "This <code>&lt;object&gt;</code>, <code>&lt;embed&gt;</code>, <code>&lt;svg&gt;</code>, or <code>&lt;canvas&gt;</code> element has no fallback text. Non-text content must provide an accessible alternative for screen reader users. This issue can only be fixed by adding the fallback content directly to the item.",
335 'show' => true,
336 'level' => 'A',
337 'type' => 'error',
338 'wcag' => '1.1.1',
339 'wcag_label' => 'Non-text Content',
340 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html',
341 ],
342
343 ],
344 'language' => [
345 'missing_lang' => [
346 'desc' => 'Missing the lang attribute',
347 'long_desc' => "The <html> element should have a lang attribute to help screen readers pronounce content correctly. You can define it in $general_link.",
348 'fix' => $general_url,
349 'level' => 'A',
350 'wcag' => '3.1.1',
351 'wcag_label' => 'Language of Page',
352 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/language-of-page.html',
353 'type' => 'error',
354 ],
355 ],
356 'links' => [
357 'duplicate_text' => [
358 'desc' => 'Duplicate link text with different destinations',
359 'long_desc' => "Ensure each link has a unique and descriptive label. Avoid multiple links that only say “Read more.” Rewrite the anchor text in your content or use aria-labels via $aria_link.",
360 'show' => true,
361 'copy' => true,
362 'fix' => $aria_url,
363 'level' => 'A',
364 'wcag' => '2.4.4',
365 'wcag_label' => 'Link Purpose (In Context)',
366 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/link-purpose-in-context.html',
367 'type' => 'warning',
368 ],
369 'improper_link_use' => [
370 'desc' => 'Link used without href or button role',
371 'long_desc' => "This <code>&lt;a&gt;</code> element is missing an <code>href</code> attribute or only links to <code>#</code>, but does not have <code>role=\"button\"</code>. Links are meant for navigation. If the element triggers an action (like opening a menu or toggling content), use a <code>&lt;button&gt;</code> instead, or add <code>role=\"button\"</code> along with the appropriate ARIA attributes. Fix this in the $roles_link.",
372 'show' => true,
373 'copy' => true,
374 'fix' => $roles_url,
375 'level' => 'A',
376 'type' => 'error',
377 'wcag' => '4.1.2',
378 'wcag_label' => 'Name, Role, Value',
379 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html',
380 ],
381
382 ],
383 'taborder' => [
384 'positive_tabindex' => [
385 'desc' => 'Uses a positive tabindex value',
386 'long_desc' => "Avoid tabindex values greater than 0 as they disrupt natural tab order. Use tabindex=\"0\" or let elements follow the default DOM order. You can fix this in $general_link.",
387 'show' => true,
388 'fix' => $general_url,
389 'fix_type' => 'auto',
390 'fix_scope' => 'safe',
391 'fix_setting_tab' => 'ariaat_general_settings',
392 'fix_setting_key' => 'fix_tabindex',
393 'fix_value' => '1',
394 'fix_label' => 'Enable Fix Tab Order',
395 'fix_pro' => false,
396 'level' => 'A',
397 'wcag' => '2.4.3',
398 'wcag_label' => 'Focus Order',
399 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/focus-order.html',
400 'type' => 'warning',
401 ],
402 ],
403 'keyboard' => [
404 'non_focusable' => [
405 'desc' => 'Not focusable via keyboard',
406 'long_desc' => "This element can’t be accessed using a keyboard (e.g. Tab, Enter, Space). It may be missing necessary HTML attributes or have JavaScript behavior that blocks focus. In some cases, this can be resolved by enabling Fix Tab Order in $general_link",
407 'show' => true,
408 'fix' => $general_url,
409 'level' => 'A',
410 'wcag' => '2.1.1',
411 'wcag_label' => 'Keyboard',
412 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/keyboard.html',
413 'type' => 'error',
414 ],
415 'focus_indicator' => [
416 'desc' => 'No visible focus indicator',
417 'long_desc' => "This element receives focus but has no visible outline or indicator. Ensure interactive elements show a clear visual focus state using <code>:focus</code> or <code>:focus-visible</code> with <code>outline</code> or <code>box-shadow</code>. You can fix this in $general_link using Show Focus Outline setting.",
418 'show' => true,
419 'fix' => $general_url,
420 'fix_type' => 'auto',
421 'fix_scope' => 'safe',
422 'fix_setting_tab' => 'ariaat_general_settings',
423 'fix_setting_key' => 'focus_outline',
424 'fix_value' => '1',
425 'fix_label' => 'Enable Show Focus Outline',
426 'fix_pro' => false,
427 'level' => 'AA',
428 'type' => 'error',
429 'wcag' => '2.4.7',
430 'wcag_label' => 'Focus Visible',
431 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/focus-visible.html',
432 ],
433
434 ],
435 'page' => [
436 'missing_title' => [
437 'desc' => 'Missing a page title',
438 'long_desc' => "Each page should have a unique and descriptive title tag to help users and screen readers understand the content. You can add a dynamic title using WordPress’s wp_title() or SEO plugins. This is something not easily fixable with a plugin like this.",
439 'show' => false,
440 'level' => 'A',
441 'wcag' => '2.4.2',
442 'wcag_label' => 'Page Titled',
443 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/page-titled.html',
444 'type' => 'error',
445 ],
446 ],
447 'dom' => [
448 'duplicate_id' => [
449 'desc' => 'Uses a duplicate ID',
450 'long_desc' => "Each ID in the DOM must be unique. Duplicate IDs can break scripts and confuse screen readers. Update your HTML to ensure each ID is only used once. This is something not easily fixable with a plugin like this.",
451 'show' => true,
452 'level' => 'A',
453 'wcag' => '4.1.1',
454 'wcag_label' => 'Parsing',
455 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/parsing.html',
456 'type' => 'error',
457 ],
458 ],
459 'viewport' => [
460 'not_scalable' => [
461 'desc' => 'Viewport not scalable',
462 'long_desc' => "The viewport meta tag contains <code>user-scalable=no</code>, which prevents users from zooming. This can impact accessibility for users who rely on pinch-to-zoom. You can fix this in $general_link",
463 'fix' => $general_url,
464 'fix_type' => 'auto',
465 'fix_scope' => 'safe',
466 'fix_setting_tab' => 'ariaat_general_settings',
467 'fix_setting_key' => 'make_viewport_scalable',
468 'fix_value' => '1',
469 'fix_label' => 'Enable Make viewport not scalable',
470 'fix_pro' => false,
471 'level' => 'AA',
472 'wcag' => '1.4.4',
473 'wcag_label' => 'Resize Text',
474 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/resize-text.html',
475 'type' => 'warning',
476 ],
477 ],
478 'navigation' => [
479 'missing_skip_link' => [
480 'desc' => 'No “Skip to content” link found',
481 'long_desc' => "This page does not include a <code>skip to content</code> link. These links help keyboard and screen reader users bypass repetitive navigation and go straight to the main content. It should appear near the top of the page and link to an element like <code>#main</code> or <code>#content</code>. You can enable this feature in the $general_link.",
482 'show' => true,
483 'fix' => $general_url,
484 'level' => 'A',
485 'type' => 'error',
486 'wcag' => '2.4.1',
487 'wcag_label' => 'Bypass Blocks',
488 'wcag_url' => 'https://www.w3.org/WAI/WCAG21/Understanding/bypass-blocks.html',
489 ],
490
491 ],
492
493 ];
494
495 return apply_filters( 'ariaat_error_definitions', $errors );
496 }
497 }
498
499 new ARIAAT_Frontend_Checker();