From fc27d9e6db61acd31a43c957493dcd2d35db6492 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Thu, 28 May 2026 18:00:19 -0400 Subject: [PATCH 1/3] POC: add per-rule exclusions config to keep WP-specific selectors out of generic JS checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an `exclusions` key to the PHP rule config (AriaHiddenRule as the first consumer). These selectors are collected at enqueue time and passed through the existing scanOptions pipeline (PHP → wp_localize_script → iframe window) to the JS check, which applies them via node.matches() before any other logic runs. Removes the hardcoded wp-block-spacer class check from aria-hidden-valid-usage.js and replaces it with the new config-driven approach alongside the two new WP core block overlay patterns from issue #567. Co-Authored-By: Claude Sonnet 4.6 --- admin/class-enqueue-admin.php | 40 +++++++++++++------ .../classes/Rules/Rule/AriaHiddenRule.php | 5 +++ src/editorApp/checkPage.js | 5 +-- .../checks/aria-hidden-valid-usage.js | 11 +++-- src/pageScanner/config/rules.js | 7 +++- 5 files changed, 48 insertions(+), 20 deletions(-) diff --git a/admin/class-enqueue-admin.php b/admin/class-enqueue-admin.php index 137374432..79dab8d77 100644 --- a/admin/class-enqueue-admin.php +++ b/admin/class-enqueue-admin.php @@ -133,18 +133,19 @@ public static function maybe_enqueue_admin_and_editor_app_scripts() { 'edac-editor-app', 'edac_editor_app', [ - 'postID' => $post_id, - 'edacUrl' => esc_url_raw( get_site_url() ), - 'edacApiUrl' => esc_url_raw( rest_url() . 'accessibility-checker/v1' ), - 'baseurl' => plugin_dir_url( __DIR__ ), - 'active' => $active, - 'pro' => $pro, - 'debug' => $debug, - 'scanUrl' => $scan_url, - 'maxAltLength' => max( 1, absint( apply_filters( 'edac_max_alt_length', 300 ) ) ), - 'version' => EDAC_VERSION, - 'postStatus' => get_post_status( $post_id ), - 'restNonce' => wp_create_nonce( 'wp_rest' ), + 'postID' => $post_id, + 'edacUrl' => esc_url_raw( get_site_url() ), + 'edacApiUrl' => esc_url_raw( rest_url() . 'accessibility-checker/v1' ), + 'baseurl' => plugin_dir_url( __DIR__ ), + 'active' => $active, + 'pro' => $pro, + 'debug' => $debug, + 'scanUrl' => $scan_url, + 'maxAltLength' => max( 1, absint( apply_filters( 'edac_max_alt_length', 300 ) ) ), + 'ruleExclusions' => self::get_rule_exclusions(), + 'version' => EDAC_VERSION, + 'postStatus' => get_post_status( $post_id ), + 'restNonce' => wp_create_nonce( 'wp_rest' ), ] ); @@ -448,4 +449,19 @@ private static function get_sr_only_editor_styles(): string { private static function get_current_page_slug(): ?string { return isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only. } + + /** + * Build a map of rule slug => exclusion selectors for rules that declare exclusions. + * + * @return array + */ + private static function get_rule_exclusions(): array { + $exclusions = []; + foreach ( edac_register_rules() as $rule ) { + if ( ! empty( $rule['exclusions'] ) ) { + $exclusions[ $rule['slug'] ] = $rule['exclusions']; + } + } + return $exclusions; + } } diff --git a/includes/classes/Rules/Rule/AriaHiddenRule.php b/includes/classes/Rules/Rule/AriaHiddenRule.php index 4a6f310db..7a8a6965b 100644 --- a/includes/classes/Rules/Rule/AriaHiddenRule.php +++ b/includes/classes/Rules/Rule/AriaHiddenRule.php @@ -62,6 +62,11 @@ public static function get_rule(): array { 'combines' => [ 'aria_hidden_validation', ], + 'exclusions' => [ + '.wp-block-spacer', + '.wp-block-cover__background.has-background-dim', + '.wp-block-post-featured-image__overlay.has-background-dim', + ], ]; } } diff --git a/src/editorApp/checkPage.js b/src/editorApp/checkPage.js index c6c59a0c1..527f66002 100644 --- a/src/editorApp/checkPage.js +++ b/src/editorApp/checkPage.js @@ -106,15 +106,14 @@ const injectIframe = ( previewUrl, postID ) => { body.setAttribute( 'data-iframe-post-id', postID ); if ( iframeDocument ) { - if ( window?.edac_editor_app?.maxAltLength ) { - // if the frame doesn't have window.scanOptions then create is as an object. + if ( window?.edac_editor_app?.maxAltLength || window?.edac_editor_app?.ruleExclusions ) { if ( ! iframeDocument.defaultView.scanOptions ) { iframeDocument.defaultView.scanOptions = {}; } - // set the maxAlthLength for the scanOptions. iframeDocument.defaultView.scanOptions = { maxAltLength: window.edac_editor_app.maxAltLength, + ruleExclusions: window.edac_editor_app.ruleExclusions, }; } diff --git a/src/pageScanner/checks/aria-hidden-valid-usage.js b/src/pageScanner/checks/aria-hidden-valid-usage.js index b37446581..68ea8764c 100644 --- a/src/pageScanner/checks/aria-hidden-valid-usage.js +++ b/src/pageScanner/checks/aria-hidden-valid-usage.js @@ -14,16 +14,19 @@ const srClasses = [ export default { id: 'aria_hidden_valid_usage', - evaluate: ( node ) => { + evaluate: ( node, options = {} ) => { // Check if element is hidden with CSS const computedStyle = window.getComputedStyle( node ); if ( computedStyle.display === 'none' || computedStyle.visibility === 'hidden' ) { return true; } - // Check for valid element properties - if ( node.classList.contains( 'wp-block-spacer' ) ) { - return true; + // Check against configured exclusion selectors. + const exclusions = options?.exclusions || []; + for ( const selector of exclusions ) { + if ( node.matches( selector ) ) { + return true; + } } const role = node.getAttribute( 'role' ); diff --git a/src/pageScanner/config/rules.js b/src/pageScanner/config/rules.js index 705d0588b..2c8726a2d 100644 --- a/src/pageScanner/config/rules.js +++ b/src/pageScanner/config/rules.js @@ -119,7 +119,12 @@ export const checksArray = [ hasAmbiguousText, anchorExists, imageInputHasAlt, - ariaHiddenValidUsage, + { + ...ariaHiddenValidUsage, + options: { + exclusions: window?.scanOptions?.ruleExclusions?.aria_hidden || [], + }, + }, tableHasHeaders, headingIsEmpty, transcriptMissing, From 0db8ad0de87760983cf646c6ea02d675d00f4e0d Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Mon, 1 Jun 2026 20:43:05 -0400 Subject: [PATCH 2/3] Address code review feedback on rule exclusions POC - aria-hidden-valid-usage.js: use Array.isArray() guard and wrap node.matches() in try/catch so malformed selectors can't crash the scan - checkPage.js: spread existing scanOptions rather than replacing the object wholesale, preserving any properties set by other code - class-enqueue-admin.php: add is_array() defensive check in get_rule_exclusions(), make the method public so it can be reused by the frontend enqueue class - class-enqueue-frontend.php: pass ruleExclusions to edacFrontendHighlighterApp so frontend highlighter scans receive the same exclusions as editor iframe scans - frontendHighlighterApp/index.js: populate window.scanOptions.ruleExclusions from the localized data before the scan runs, fixing a regression where removing the hardcoded wp-block-spacer check would have caused false positives on frontend rescans Co-Authored-By: Claude Sonnet 4.6 --- admin/class-enqueue-admin.php | 4 ++-- includes/classes/class-enqueue-frontend.php | 1 + src/editorApp/checkPage.js | 1 + src/frontendHighlighterApp/index.js | 8 ++++++++ src/pageScanner/checks/aria-hidden-valid-usage.js | 10 +++++++--- 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/admin/class-enqueue-admin.php b/admin/class-enqueue-admin.php index 79dab8d77..ee732bfa4 100644 --- a/admin/class-enqueue-admin.php +++ b/admin/class-enqueue-admin.php @@ -455,10 +455,10 @@ private static function get_current_page_slug(): ?string { * * @return array */ - private static function get_rule_exclusions(): array { + public static function get_rule_exclusions(): array { $exclusions = []; foreach ( edac_register_rules() as $rule ) { - if ( ! empty( $rule['exclusions'] ) ) { + if ( ! empty( $rule['slug'] ) && ! empty( $rule['exclusions'] ) && is_array( $rule['exclusions'] ) ) { $exclusions[ $rule['slug'] ] = $rule['exclusions']; } } diff --git a/includes/classes/class-enqueue-frontend.php b/includes/classes/class-enqueue-frontend.php index 64f349526..d61735a02 100644 --- a/includes/classes/class-enqueue-frontend.php +++ b/includes/classes/class-enqueue-frontend.php @@ -163,6 +163,7 @@ public static function maybe_enqueue_frontend_highlighter() { 'editorLink' => get_edit_post_link( $post_id ), 'scannerBundleUrl' => esc_url_raw( add_query_arg( 'ver', EDAC_VERSION, plugin_dir_url( EDAC_PLUGIN_FILE ) . 'build/pageScanner.bundle.js' ) ), 'adminThemeColor' => self::get_admin_theme_color(), + 'ruleExclusions' => \EDAC\Admin\Enqueue_Admin::get_rule_exclusions(), ] ); diff --git a/src/editorApp/checkPage.js b/src/editorApp/checkPage.js index 527f66002..38848bbd6 100644 --- a/src/editorApp/checkPage.js +++ b/src/editorApp/checkPage.js @@ -112,6 +112,7 @@ const injectIframe = ( previewUrl, postID ) => { } iframeDocument.defaultView.scanOptions = { + ...iframeDocument.defaultView.scanOptions, maxAltLength: window.edac_editor_app.maxAltLength, ruleExclusions: window.edac_editor_app.ruleExclusions, }; diff --git a/src/frontendHighlighterApp/index.js b/src/frontendHighlighterApp/index.js index 8a091c3e6..c113f7efd 100644 --- a/src/frontendHighlighterApp/index.js +++ b/src/frontendHighlighterApp/index.js @@ -1779,6 +1779,14 @@ class AccessibilityCheckerHighlight { const scriptId = 'edac-accessibility-checker-scanner-script'; return new Promise( ( resolve, reject ) => { + // Ensure rule exclusions from the localized config are available to the scanner. + if ( window.edacFrontendHighlighterApp?.ruleExclusions ) { + window.scanOptions = { + ...window.scanOptions, + ruleExclusions: window.edacFrontendHighlighterApp.ruleExclusions, + }; + } + const runScan = () => { self._runScanOrShowError( densityMetrics ) .then( resolve ) diff --git a/src/pageScanner/checks/aria-hidden-valid-usage.js b/src/pageScanner/checks/aria-hidden-valid-usage.js index 68ea8764c..c9606d6c1 100644 --- a/src/pageScanner/checks/aria-hidden-valid-usage.js +++ b/src/pageScanner/checks/aria-hidden-valid-usage.js @@ -22,10 +22,14 @@ export default { } // Check against configured exclusion selectors. - const exclusions = options?.exclusions || []; + const exclusions = Array.isArray( options?.exclusions ) ? options.exclusions : []; for ( const selector of exclusions ) { - if ( node.matches( selector ) ) { - return true; + try { + if ( node.matches( selector ) ) { + return true; + } + } catch ( e ) { + // Skip malformed selectors so the scan remains stable. } } From bfe48aef20500a911fdc664ac039b2b8469ffc28 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Mon, 1 Jun 2026 20:46:21 -0400 Subject: [PATCH 3/3] Fix failing Jest test and add coverage for new overlay exclusions The ariaHiddenValid test registered the check without options, so the wp-block-spacer test case started failing once the hardcoded class check was removed. Update beforeAll to pass the WordPress exclusions via check options, mirroring exactly what config/rules.js does at runtime via window.scanOptions. Also adds test cases for the two new WP core overlay selectors introduced in this POC (cover block background dim and post featured image overlay). Co-Authored-By: Claude Sonnet 4.6 --- tests/jest/rules/ariaHiddenValid.test.js | 27 ++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/jest/rules/ariaHiddenValid.test.js b/tests/jest/rules/ariaHiddenValid.test.js index d2008adae..f77fd1468 100644 --- a/tests/jest/rules/ariaHiddenValid.test.js +++ b/tests/jest/rules/ariaHiddenValid.test.js @@ -9,10 +9,23 @@ beforeAll( async () => { const ariaHiddenRule = ariaHiddenRuleModule.default; const ariaHiddenCheck = ariaHiddenCheckModule.default; - // Configure axe with the imported rules + // Configure axe with the imported rules. + // Pass the WordPress-specific exclusions the same way config/rules.js does + // at runtime via window.scanOptions, so tests reflect production behaviour. axe.configure( { rules: [ ariaHiddenRule ], - checks: [ ariaHiddenCheck ], + checks: [ + { + ...ariaHiddenCheck, + options: { + exclusions: [ + '.wp-block-spacer', + '.wp-block-cover__background.has-background-dim', + '.wp-block-post-featured-image__overlay.has-background-dim', + ], + }, + }, + ], } ); } ); @@ -74,6 +87,16 @@ describe( 'Aria Hidden Validation', () => { html: '', shouldPass: true, }, + { + name: 'should pass for cover block background dim overlay', + html: '', + shouldPass: true, + }, + { + name: 'should pass for post featured image overlay with dim', + html: '', + shouldPass: true, + }, { name: 'should pass for element with role="presentation"', html: '',