Make WordPress Core

Changeset 61469


Ignore:
Timestamp:
01/11/2026 06:34:57 AM (7 months ago)
Author:
westonruter
Message:

Themes: Use WP_HTML_Tag_Processor to insert the block template skip link instead of JavaScript.

  • The skip link now works when JavaScript is turned off.
  • By removing the script, the amount of JavaScript sent to the client is reduced for a very marginal performance improvement.
  • A new wp-block-template-skip-link stylesheet is registered, with minification and path data for inlining.
  • The CSS for the skip link now has an RTL version generated, although it is not yet served when the styles are inlined. See #61625.
  • The wp_enqueue_block_template_skip_link() function now exclusively enqueues the stylesheet since the script is removed.
  • For backwards-compatibility, the skip link will continue to be omitted if the_block_template_skip_link() is unhooked from the wp_footer action or wp_enqueue_block_template_skip_link() is unhooked from wp_enqueue_scripts.

Developed in https://github.com/WordPress/wordpress-develop/pull/10676

Follow-up to [56932], [51003].

Props rutviksavsani, westonruter, dmsnell, whiteshadow01, Slieptsov.
See #59505, #53176.
Fixes #64361.

Location:
trunk
Files:
1 added
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/block-template.php

    r61431 r61469  
    302302        // Wrap block template in .wp-site-blocks to allow for specific descendant styles
    303303        // (e.g. `.wp-site-blocks > *`).
    304         return '<div class="wp-site-blocks">' . $content . '</div>';
     304        $template_html = '<div class="wp-site-blocks">' . $content . '</div>';
     305
     306        // Back-compat for plugins that disable functionality by unhooking one of these actions.
     307        if (
     308                ! has_action( 'wp_footer', 'the_block_template_skip_link' ) ||
     309                ! has_action( 'wp_enqueue_scripts', 'wp_enqueue_block_template_skip_link' )
     310        ) {
     311                return $template_html;
     312        }
     313
     314        return _block_template_add_skip_link( $template_html );
     315}
     316
     317/**
     318 * Inserts the block template skip-link into the template HTML.
     319 *
     320 * When a `MAIN` element exists in the template, this function will ensure
     321 * that the element contains an `id` attribute, and it will insert a link to
     322 * that `MAIN` element before the first `DIV.wp-site-blocks` element, which
     323 * is the wrapper for all blocks in a block template as constructed by
     324 * {@see get_the_block_template_html()}.
     325 *
     326 * Example:
     327 *
     328 *     // Input.
     329 *     <div class="wp-site-blocks">
     330 *         <nav>...</nav>
     331 *         <main>
     332 *             <h2>...
     333 *
     334 *     // Output.
     335 *     <a href="#wp--skip-link--target" id="wp-skip-link" class="...">
     336 *     <div class="wp-site-blocks">
     337 *         <nav>...</nav>
     338 *         <main id="wp--skip-link--target">
     339 *             <h2>...
     340 *
     341 * When the `MAIN` element already contains a non-empty `id` value it will be
     342 * used instead of the default skip-link id.
     343 *
     344 * @access private
     345 * @since 7.0.0
     346 *
     347 * @param string $template_html Block template markup.
     348 * @return string Modified markup with skip link when applicable.
     349 */
     350function _block_template_add_skip_link( string $template_html ): string {
     351        // Anonymous subclass of WP_HTML_Tag_Processor to access protected bookmark spans.
     352        $processor = new class( $template_html ) extends WP_HTML_Tag_Processor {
     353                /**
     354                 * Inserts text before the current token.
     355                 *
     356                 * @param string $text Text to insert.
     357                 */
     358                public function insert_before( string $text ) {
     359                        $this->set_bookmark( 'here' );
     360                        $this->lexical_updates[] = new WP_HTML_Text_Replacement( $this->bookmarks['here']->start, 0, $text );
     361                }
     362        };
     363
     364        // Find and bookmark the first DIV.wp-site-blocks.
     365        if (
     366                ! $processor->next_tag(
     367                        array(
     368                                'tag_name'   => 'DIV',
     369                                'class_name' => 'wp-site-blocks',
     370                        )
     371                )
     372        ) {
     373                return $template_html;
     374        }
     375        $processor->set_bookmark( 'skip_link_insertion_point' );
     376
     377        // Ensure the MAIN element has an ID.
     378        if ( ! $processor->next_tag( 'MAIN' ) ) {
     379                return $template_html;
     380        }
     381
     382        $skip_link_target_id = $processor->get_attribute( 'id' );
     383        if ( ! is_string( $skip_link_target_id ) || '' === $skip_link_target_id ) {
     384                $skip_link_target_id = 'wp--skip-link--target';
     385                $processor->set_attribute( 'id', $skip_link_target_id );
     386        }
     387
     388        // Seek back to the bookmarked insertion point.
     389        $processor->seek( 'skip_link_insertion_point' );
     390
     391        $skip_link = sprintf(
     392                '<a class="skip-link screen-reader-text" id="wp-skip-link" href="%s">%s</a>',
     393                esc_url( '#' . $skip_link_target_id ),
     394                /* translators: Hidden accessibility text. */
     395                esc_html__( 'Skip to content' )
     396        );
     397        $processor->insert_before( $skip_link );
     398
     399        return $processor->get_updated_html();
    305400}
    306401
  • trunk/src/wp-includes/script-loader.php

    r61442 r61469  
    16061606        $styles->add( 'customize-preview', "/wp-includes/css/customize-preview$suffix.css", array( 'dashicons' ) );
    16071607        $styles->add( 'wp-empty-template-alert', "/wp-includes/css/wp-empty-template-alert$suffix.css" );
     1608        $skip_link_style_path = WPINC . "/css/wp-block-template-skip-link$suffix.css";
     1609        $styles->add( 'wp-block-template-skip-link', "/$skip_link_style_path" );
     1610        $styles->add_data( 'wp-block-template-skip-link', 'path', ABSPATH . $skip_link_style_path );
    16081611
    16091612        // External libraries and friends.
     
    18011804                'wp-pointer',
    18021805                'wp-jquery-ui-dialog',
     1806                'wp-block-template-skip-link',
    18031807                // Package styles.
    18041808                'wp-reset-editor-styles',
  • trunk/src/wp-includes/theme-templates.php

    r61178 r61469  
    100100
    101101/**
    102  * Enqueues the skip-link script & styles.
     102 * Enqueues the skip-link styles.
    103103 *
    104104 * @access private
    105105 * @since 6.4.0
     106 * @since 7.0.0 A script is no longer printed in favor of being added via {@see _block_template_add_skip_link()}.
    106107 *
    107108 * @global string $_wp_current_template_content
     
    126127        }
    127128
    128         $skip_link_styles = '
    129                 .skip-link.screen-reader-text {
    130                         border: 0;
    131                         clip-path: inset(50%);
    132                         height: 1px;
    133                         margin: -1px;
    134                         overflow: hidden;
    135                         padding: 0;
    136                         position: absolute !important;
    137                         width: 1px;
    138                         word-wrap: normal !important;
    139                 }
    140 
    141                 .skip-link.screen-reader-text:focus {
    142                         background-color: #eee;
    143                         clip-path: none;
    144                         color: #444;
    145                         display: block;
    146                         font-size: 1em;
    147                         height: auto;
    148                         left: 5px;
    149                         line-height: normal;
    150                         padding: 15px 23px 14px;
    151                         text-decoration: none;
    152                         top: 5px;
    153                         width: auto;
    154                         z-index: 100000;
    155                 }';
    156 
    157         $handle = 'wp-block-template-skip-link';
    158 
    159         /**
    160          * Print the skip-link styles.
    161          */
    162         wp_register_style( $handle, false );
    163         wp_add_inline_style( $handle, $skip_link_styles );
    164         wp_enqueue_style( $handle );
    165 
    166         /**
    167          * Enqueue the skip-link script.
    168          */
    169         ob_start();
    170         ?>
    171         <script>
    172         ( function() {
    173                 var skipLinkTarget = document.querySelector( 'main' ),
    174                         sibling,
    175                         skipLinkTargetID,
    176                         skipLink;
    177 
    178                 // Early exit if a skip-link target can't be located.
    179                 if ( ! skipLinkTarget ) {
    180                         return;
    181                 }
    182 
    183                 /*
    184                  * Get the site wrapper.
    185                  * The skip-link will be injected in the beginning of it.
    186                  */
    187                 sibling = document.querySelector( '.wp-site-blocks' );
    188 
    189                 // Early exit if the root element was not found.
    190                 if ( ! sibling ) {
    191                         return;
    192                 }
    193 
    194                 // Get the skip-link target's ID, and generate one if it doesn't exist.
    195                 skipLinkTargetID = skipLinkTarget.id;
    196                 if ( ! skipLinkTargetID ) {
    197                         skipLinkTargetID = 'wp--skip-link--target';
    198                         skipLinkTarget.id = skipLinkTargetID;
    199                 }
    200 
    201                 // Create the skip link.
    202                 skipLink = document.createElement( 'a' );
    203                 skipLink.classList.add( 'skip-link', 'screen-reader-text' );
    204                 skipLink.id = 'wp-skip-link';
    205                 skipLink.href = '#' + skipLinkTargetID;
    206                 skipLink.innerText = '<?php /* translators: Hidden accessibility text. Do not use HTML entities (&nbsp;, etc.). */ esc_html_e( 'Skip to content' ); ?>';
    207 
    208                 // Inject the skip link.
    209                 sibling.parentElement.insertBefore( skipLink, sibling );
    210         }() );
    211         </script>
    212         <?php
    213         $skip_link_script = wp_remove_surrounding_empty_script_tags( ob_get_clean() );
    214         $script_handle    = 'wp-block-template-skip-link';
    215         wp_register_script( $script_handle, false, array(), false, array( 'in_footer' => true ) );
    216         wp_add_inline_script( $script_handle, $skip_link_script );
    217         wp_enqueue_script( $script_handle );
     129        wp_enqueue_style( 'wp-block-template-skip-link' );
    218130}
    219131
  • trunk/tests/phpunit/tests/block-template-utils.php

    r58235 r61469  
    299299
    300300        /**
     301         * Tests that a skip link is added and a MAIN element without an ID receives the default ID.
     302         *
     303         * @ticket 64361
     304         *
     305         * @covers ::_block_template_add_skip_link
     306         */
     307        public function test_block_template_add_skip_link_inserts_link_and_adds_main_id_when_missing() {
     308                $template_html = '<div class="wp-site-blocks"><main>Content</main></div>';
     309                $expected      = '
     310                        <a class="skip-link screen-reader-text" id="wp-skip-link" href="#wp--skip-link--target">Skip to content</a>
     311                        <div class="wp-site-blocks"><main id="wp--skip-link--target">Content</main></div>
     312                ';
     313
     314                $this->assertEqualHTML( $expected, _block_template_add_skip_link( $template_html ) );
     315        }
     316
     317        /**
     318         * Tests that an existing MAIN ID is reused for the skip link.
     319         *
     320         * @ticket 64361
     321         *
     322         * @covers ::_block_template_add_skip_link
     323         */
     324        public function test_block_template_add_skip_link_uses_existing_main_id() {
     325                $template_html = '<div class="wp-site-blocks"><main id="custom-id">Content</main></div>';
     326                $expected      = '
     327                        <a class="skip-link screen-reader-text" id="wp-skip-link" href="#custom-id">Skip to content</a>
     328                        <div class="wp-site-blocks"><main id="custom-id">Content</main></div>
     329                ';
     330
     331                $this->assertEqualHTML( $expected, _block_template_add_skip_link( $template_html ) );
     332        }
     333
     334        /**
     335         * Tests that a boolean MAIN ID is treated as missing and replaced with the default.
     336         *
     337         * @ticket 64361
     338         *
     339         * @covers ::_block_template_add_skip_link
     340         */
     341        public function test_block_template_add_skip_link_handles_boolean_main_id() {
     342                $template_html = '<div class="wp-site-blocks"><main id>Content</main></div>';
     343                $expected      = '
     344                        <a class="skip-link screen-reader-text" id="wp-skip-link" href="#wp--skip-link--target">Skip to content</a>
     345                        <div class="wp-site-blocks"><main id="wp--skip-link--target">Content</main></div>
     346                ';
     347
     348                $this->assertEqualHTML( $expected, _block_template_add_skip_link( $template_html ) );
     349        }
     350
     351        /**
     352         * Tests that a MAIN ID containing whitespace is preserved and used for the skip link.
     353         *
     354         * @ticket 64361
     355         *
     356         * @covers ::_block_template_add_skip_link
     357         */
     358        public function test_block_template_add_skip_link_preserves_whitespace_main_id() {
     359                $template_html = '<div class="wp-site-blocks"><main id=" my-id ">Content</main></div>';
     360                $expected      = '
     361                        <a class="skip-link screen-reader-text" id="wp-skip-link" href="#%20my-id%20">Skip to content</a>
     362                        <div class="wp-site-blocks"><main id=" my-id ">Content</main></div>
     363                ';
     364
     365                $this->assertEqualHTML( $expected, _block_template_add_skip_link( $template_html ) );
     366        }
     367
     368        /**
     369         * Tests that no changes are made when there is no MAIN element.
     370         *
     371         * @ticket 64361
     372         *
     373         * @covers ::_block_template_add_skip_link
     374         */
     375        public function test_block_template_add_skip_link_does_not_modify_when_main_missing() {
     376                $template_html = '<div class="wp-site-blocks"><div>Content</div></div>';
     377
     378                $this->assertSame( $template_html, _block_template_add_skip_link( $template_html ) );
     379        }
     380
     381        /**
    301382         * Should retrieve the template from the theme files.
    302383         */
  • trunk/tests/phpunit/tests/block-template.php

    r61090 r61469  
    313313
    314314        /**
     315         * Tests that `get_the_block_template_html()` adds a skip link when a MAIN element is present.
     316         *
     317         * @ticket 64361
     318         * @covers ::get_the_block_template_html
     319         */
     320        public function test_get_the_block_template_html_adds_skip_link_when_main_present() {
     321                global $_wp_current_template_id, $_wp_current_template_content;
     322
     323                $_wp_current_template_id      = get_stylesheet() . '//index';
     324                $_wp_current_template_content = '<main>Content</main>';
     325
     326                $processor = new WP_HTML_Tag_Processor( get_the_block_template_html() );
     327                $this->assertTrue(
     328                        $processor->next_tag(
     329                                array(
     330                                        'tag_name'   => 'A',
     331                                        'class_name' => 'skip-link',
     332                                )
     333                        ),
     334                        'Expected skip link was not added to the block template HTML.'
     335                );
     336                $this->assertSame( 'wp-skip-link', $processor->get_attribute( 'id' ), 'Unexpected ID on skip link.' );
     337                $this->assertTrue( $processor->has_class( 'screen-reader-text' ), 'Expected "screen-reader-text" class on skip link.' );
     338        }
     339
     340        /**
     341         * Tests that `get_the_block_template_html()` does not add a skip link when the skip-link action is unhooked.
     342         *
     343         * @ticket 64361
     344         * @covers ::get_the_block_template_html
     345         *
     346         * @dataProvider data_provider_skip_link_actions
     347         */
     348        public function test_get_the_block_template_html_does_not_add_skip_link_when_action_unhooked( string $action, string $callback ) {
     349                global $_wp_current_template_id, $_wp_current_template_content;
     350
     351                $_wp_current_template_id      = get_stylesheet() . '//index';
     352                $_wp_current_template_content = '<main>Content</main>';
     353
     354                remove_action( $action, $callback );
     355
     356                $processor = new WP_HTML_Tag_Processor( get_the_block_template_html() );
     357                $this->assertFalse(
     358                        $processor->next_tag(
     359                                array(
     360                                        'tag_name'   => 'A',
     361                                        'class_name' => 'skip-link',
     362                                )
     363                        ),
     364                        'Unexpected skip link was added to the block template HTML when the action was unhooked.'
     365                );
     366        }
     367
     368        /**
     369         * Data provider for test_get_the_block_template_html_does_not_add_skip_link_when_action_unhooked.
     370         *
     371         * @return array<string, array<string, string>>
     372         */
     373        public function data_provider_skip_link_actions(): array {
     374                return array(
     375                        'the_block_template_skip_link'        => array(
     376                                'action'   => 'wp_footer',
     377                                'callback' => 'the_block_template_skip_link',
     378                        ),
     379                        'wp_enqueue_block_template_skip_link' => array(
     380                                'action'   => 'wp_enqueue_scripts',
     381                                'callback' => 'wp_enqueue_block_template_skip_link',
     382                        ),
     383                );
     384        }
     385
     386        /**
    315387         * @ticket 58319
    316388         *
  • trunk/tests/phpunit/tests/template.php

    r61416 r61469  
    17981798                        'core-block-supports-duotone-inline-css',
    17991799                        'wp-block-library-theme-css',
     1800                        'wp-block-template-skip-link-css',
    18001801                        'wp-block-template-skip-link-inline-css',
    18011802                );
Note: See TracChangeset for help on using the changeset viewer.