diff --git a/src/wp-includes/block-template.php b/src/wp-includes/block-template.php index df3735b238e4f..14976fdd6f19e 100644 --- a/src/wp-includes/block-template.php +++ b/src/wp-includes/block-template.php @@ -301,7 +301,102 @@ 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 . '
'; + + // Back-compat for plugins that disable functionality by unhooking this action. + if ( + ! has_action( 'wp_footer', 'the_block_template_skip_link' ) || + ! has_action( 'wp_enqueue_scripts', 'wp_enqueue_block_template_skip_link' ) + ) { + return $template_html; + } + + return _block_template_add_skip_link( $template_html ); +} + +/** + * Inserts the block template skip-link into the template HTML. + * + * When a `MAIN` element exists in the template, this function will ensure + * that the element contains an `id` attribute, and it will insert a link to + * that `MAIN` element before the first `DIV.wp-site-blocks` element, which + * is the wrapper for all blocks in a block template as constructed by + * {@see get_the_block_template_html()}. + * + * Example: + * + * // Input. + *
+ * + *
+ *

... + * + * // Output. + * + *
+ * + *
+ *

... + * + * When the `MAIN` element already contains a non-empty `id` value it will be + * used instead of the default skip-link id. + * + * @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_add_skip_link( string $template_html ): string { + // Anonymous subclass of WP_HTML_Tag_Processor to access protected bookmark spans. + $processor = 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 ); + } + }; + + // Find and bookmark the first DIV.wp-site-blocks. + if ( + ! $processor->next_tag( + array( + 'tag_name' => 'DIV', + 'class_name' => 'wp-site-blocks', + ) + ) + ) { + return $template_html; + } + $processor->set_bookmark( 'skip_link_insertion_point' ); + + // Ensure the MAIN element has an ID. + if ( ! $processor->next_tag( 'MAIN' ) ) { + return $template_html; + } + + $skip_link_target_id = $processor->get_attribute( 'id' ); + if ( ! is_string( $skip_link_target_id ) || '' === $skip_link_target_id ) { + $skip_link_target_id = 'wp--skip-link--target'; + $processor->set_attribute( 'id', $skip_link_target_id ); + } + + // Seek back to the bookmarked insertion point. + $processor->seek( 'skip_link_insertion_point' ); + + $skip_link = sprintf( + '', + esc_url( '#' . $skip_link_target_id ), + /* translators: Hidden accessibility text. */ + esc_html__( 'Skip to content' ) + ); + $processor->insert_before( $skip_link ); + + return $processor->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 2d1f49ff95f0d..2946f19656d4c 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' ); @@ -1800,6 +1803,7 @@ function wp_default_styles( $styles ) { 'media-views', 'wp-pointer', 'wp-jquery-ui-dialog', + 'wp-block-template-skip-link', // Package styles. 'wp-reset-editor-styles', 'wp-editor-classic-layout-styles', diff --git a/src/wp-includes/theme-templates.php b/src/wp-includes/theme-templates.php index eed0fb9b2b029..301820f78fba7 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_add_skip_link()}. * * @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..b446ede299efa 100644 --- a/tests/phpunit/tests/block-template-utils.php +++ b/tests/phpunit/tests/block-template-utils.php @@ -297,6 +297,87 @@ public function data_remove_theme_attribute_in_block_template_content() { ); } + /** + * Tests that a skip link is added and a MAIN element without an ID receives the default ID. + * + * @ticket 64361 + * + * @covers ::_block_template_add_skip_link + */ + public function test_block_template_add_skip_link_inserts_link_and_adds_main_id_when_missing() { + $template_html = '
Content
'; + $expected = ' + +
Content
+ '; + + $this->assertEqualHTML( $expected, _block_template_add_skip_link( $template_html ) ); + } + + /** + * Tests that an existing MAIN ID is reused for the skip link. + * + * @ticket 64361 + * + * @covers ::_block_template_add_skip_link + */ + public function test_block_template_add_skip_link_uses_existing_main_id() { + $template_html = '
Content
'; + $expected = ' + +
Content
+ '; + + $this->assertEqualHTML( $expected, _block_template_add_skip_link( $template_html ) ); + } + + /** + * Tests that a boolean MAIN ID is treated as missing and replaced with the default. + * + * @ticket 64361 + * + * @covers ::_block_template_add_skip_link + */ + public function test_block_template_add_skip_link_handles_boolean_main_id() { + $template_html = '
Content
'; + $expected = ' + +
Content
+ '; + + $this->assertEqualHTML( $expected, _block_template_add_skip_link( $template_html ) ); + } + + /** + * Tests that a MAIN ID containing whitespace is preserved and used for the skip link. + * + * @ticket 64361 + * + * @covers ::_block_template_add_skip_link + */ + public function test_block_template_add_skip_link_preserves_whitespace_main_id() { + $template_html = '
Content
'; + $expected = ' + +
Content
+ '; + + $this->assertEqualHTML( $expected, _block_template_add_skip_link( $template_html ) ); + } + + /** + * Tests that no changes are made when there is no MAIN element. + * + * @ticket 64361 + * + * @covers ::_block_template_add_skip_link + */ + public function test_block_template_add_skip_link_does_not_modify_when_main_missing() { + $template_html = '
Content
'; + + $this->assertSame( $template_html, _block_template_add_skip_link( $template_html ) ); + } + /** * Should retrieve the template from the theme files. */ diff --git a/tests/phpunit/tests/block-template.php b/tests/phpunit/tests/block-template.php index bb153dfea6c9a..e0307b5782165 100644 --- a/tests/phpunit/tests/block-template.php +++ b/tests/phpunit/tests/block-template.php @@ -311,6 +311,78 @@ public function test_get_the_block_template_html_skips_singular_query_loop_when_ $this->assertSame( array( false ), $in_the_loop_logs, 'Main query loop was triggered despite a custom block template outside the current theme being used' ); } + /** + * Tests that `get_the_block_template_html()` adds a skip link when a MAIN element is present. + * + * @ticket 64361 + * @covers ::get_the_block_template_html + */ + public function test_get_the_block_template_html_adds_skip_link_when_main_present() { + global $_wp_current_template_id, $_wp_current_template_content; + + $_wp_current_template_id = get_stylesheet() . '//index'; + $_wp_current_template_content = '
Content
'; + + $processor = new WP_HTML_Tag_Processor( get_the_block_template_html() ); + $this->assertTrue( + $processor->next_tag( + array( + 'tag_name' => 'A', + 'class_name' => 'skip-link', + ) + ), + 'Expected skip link was not added to the block template HTML.' + ); + $this->assertSame( 'wp-skip-link', $processor->get_attribute( 'id' ), 'Unexpected ID on skip link.' ); + $this->assertTrue( $processor->has_class( 'screen-reader-text' ), 'Expected "screen-reader-text" class on skip link.' ); + } + + /** + * Tests that `get_the_block_template_html()` does not add a skip link when the skip-link action is unhooked. + * + * @ticket 64361 + * @covers ::get_the_block_template_html + * + * @dataProvider data_provider_skip_link_actions + */ + public function test_get_the_block_template_html_does_not_add_skip_link_when_action_unhooked( string $action, string $callback ) { + global $_wp_current_template_id, $_wp_current_template_content; + + $_wp_current_template_id = get_stylesheet() . '//index'; + $_wp_current_template_content = '
Content
'; + + remove_action( $action, $callback ); + + $processor = new WP_HTML_Tag_Processor( get_the_block_template_html() ); + $this->assertFalse( + $processor->next_tag( + array( + 'tag_name' => 'A', + 'class_name' => 'skip-link', + ) + ), + 'Unexpected skip link was added to the block template HTML when the action was unhooked.' + ); + } + + /** + * Data provider for test_get_the_block_template_html_does_not_add_skip_link_when_action_unhooked. + * + * @return array> + */ + public function data_provider_skip_link_actions(): array { + return array( + 'the_block_template_skip_link' => array( + 'action' => 'wp_footer', + 'callback' => 'the_block_template_skip_link', + ), + 'wp_enqueue_block_template_skip_link' => array( + 'action' => 'wp_enqueue_scripts', + 'callback' => 'wp_enqueue_block_template_skip_link', + ), + ); + } + /** * @ticket 58319 * diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index a965665360b05..dacc69fba6330 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1797,6 +1797,7 @@ static function () { $ignored_styles = array( 'core-block-supports-duotone-inline-css', 'wp-block-library-theme-css', + 'wp-block-template-skip-link-css', 'wp-block-template-skip-link-inline-css', );