From 6145c4477b1b1a980478f29f0d5bc72a526b5254 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Tue, 17 Mar 2026 14:03:56 +0000 Subject: [PATCH 1/3] fix: Use HTML-based detection for navigation overlay close block Replace block tree check and pattern resolution with WP_HTML_Tag_Processor on rendered overlay HTML. Fixes double close button when overlay template parts use patterns. Fixes #76567 --- .../block-library/src/navigation/index.php | 30 +++++-- ...lass-wp-navigation-block-renderer-test.php | 90 +++++++++++++++++++ 2 files changed, 114 insertions(+), 6 deletions(-) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index fcc4c9198d4cc0..86023738370090 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -697,14 +697,10 @@ private static function get_responsive_container_markup( $attributes, $inner_blo if ( ! empty( $attributes['overlay'] ) ) { // Get blocks from the overlay template part. $overlay_blocks = static::get_overlay_blocks_from_template_part( $attributes['overlay'], $attributes ); - // Check if overlay contains a navigation-overlay-close block. - $has_custom_overlay_close_block = block_core_navigation_block_tree_has_block_type( - $overlay_blocks, - 'core/navigation-overlay-close', - array( 'core/navigation' ) // Skip navigation blocks, as they cannot contain an overlay close block - ); // Render template part blocks directly without navigation container wrapper. $overlay_blocks_html = static::get_template_part_blocks_html( $overlay_blocks ); + // Check if overlay contains a navigation-overlay-close block (detect in rendered HTML so it works with patterns). + $has_custom_overlay_close_block = block_core_navigation_overlay_html_has_close_block( $overlay_blocks_html ); // Add Interactivity API directives to the overlay close block if present. if ( $has_custom_overlay_close_block && $is_interactive ) { $tags = new WP_HTML_Tag_Processor( $overlay_blocks_html ); @@ -1094,6 +1090,28 @@ function block_core_navigation_get_inner_blocks_from_unstable_location( $attribu } } +/** + * Checks if the overlay HTML contains a navigation-overlay-close block. + * + * Uses WP_HTML_Tag_Processor to detect the close button in rendered output, + * so it works when the overlay uses patterns (pattern content is rendered at + * output time, not in the block tree). + * + * @since 7.0.0 + * + * @param string $html The rendered overlay HTML. + * @return bool True if a close button element is found. + */ +function block_core_navigation_overlay_html_has_close_block( $html ) { + $tags = new WP_HTML_Tag_Processor( $html ); + return $tags->next_tag( + array( + 'tag_name' => 'BUTTON', + 'class_name' => 'wp-block-navigation-overlay-close', + ) + ); +} + /** * Add Interactivity API directives to the navigation-overlay-close block * markup using the Tag Processor. diff --git a/phpunit/blocks/class-wp-navigation-block-renderer-test.php b/phpunit/blocks/class-wp-navigation-block-renderer-test.php index e6f7c1dd5144fb..3d14b6d74444ea 100644 --- a/phpunit/blocks/class-wp-navigation-block-renderer-test.php +++ b/phpunit/blocks/class-wp-navigation-block-renderer-test.php @@ -253,6 +253,96 @@ public function test_gutenberg_block_core_navigation_block_tree_has_block_type_r $this->assertFalse( $result ); } + /** + * Test that gutenberg_block_core_navigation_overlay_html_has_close_block returns true when HTML contains the close button element. + * + * @group navigation-renderer + * + * @covers ::gutenberg_block_core_navigation_overlay_html_has_close_block + */ + public function test_block_core_navigation_overlay_html_has_close_block_returns_true_when_close_button_present() { + $html = '
'; + $result = gutenberg_block_core_navigation_overlay_html_has_close_block( $html ); + $this->assertTrue( $result ); + } + + /** + * Test that gutenberg_block_core_navigation_overlay_html_has_close_block returns false when HTML does not contain the close button. + * + * @group navigation-renderer + * + * @covers ::gutenberg_block_core_navigation_overlay_html_has_close_block + */ + public function test_block_core_navigation_overlay_html_has_close_block_returns_false_when_absent() { + $html = '

