Skip to content

Commit 6bdb78e

Browse files
committed
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 WordPress#10676 Follow-up to [56932], [51003]. Props rutviksavsani, westonruter, dmsnell, whiteshadow01, Slieptsov. See #59505, #53176. Fixes #64361. git-svn-id: https://develop.svn.wordpress.org/trunk@61469 602fd350-edb4-49c9-b593-d223f7449a82
1 parent ffef795 commit 6bdb78e

7 files changed

Lines changed: 284 additions & 92 deletions

File tree

src/wp-includes/block-template.php

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,102 @@ function get_the_block_template_html() {
301301

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+
*/
350+
function _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

307402
/**
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.skip-link.screen-reader-text {
2+
border: 0;
3+
clip-path: inset(50%);
4+
height: 1px;
5+
margin: -1px;
6+
overflow: hidden;
7+
padding: 0;
8+
position: absolute !important;
9+
width: 1px;
10+
word-wrap: normal !important;
11+
}
12+
13+
.skip-link.screen-reader-text:focus {
14+
background-color: #eee;
15+
clip-path: none;
16+
color: #444;
17+
display: block;
18+
font-size: 1em;
19+
height: auto;
20+
left: 5px;
21+
line-height: normal;
22+
padding: 15px 23px 14px;
23+
text-decoration: none;
24+
top: 5px;
25+
width: auto;
26+
z-index: 100000;
27+
}

src/wp-includes/script-loader.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1605,6 +1605,9 @@ function wp_default_styles( $styles ) {
16051605
$styles->add( 'wp-pointer', "/wp-includes/css/wp-pointer$suffix.css", array( 'dashicons' ) );
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.
16101613
$styles->add( 'imgareaselect', '/wp-includes/js/imgareaselect/imgareaselect.css', array(), '0.9.8' );
@@ -1800,6 +1803,7 @@ function wp_default_styles( $styles ) {
18001803
'media-views',
18011804
'wp-pointer',
18021805
'wp-jquery-ui-dialog',
1806+
'wp-block-template-skip-link',
18031807
// Package styles.
18041808
'wp-reset-editor-styles',
18051809
'wp-editor-classic-layout-styles',

src/wp-includes/theme-templates.php

Lines changed: 3 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,11 @@ function wp_filter_wp_template_unique_post_slug( $override_slug, $slug, $post_id
9999
}
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
108109
*/
@@ -125,96 +126,7 @@ function wp_enqueue_block_template_skip_link() {
125126
return;
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

220132
/**

tests/phpunit/tests/block-template-utils.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,87 @@ public function data_remove_theme_attribute_in_block_template_content() {
297297
);
298298
}
299299

300+
/**
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+
300381
/**
301382
* Should retrieve the template from the theme files.
302383
*/

0 commit comments

Comments
 (0)