diff --git a/admin/class-enqueue-admin.php b/admin/class-enqueue-admin.php index 137374432..ee732bfa4 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 + */ + public static function get_rule_exclusions(): array { + $exclusions = []; + foreach ( edac_register_rules() as $rule ) { + if ( ! empty( $rule['slug'] ) && ! empty( $rule['exclusions'] ) && is_array( $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/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 c6c59a0c1..38848bbd6 100644 --- a/src/editorApp/checkPage.js +++ b/src/editorApp/checkPage.js @@ -106,15 +106,15 @@ 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 = { + ...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 b37446581..c9606d6c1 100644 --- a/src/pageScanner/checks/aria-hidden-valid-usage.js +++ b/src/pageScanner/checks/aria-hidden-valid-usage.js @@ -14,16 +14,23 @@ 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 = Array.isArray( options?.exclusions ) ? options.exclusions : []; + for ( const selector of exclusions ) { + try { + if ( node.matches( selector ) ) { + return true; + } + } catch ( e ) { + // Skip malformed selectors so the scan remains stable. + } } 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, 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: '',