Make WordPress Core

Changeset 61469


Ignore:
Timestamp:
01/11/2026 06:34:57 AM (5 weeks 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.