No close button here

'; + $result = gutenberg_block_core_navigation_overlay_html_has_close_block( $html ); + $this->assertFalse( $result ); + } + + /** + * Test that gutenberg_block_core_navigation_overlay_html_has_close_block returns false when class string appears only in text content. + * + * @group navigation-renderer + * + * @covers ::gutenberg_block_core_navigation_overlay_html_has_close_block + */ + public function test_block_core_navigation_overlay_html_has_close_block_returns_false_when_class_in_text_only() { + $html = '

Use the wp-block-navigation-overlay-close button to close

'; + $result = gutenberg_block_core_navigation_overlay_html_has_close_block( $html ); + $this->assertFalse( $result ); + } + + /** + * Test that gutenberg_block_core_navigation_overlay_html_has_close_block finds nested close button. + * + * @group navigation-renderer + * + * @covers ::gutenberg_block_core_navigation_overlay_html_has_close_block + */ + public function test_block_core_navigation_overlay_html_has_close_block_finds_nested_close_button() { + $html = '
'; + $result = gutenberg_block_core_navigation_overlay_html_has_close_block( $html ); + $this->assertTrue( $result ); + } + + /** + * Test that gutenberg_block_core_navigation_overlay_html_has_close_block detects close block when overlay content is a pattern. + * + * Simulates the bug scenario: template part contains wp:pattern, pattern renders its content including the close block. + * + * @group navigation-renderer + * + * @covers ::gutenberg_block_core_navigation_overlay_html_has_close_block + */ + public function test_block_core_navigation_overlay_html_has_close_block_detects_close_in_pattern_output() { + register_block_pattern( + 'test/navigation-overlay-pattern', + array( + 'title' => 'Navigation Overlay Pattern', + 'content' => '
', + 'description' => 'Pattern containing navigation-overlay-close (simulates overlay template part using pattern).', + 'categories' => array( 'navigation' ), + ) + ); + + // Simulate overlay template part content: just a pattern block (unresolved in block tree). + $parsed_blocks = parse_blocks( '' ); + $blocks = new WP_Block_List( $parsed_blocks, array() ); + + // Render blocks (pattern block's render_callback outputs pattern content). + $html = ''; + foreach ( $blocks as $block ) { + $html .= $block->render(); + } + + $this->assertTrue( + gutenberg_block_core_navigation_overlay_html_has_close_block( $html ), + 'Close block should be detected in rendered pattern output (fixes #76567).' + ); + + unregister_block_pattern( 'test/navigation-overlay-pattern' ); + } + /** * Test that gutenberg_block_core_navigation_block_tree_has_block_type skips searching inside specified block types. * From d9f20cab3e704fb28fb74cf1dccc7e434af96024 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Tue, 17 Mar 2026 14:13:05 +0000 Subject: [PATCH 2/3] fix: Apply PHP coding standards to navigation block renderer test --- phpunit/blocks/class-wp-navigation-block-renderer-test.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phpunit/blocks/class-wp-navigation-block-renderer-test.php b/phpunit/blocks/class-wp-navigation-block-renderer-test.php index 3d14b6d74444ea..896b6c51718e60 100644 --- a/phpunit/blocks/class-wp-navigation-block-renderer-test.php +++ b/phpunit/blocks/class-wp-navigation-block-renderer-test.php @@ -261,7 +261,7 @@ public function test_gutenberg_block_core_navigation_block_tree_has_block_type_r * @covers ::gutenberg_block_core_navigation_overlay_html_has_close_block */ public function test_block_core_navigation_overlay_html_has_close_block_returns_true_when_close_button_present() { - $html = '
'; + $html = '
'; $result = gutenberg_block_core_navigation_overlay_html_has_close_block( $html ); $this->assertTrue( $result ); } @@ -327,7 +327,7 @@ public function test_block_core_navigation_overlay_html_has_close_block_detects_ // Simulate overlay template part content: just a pattern block (unresolved in block tree). $parsed_blocks = parse_blocks( '' ); - $blocks = new WP_Block_List( $parsed_blocks, array() ); + $blocks = new WP_Block_List( $parsed_blocks, array() ); // Render blocks (pattern block's render_callback outputs pattern content). $html = ''; From ca8ba3eb19a30a0c74e7a6acdc7f8fa8c59522c5 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Tue, 17 Mar 2026 20:09:08 +0000 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ...lass-wp-navigation-block-renderer-test.php | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/phpunit/blocks/class-wp-navigation-block-renderer-test.php b/phpunit/blocks/class-wp-navigation-block-renderer-test.php index 896b6c51718e60..fc81a57f659194 100644 --- a/phpunit/blocks/class-wp-navigation-block-renderer-test.php +++ b/phpunit/blocks/class-wp-navigation-block-renderer-test.php @@ -260,7 +260,7 @@ public function test_gutenberg_block_core_navigation_block_tree_has_block_type_r * * @covers ::gutenberg_block_core_navigation_overlay_html_has_close_block */ - public function test_block_core_navigation_overlay_html_has_close_block_returns_true_when_close_button_present() { + public function test_gutenberg_block_core_navigation_overlay_html_has_close_block_returns_true_when_close_button_present() { $html = '
'; $result = gutenberg_block_core_navigation_overlay_html_has_close_block( $html ); $this->assertTrue( $result ); @@ -273,7 +273,7 @@ public function test_block_core_navigation_overlay_html_has_close_block_returns_ * * @covers ::gutenberg_block_core_navigation_overlay_html_has_close_block */ - public function test_block_core_navigation_overlay_html_has_close_block_returns_false_when_absent() { + public function test_gutenberg_block_core_navigation_overlay_html_has_close_block_returns_false_when_absent() { $html = '

No close button here

'; $result = gutenberg_block_core_navigation_overlay_html_has_close_block( $html ); $this->assertFalse( $result ); @@ -286,7 +286,7 @@ public function test_block_core_navigation_overlay_html_has_close_block_returns_ * * @covers ::gutenberg_block_core_navigation_overlay_html_has_close_block */ - public function test_block_core_navigation_overlay_html_has_close_block_returns_false_when_class_in_text_only() { + public function test_gutenberg_block_core_navigation_overlay_html_has_close_block_returns_false_when_class_in_text_only() { $html = '

Use the wp-block-navigation-overlay-close button to close

'; $result = gutenberg_block_core_navigation_overlay_html_has_close_block( $html ); $this->assertFalse( $result ); @@ -325,22 +325,24 @@ public function test_block_core_navigation_overlay_html_has_close_block_detects_ ) ); - // Simulate overlay template part content: just a pattern block (unresolved in block tree). - $parsed_blocks = parse_blocks( '' ); - $blocks = new WP_Block_List( $parsed_blocks, array() ); - - // Render blocks (pattern block's render_callback outputs pattern content). - $html = ''; - foreach ( $blocks as $block ) { - $html .= $block->render(); + try { + // Simulate overlay template part content: just a pattern block (unresolved in block tree). + $parsed_blocks = parse_blocks( '' ); + $blocks = new WP_Block_List( $parsed_blocks, array() ); + + // Render blocks (pattern block's render_callback outputs pattern content). + $html = ''; + foreach ( $blocks as $block ) { + $html .= $block->render(); + } + + $this->assertTrue( + gutenberg_block_core_navigation_overlay_html_has_close_block( $html ), + 'Close block should be detected in rendered pattern output (fixes #76567).' + ); + } finally { + unregister_block_pattern( 'test/navigation-overlay-pattern' ); } - - $this->assertTrue( - gutenberg_block_core_navigation_overlay_html_has_close_block( $html ), - 'Close block should be detected in rendered pattern output (fixes #76567).' - ); - - unregister_block_pattern( 'test/navigation-overlay-pattern' ); } /**