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( + '', + 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' => ' + +
Content
+ ', + ), + 'uses_existing_main_id' => array( + 'set_up' => null, + 'template_html' => '
Content
', + 'expected' => ' + +
Content
+ ', + ), + 'main_has_boolean_id' => array( + 'set_up' => null, + 'template_html' => '
Content
', + 'expected' => ' + +
Content
+ ', + ), + 'main_has_whitespace_id' => array( + 'set_up' => null, + 'template_html' => '
Content
', + 'expected' => ' + +
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' => '
Content
', + 'expected' => '
Content
', + ), + ); + } + /** * Should retrieve the template from the theme files. */