diff --git a/src/wp-includes/block-template.php b/src/wp-includes/block-template.php
index df3735b238e4f..7b09c3fe77d10 100644
--- a/src/wp-includes/block-template.php
+++ b/src/wp-includes/block-template.php
@@ -301,7 +301,78 @@ function get_the_block_template_html() {
// Wrap block template in .wp-site-blocks to allow for specific descendant styles
// (e.g. `.wp-site-blocks > *`).
- return '
' . $content . '
';
+ $template_html = '' . $content . '
';
+
+ return _block_template_skip_link_markup( $template_html );
+}
+
+/**
+ * Inserts the block template skip link into the template HTML.
+ *
+ * Uses the HTML API to ensure that the main content element has an ID and to
+ * inject the skip-link anchor before the block template wrapper.
+ *
+ * @access private
+ * @since 7.0.0
+ *
+ * @param string $template_html Block template markup.
+ * @return string Modified markup with skip link when applicable.
+ */
+function _block_template_skip_link_markup( string $template_html ): string {
+
+ // Back-compat for plugins that disable functionality by unhooking this action.
+ if ( ! has_action( 'wp_footer', 'the_block_template_skip_link' ) ) {
+ return $template_html;
+ }
+
+ // Ensure a skip-link target exists and has an ID.
+ $processor = new WP_HTML_Tag_Processor( $template_html );
+ $skip_link_target_id = null;
+
+ // Get the first element.
+ if ( $processor->next_tag( 'MAIN' ) ) {
+ $skip_link_target_id = $processor->get_attribute( 'id' );
+ if ( ! is_string( $skip_link_target_id ) || '' === trim( $skip_link_target_id ) ) {
+ $skip_link_target_id = 'wp--skip-link--target';
+ $processor->set_attribute( 'id', $skip_link_target_id );
+ }
+ }
+
+ // Early exit if a skip-link target can't be located.
+ if ( null === $skip_link_target_id ) {
+ return $template_html;
+ }
+
+ // Apply any updates from setting the main ID.
+ $template_html = $processor->get_updated_html();
+
+ // Anonymous subclass of WP_HTML_Tag_Processor to access protected bookmark spans.
+ $inserter = new class( $template_html ) extends WP_HTML_Tag_Processor {
+ /**
+ * Inserts text before the current token.
+ *
+ * @param string $text Text to insert.
+ */
+ public function insert_before( string $text ) {
+ $this->set_bookmark( 'here' );
+ $this->lexical_updates[] = new WP_HTML_Text_Replacement( $this->bookmarks['here']->start, 0, $text );
+ }
+ };
+
+ while ( $inserter->next_tag() ) {
+ if ( 'DIV' === $inserter->get_tag() && $inserter->has_class( 'wp-site-blocks' ) ) {
+ $skip_link = sprintf(
+ '%s',
+ esc_url( '#' . $skip_link_target_id ),
+ /* translators: Hidden accessibility text. */
+ esc_html__( 'Skip to content' )
+ );
+ $inserter->insert_before( $skip_link );
+ break;
+ }
+ }
+
+ return $inserter->get_updated_html();
}
/**
diff --git a/src/wp-includes/css/wp-block-template-skip-link.css b/src/wp-includes/css/wp-block-template-skip-link.css
new file mode 100644
index 0000000000000..4176599ad0667
--- /dev/null
+++ b/src/wp-includes/css/wp-block-template-skip-link.css
@@ -0,0 +1,27 @@
+.skip-link.screen-reader-text {
+ border: 0;
+ clip-path: inset(50%);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute !important;
+ width: 1px;
+ word-wrap: normal !important;
+}
+
+.skip-link.screen-reader-text:focus {
+ background-color: #eee;
+ clip-path: none;
+ color: #444;
+ display: block;
+ font-size: 1em;
+ height: auto;
+ left: 5px;
+ line-height: normal;
+ padding: 15px 23px 14px;
+ text-decoration: none;
+ top: 5px;
+ width: auto;
+ z-index: 100000;
+}
diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php
index 89ef4acf734f3..326248122f129 100644
--- a/src/wp-includes/script-loader.php
+++ b/src/wp-includes/script-loader.php
@@ -1605,6 +1605,9 @@ function wp_default_styles( $styles ) {
$styles->add( 'wp-pointer', "/wp-includes/css/wp-pointer$suffix.css", array( 'dashicons' ) );
$styles->add( 'customize-preview', "/wp-includes/css/customize-preview$suffix.css", array( 'dashicons' ) );
$styles->add( 'wp-empty-template-alert', "/wp-includes/css/wp-empty-template-alert$suffix.css" );
+ $skip_link_style_path = WPINC . "/css/wp-block-template-skip-link$suffix.css";
+ $styles->add( 'wp-block-template-skip-link', $skip_link_style_path );
+ $styles->add_data( 'wp-block-template-skip-link', 'path', ABSPATH . $skip_link_style_path );
// External libraries and friends.
$styles->add( 'imgareaselect', '/wp-includes/js/imgareaselect/imgareaselect.css', array(), '0.9.8' );
diff --git a/src/wp-includes/theme-templates.php b/src/wp-includes/theme-templates.php
index eed0fb9b2b029..cc3af0d53d8f5 100644
--- a/src/wp-includes/theme-templates.php
+++ b/src/wp-includes/theme-templates.php
@@ -99,10 +99,11 @@ function wp_filter_wp_template_unique_post_slug( $override_slug, $slug, $post_id
}
/**
- * Enqueues the skip-link script & styles.
+ * Enqueues the skip-link styles.
*
* @access private
* @since 6.4.0
+ * @since 7.0.0 A script is no longer printed in favor of being added via {@see _block_template_skip_link_markup()}.
*
* @global string $_wp_current_template_content
*/
@@ -125,96 +126,7 @@ function wp_enqueue_block_template_skip_link() {
return;
}
- $skip_link_styles = '
- .skip-link.screen-reader-text {
- border: 0;
- clip-path: inset(50%);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute !important;
- width: 1px;
- word-wrap: normal !important;
- }
-
- .skip-link.screen-reader-text:focus {
- background-color: #eee;
- clip-path: none;
- color: #444;
- display: block;
- font-size: 1em;
- height: auto;
- left: 5px;
- line-height: normal;
- padding: 15px 23px 14px;
- text-decoration: none;
- top: 5px;
- width: auto;
- z-index: 100000;
- }';
-
- $handle = 'wp-block-template-skip-link';
-
- /**
- * Print the skip-link styles.
- */
- wp_register_style( $handle, false );
- wp_add_inline_style( $handle, $skip_link_styles );
- wp_enqueue_style( $handle );
-
- /**
- * Enqueue the skip-link script.
- */
- ob_start();
- ?>
-
- true ) );
- wp_add_inline_script( $script_handle, $skip_link_script );
- wp_enqueue_script( $script_handle );
+ wp_enqueue_style( 'wp-block-template-skip-link' );
}
/**
diff --git a/tests/phpunit/tests/block-template-utils.php b/tests/phpunit/tests/block-template-utils.php
index e5255ba5ae011..f46f044a04327 100644
--- a/tests/phpunit/tests/block-template-utils.php
+++ b/tests/phpunit/tests/block-template-utils.php
@@ -297,6 +297,76 @@ public function data_remove_theme_attribute_in_block_template_content() {
);
}
+ /**
+ * Tests adding the skip link (or not).
+ *
+ * @ticket 64361
+ *
+ * @covers ::_block_template_skip_link_markup
+ *
+ * @dataProvider data_provider_to_test_block_template_skip_link_markup
+ */
+ public function test_block_template_skip_link_markup( ?Closure $set_up, string $template_html, string $expected ) {
+ if ( $set_up instanceof Closure ) {
+ $set_up();
+ }
+ $this->assertEqualHTML( $expected, _block_template_skip_link_markup( $template_html ) );
+ }
+
+ /**
+ * Data provider for test_block_template_skip_link_markup.
+ *
+ * @return array>
+ */
+ public function data_provider_to_test_block_template_skip_link_markup(): array {
+ return array(
+ 'inserts_link_and_adds_main_id_when_missing' => array(
+ 'set_up' => null,
+ 'template_html' => 'Content
',
+ 'expected' => '
+ Skip to content
+ Content
+ ',
+ ),
+ 'uses_existing_main_id' => array(
+ 'set_up' => null,
+ 'template_html' => 'Content
',
+ 'expected' => '
+ Skip to content
+ Content
+ ',
+ ),
+ 'main_has_boolean_id' => array(
+ 'set_up' => null,
+ 'template_html' => 'Content
',
+ 'expected' => '
+ Skip to content
+ Content
+ ',
+ ),
+ 'main_has_whitespace_id' => array(
+ 'set_up' => null,
+ 'template_html' => 'Content
',
+ 'expected' => '
+ Skip to content
+ Content
+ ',
+ ),
+ 'action_removed' => array(
+ 'set_up' => static function () {
+ remove_action( 'wp_footer', 'the_block_template_skip_link' );
+ },
+ 'template_html' => 'Content
',
+ 'expected' => 'Content
',
+ ),
+ 'main_missing' => array(
+ 'set_up' => null,
+ 'template_html' => '',
+ 'expected' => '',
+ ),
+ );
+ }
+
/**
* Should retrieve the template from the theme files.
*/