From 854330039d015a4a4091ebe423d3db4132263644 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Fri, 12 Dec 2025 12:17:27 +0000 Subject: [PATCH 01/19] Render custom overlay template parts in Navigation block - Add get_overlay_blocks_from_template_part() helper to retrieve blocks from overlay template parts - Add get_template_part_blocks_html() to render template part blocks without navigation container wrapper - Modify get_responsive_container_markup() to render both desktop and overlay blocks in separate containers - Add CSS rules to show/hide desktop and overlay containers based on breakpoint and menu state - Ensure overlay container is full width - Maintain backward compatibility when no overlay is selected --- .../block-library/src/navigation/index.php | 124 +++++++++++++++++- .../block-library/src/navigation/style.scss | 48 +++++++ 2 files changed, 171 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index b3993e3e899923..fc3daf1e878b30 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -158,6 +158,22 @@ private static function get_markup_for_inner_block( $inner_block ) { return $inner_block_content; } + /** + * Returns the html for blocks from a template part (without navigation container wrapper). + * + * @since 6.5.0 + * + * @param WP_Block_List $blocks The list of blocks to render. + * @return string Returns the html for the template part blocks. + */ + private static function get_template_part_blocks_html( $blocks ) { + $html = ''; + foreach ( $blocks as $block ) { + $html .= $block->render(); + } + return $html; + } + /** * Returns the html for the inner blocks of the navigation block. * @@ -272,6 +288,86 @@ private static function get_inner_blocks_from_fallback( $attributes ) { return new WP_Block_List( $fallback_blocks, $attributes ); } + /** + * Gets the inner blocks for the navigation block from an overlay template part. + * + * @since 6.5.0 + * + * @param string $overlay_template_part_id The overlay template part ID in format "theme//slug". + * @param array $attributes The block attributes. + * @return WP_Block_List Returns the inner blocks for the overlay template part. + */ + private static function get_overlay_blocks_from_template_part( $overlay_template_part_id, $attributes ) { + if ( empty( $overlay_template_part_id ) || ! is_string( $overlay_template_part_id ) ) { + return new WP_Block_List( array(), $attributes ); + } + + // Parse the template part ID (format: "theme//slug"). + $parts = explode( '//', $overlay_template_part_id, 2 ); + if ( count( $parts ) !== 2 ) { + return new WP_Block_List( array(), $attributes ); + } + + $theme = $parts[0]; + $slug = $parts[1]; + + // Only query for template parts from the active theme. + if ( get_stylesheet() !== $theme ) { + return new WP_Block_List( array(), $attributes ); + } + + // Query for the template part post. + $template_part_query = new WP_Query( + array( + 'post_type' => 'wp_template_part', + 'post_status' => 'publish', + 'post_name__in' => array( $slug ), + 'tax_query' => array( + array( + 'taxonomy' => 'wp_theme', + 'field' => 'name', + 'terms' => $theme, + ), + ), + 'posts_per_page' => 1, + 'no_found_rows' => true, + 'lazy_load_term_meta' => false, // Do not lazy load term meta, as template parts only have one term. + ) + ); + + $template_part_post = $template_part_query->have_posts() ? $template_part_query->next_post() : null; + + if ( ! $template_part_post ) { + // Try to get from theme file if not in database. + $block_template = get_block_file_template( $overlay_template_part_id, 'wp_template_part' ); + if ( isset( $block_template->content ) ) { + $parsed_blocks = parse_blocks( $block_template->content ); + $blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); + return new WP_Block_List( $blocks, $attributes ); + } + return new WP_Block_List( array(), $attributes ); + } + + // Get the template part content. + $block_template = _build_block_template_result_from_post( $template_part_post ); + if ( ! isset( $block_template->content ) ) { + return new WP_Block_List( array(), $attributes ); + } + + $parsed_blocks = parse_blocks( $block_template->content ); + + // 'parse_blocks' includes a null block with '\n\n' as the content when + // it encounters whitespace. This code strips it. + $blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); + + // Re-serialize, and run Block Hooks algorithm to inject hooked blocks. + $markup = serialize_blocks( $blocks ); + $markup = apply_block_hooks_to_content_from_post_object( $markup, $template_part_post ); + $blocks = parse_blocks( $markup ); + + return new WP_Block_List( $blocks, $attributes ); + } + /** * Gets the inner blocks for the navigation block. * @@ -523,6 +619,32 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $overlay_inline_styles = esc_attr( safecss_filter_attr( $colors['overlay_inline_styles'] ) ); + // Check if an overlay template part is selected. + $has_overlay = ! empty( $attributes['overlay'] ); + $overlay_blocks_html = ''; + + if ( $has_overlay ) { + // Get blocks from the overlay template part. + $overlay_blocks = static::get_overlay_blocks_from_template_part( $attributes['overlay'], $attributes ); + // Render template part blocks directly without navigation container wrapper. + $overlay_blocks_html = static::get_template_part_blocks_html( $overlay_blocks ); + } + + // Build the content markup with desktop and overlay containers. + $content_markup = ''; + if ( $has_overlay && ! empty( $overlay_blocks_html ) ) { + // Render both desktop and overlay blocks in separate containers. + $content_markup = sprintf( + '
%1$s
+ ', + $inner_blocks_html, + $overlay_blocks_html + ); + } else { + // No overlay selected, use existing behavior. + $content_markup = $inner_blocks_html; + } + return sprintf( '
@@ -536,7 +658,7 @@ private static function get_responsive_container_markup( $attributes, $inner_blo
', esc_attr( $modal_unique_id ), - $inner_blocks_html, + $content_markup, $toggle_aria_label_open, $toggle_aria_label_close, esc_attr( trim( implode( ' ', $responsive_container_classes ) ) ), diff --git a/packages/block-library/src/navigation/style.scss b/packages/block-library/src/navigation/style.scss index e5f2fb8779620a..8b687a3158c7c4 100644 --- a/packages/block-library/src/navigation/style.scss +++ b/packages/block-library/src/navigation/style.scss @@ -499,6 +499,32 @@ button.wp-block-navigation-item__content { align-items: var(--navigation-layout-align, initial); } + // Desktop and overlay container visibility. + // By default, show desktop container and hide overlay container. + .wp-block-navigation__desktop-container { + display: block; + } + + .wp-block-navigation__overlay-container { + display: none; + width: 100%; + // Visually hidden and hidden from assistive tech by default. + // aria-hidden is set in the markup. + } + + // When menu is closed (not open), show desktop container and hide overlay container. + &:not(.is-menu-open) { + .wp-block-navigation__responsive-container-content { + .wp-block-navigation__desktop-container { + display: block; + } + + .wp-block-navigation__overlay-container { + display: none; + } + } + } + // If the responsive wrapper is present but overlay is not open, // overlay styles shouldn't apply. &:not(.is-menu-open.is-menu-open) { @@ -551,6 +577,17 @@ button.wp-block-navigation-item__content { // Inherit alignment settings from container. align-items: var(--navigation-layout-justification-setting, inherit); + // When menu is open at mobile, show overlay container and hide desktop container. + .wp-block-navigation__desktop-container { + display: none; + } + + .wp-block-navigation__overlay-container { + display: block; + width: 100%; + // Remove aria-hidden when visible (handled by JavaScript/Interactivity API). + } + // Always align the contents of the menu to the top. &, .wp-block-page-list, @@ -644,6 +681,17 @@ button.wp-block-navigation-item__content { left: 0; } } + + // At desktop breakpoint, always show desktop container and hide overlay container. + .wp-block-navigation__responsive-container-content { + .wp-block-navigation__desktop-container { + display: block; + } + + .wp-block-navigation__overlay-container { + display: none; + } + } } } From 8964b52797397dea10f630e66bced580d46b9e50 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Fri, 12 Dec 2025 12:24:45 +0000 Subject: [PATCH 02/19] Remove default spacing and colors when custom overlay is rendered - Add has-custom-overlay class to responsive container when overlay template part is selected - Exclude default padding from responsive-container when custom overlay is present - Exclude default padding-top from responsive-container-content when custom overlay is present - Exclude default margin-top from responsive-dialog when custom overlay is present - Exclude default background and text colors when custom overlay is present - Simplify CSS structure by removing duplicate selectors --- .../block-library/src/navigation/index.php | 2 ++ .../block-library/src/navigation/style.scss | 26 ++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index fc3daf1e878b30..916eaf8ec52eea 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -562,10 +562,12 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $modal_unique_id = wp_unique_id( 'modal-' ); $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; + $has_custom_overlay = ! empty( $attributes['overlay'] ); $responsive_container_classes = array( 'wp-block-navigation__responsive-container', $is_hidden_by_default ? 'hidden-by-default' : '', + $has_custom_overlay ? 'has-custom-overlay' : '', implode( ' ', $colors['overlay_css_classes'] ), ); $open_button_classes = array( diff --git a/packages/block-library/src/navigation/style.scss b/packages/block-library/src/navigation/style.scss index 8b687a3158c7c4..885ca6f5845468 100644 --- a/packages/block-library/src/navigation/style.scss +++ b/packages/block-library/src/navigation/style.scss @@ -549,10 +549,13 @@ button.wp-block-navigation-item__content { } // Try to inherit any root paddings set, so the X can align to a top-right aligned menu. - padding-top: clamp(1rem, var(--wp--style--root--padding-top), 20rem); - padding-right: clamp(1rem, var(--wp--style--root--padding-right), 20rem); - padding-bottom: clamp(1rem, var(--wp--style--root--padding-bottom), 20rem); - padding-left: clamp(1rem, var(--wp--style--root--padding-left), 20rem); + // Don't apply default padding when custom overlay is present. + &:not(.has-custom-overlay) { + padding-top: clamp(1rem, var(--wp--style--root--padding-top), 20rem); + padding-right: clamp(1rem, var(--wp--style--root--padding-right), 20rem); + padding-bottom: clamp(1rem, var(--wp--style--root--padding-bottom), 20rem); + padding-left: clamp(1rem, var(--wp--style--root--padding-left), 20rem); + } // Allow modal to scroll. overflow: auto; @@ -560,10 +563,13 @@ button.wp-block-navigation-item__content { // Give it a z-index just higher than the adminbar. z-index: 100000; - .wp-block-navigation__responsive-container-content { - // Add padding above to accommodate close button. + // Add padding above to accommodate close button. + // Don't apply default padding when custom overlay is present. + &:not(.has-custom-overlay) .wp-block-navigation__responsive-container-content { padding-top: calc(2rem + #{ $navigation-icon-size }); + } + .wp-block-navigation__responsive-container-content { // Don't crop the focus style. overflow: visible; @@ -696,13 +702,14 @@ button.wp-block-navigation-item__content { } // Default menu background and font color. +// Don't apply default colors when custom overlay is present. .wp-block-navigation:not(.has-background) -.wp-block-navigation__responsive-container.is-menu-open { +.wp-block-navigation__responsive-container.is-menu-open:not(.has-custom-overlay) { background-color: #fff; } .wp-block-navigation:not(.has-text-color) -.wp-block-navigation__responsive-container.is-menu-open { +.wp-block-navigation__responsive-container.is-menu-open:not(.has-custom-overlay) { color: #000; } @@ -801,7 +808,8 @@ button.wp-block-navigation-item__content { // Adjust open dialog top margin when admin-bar is visible. // Needs to be scoped to .is-menu-open, or it will shift the position of any other navigations that may be present. -.has-modal-open .admin-bar .is-menu-open .wp-block-navigation__responsive-dialog { +// Don't apply margin when custom overlay is present. +.has-modal-open .admin-bar .is-menu-open:not(.has-custom-overlay) .wp-block-navigation__responsive-dialog { margin-top: $admin-bar-height-big; // Handle smaller admin-bar. From 1ab3cd099f591d6882a248f9fbfcff68d15b37b0 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Fri, 12 Dec 2025 12:28:15 +0000 Subject: [PATCH 03/19] Conditionally show/hide default close button based on overlay close block - Add has_navigation_overlay_close_block() helper to check for core/navigation-overlay-close block - Skip checking inner blocks of navigation blocks for optimization - Conditionally render default close button only when overlay doesn't have its own close block - Hide default close button when core/navigation-overlay-close block is present in overlay template part --- .../block-library/src/navigation/index.php | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 916eaf8ec52eea..8fe4cd991d7e77 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -158,6 +158,35 @@ private static function get_markup_for_inner_block( $inner_block ) { return $inner_block_content; } + /** + * Checks if blocks contain a core/navigation-overlay-close block. + * + * @since 6.5.0 + * + * @param WP_Block_List $blocks The list of blocks to check. + * @return bool Returns true if a navigation-overlay-close block is found. + */ + private static function has_navigation_overlay_close_block( $blocks ) { + if ( empty( $blocks ) ) { + return false; + } + + foreach ( $blocks as $block ) { + if ( 'core/navigation-overlay-close' === $block->name ) { + return true; + } + + // Recursively check inner blocks, but skip navigation blocks. + if ( 'core/navigation' !== $block->name && ! empty( $block->inner_blocks ) ) { + if ( static::has_navigation_overlay_close_block( $block->inner_blocks ) ) { + return true; + } + } + } + + return false; + } + /** * Returns the html for blocks from a template part (without navigation container wrapper). * @@ -624,10 +653,13 @@ private static function get_responsive_container_markup( $attributes, $inner_blo // Check if an overlay template part is selected. $has_overlay = ! empty( $attributes['overlay'] ); $overlay_blocks_html = ''; + $has_overlay_close_block = false; if ( $has_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_overlay_close_block = static::has_navigation_overlay_close_block( $overlay_blocks ); // Render template part blocks directly without navigation container wrapper. $overlay_blocks_html = static::get_template_part_blocks_html( $overlay_blocks ); } @@ -647,12 +679,23 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $content_markup = $inner_blocks_html; } + // Conditionally show the default close button only if overlay doesn't have its own close block. + $close_button_markup = ''; + if ( ! $has_overlay_close_block ) { + $close_button_markup = sprintf( + '', + $toggle_aria_label_close, + $close_button_directives, + $toggle_close_button_content + ); + } + return sprintf( '
- + %13$s
%2$s
@@ -671,7 +714,7 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $open_button_directives, $responsive_container_directives, $responsive_dialog_directives, - $close_button_directives, + $close_button_markup, $responsive_container_content_directives ); } From 575e4e7d9f7eff8a9cf05008641715dca27aa00a Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Fri, 12 Dec 2025 12:42:15 +0000 Subject: [PATCH 04/19] Add interactivity API directives to navigation-overlay-close block - Add block_core_navigation_add_directives_to_overlay_close() function following the same pattern as block_core_navigation_add_directives_to_submenu() - Use WP_HTML_Tag_Processor to find and add directives to the overlay close button - Add data-wp-on--click directive to enable closing the overlay menu - Apply directives only when overlay close block is present and navigation is interactive --- .../block-library/src/navigation/index.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 8fe4cd991d7e77..e3019b8cb668c6 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -662,6 +662,11 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $has_overlay_close_block = static::has_navigation_overlay_close_block( $overlay_blocks ); // Render template part blocks directly without navigation container wrapper. $overlay_blocks_html = static::get_template_part_blocks_html( $overlay_blocks ); + // Add Interactivity API directives to the overlay close block if present. + if ( $has_overlay_close_block && $is_interactive ) { + $tags = new WP_HTML_Tag_Processor( $overlay_blocks_html ); + $overlay_blocks_html = block_core_navigation_add_directives_to_overlay_close( $tags ); + } } // Build the content markup with desktop and overlay containers. @@ -965,6 +970,29 @@ function block_core_navigation_get_inner_blocks_from_unstable_location( $attribu } } +/** + * Add Interactivity API directives to the navigation-overlay-close block + * markup using the Tag Processor. + * + * @since 6.5.0 + * + * @param WP_HTML_Tag_Processor $tags Markup of the navigation block. + * @return string Overlay close markup with the directives injected. + */ +function block_core_navigation_add_directives_to_overlay_close( $tags ) { + // Find the navigation-overlay-close button. + if ( $tags->next_tag( + array( + 'tag_name' => 'BUTTON', + 'class_name' => 'wp-block-navigation-overlay-close', + ) + ) ) { + // Add the same close directive as the default close button. + $tags->set_attribute( 'data-wp-on--click', 'actions.closeMenuOnClick' ); + } + return $tags->get_updated_html(); +} + /** * Add Interactivity API directives to the navigation-submenu and page-list * blocks markup using the Tag Processor. From 134cdf883620d8c803f779a7ab9c8ed166b93e7a Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Fri, 12 Dec 2025 12:46:48 +0000 Subject: [PATCH 05/19] Fix block gap inheritance for desktop container - Add wp-block-navigation__desktop-container to gap: inherit rule - Ensures navigation block continues to inherit block gap CSS after adding desktop container wrapper - Overlay container excluded from gap inheritance as it controls its own spacing - Fix indentation issues in padding rules --- packages/block-library/src/navigation/style.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/navigation/style.scss b/packages/block-library/src/navigation/style.scss index 885ca6f5845468..083553fb4e845a 100644 --- a/packages/block-library/src/navigation/style.scss +++ b/packages/block-library/src/navigation/style.scss @@ -353,7 +353,8 @@ button.wp-block-navigation-item__content { .wp-block-navigation__responsive-dialog, .wp-block-navigation .wp-block-page-list, .wp-block-navigation__container, -.wp-block-navigation__responsive-container-content { +.wp-block-navigation__responsive-container-content, +.wp-block-navigation__desktop-container { gap: inherit; } From 64ded17c503a6e7c4d835f11075a531a7f362368 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Fri, 12 Dec 2025 12:50:07 +0000 Subject: [PATCH 06/19] Disable overlay menu for navigation blocks within overlay template parts - Add disable_overlay_menu_for_nested_navigation_blocks() function to recursively find and modify navigation blocks - Set overlayMenu attribute to 'never' for any navigation blocks found in overlay template parts - Prevents nested overlays (inception problem) when navigation blocks are included in overlay content - Applied to both theme file and database post code paths --- .../block-library/src/navigation/index.php | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index e3019b8cb668c6..a9eb202f56d7f9 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -317,6 +317,42 @@ private static function get_inner_blocks_from_fallback( $attributes ) { return new WP_Block_List( $fallback_blocks, $attributes ); } + /** + * Recursively disables overlay menu for navigation blocks within overlay blocks. + * Prevents nested overlays (inception). + * + * @since 6.5.0 + * + * @param array $blocks Array of parsed block arrays. + * @return array Modified blocks with overlayMenu set to 'never' for navigation blocks. + */ + private static function disable_overlay_menu_for_nested_navigation_blocks( $blocks ) { + if ( empty( $blocks ) || ! is_array( $blocks ) ) { + return $blocks; + } + + foreach ( $blocks as &$block ) { + if ( ! isset( $block['blockName'] ) ) { + continue; + } + + // If this is a navigation block, disable its overlay menu. + if ( 'core/navigation' === $block['blockName'] ) { + if ( ! isset( $block['attrs'] ) ) { + $block['attrs'] = array(); + } + $block['attrs']['overlayMenu'] = 'never'; + } + + // Recursively process inner blocks. + if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { + $block['innerBlocks'] = static::disable_overlay_menu_for_nested_navigation_blocks( $block['innerBlocks'] ); + } + } + + return $blocks; + } + /** * Gets the inner blocks for the navigation block from an overlay template part. * @@ -372,6 +408,8 @@ private static function get_overlay_blocks_from_template_part( $overlay_template if ( isset( $block_template->content ) ) { $parsed_blocks = parse_blocks( $block_template->content ); $blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); + // Disable overlay menu for any navigation blocks within the overlay to prevent nested overlays. + $blocks = static::disable_overlay_menu_for_nested_navigation_blocks( $blocks ); return new WP_Block_List( $blocks, $attributes ); } return new WP_Block_List( array(), $attributes ); @@ -394,6 +432,9 @@ private static function get_overlay_blocks_from_template_part( $overlay_template $markup = apply_block_hooks_to_content_from_post_object( $markup, $template_part_post ); $blocks = parse_blocks( $markup ); + // Disable overlay menu for any navigation blocks within the overlay to prevent nested overlays. + $blocks = static::disable_overlay_menu_for_nested_navigation_blocks( $blocks ); + return new WP_Block_List( $blocks, $attributes ); } From 4810881bbcac4171e0932d788d2890133c1b4012 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Fri, 12 Dec 2025 12:55:51 +0000 Subject: [PATCH 07/19] Add spacing to default close button when within custom overlay - Add top and right spacing to wp-block-navigation__responsive-container-close when parent has has-custom-overlay class - Use clamp value matching root padding pattern to position button away from overlay edges - Only applies when default close button is rendered within a custom overlay template part --- packages/block-library/src/navigation/style.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/block-library/src/navigation/style.scss b/packages/block-library/src/navigation/style.scss index 083553fb4e845a..3bc881c9eb26a8 100644 --- a/packages/block-library/src/navigation/style.scss +++ b/packages/block-library/src/navigation/style.scss @@ -777,6 +777,12 @@ button.wp-block-navigation-item__content { } } +// When default close button is within a custom overlay, add spacing from edges. +.has-custom-overlay .wp-block-navigation__responsive-container-close { + top: clamp(1rem, var(--wp--style--root--padding-left), 20rem); + right: clamp(1rem, var(--wp--style--root--padding-left), 20rem); +} + // The menu adds wrapping containers. .wp-block-navigation__responsive-close { width: 100%; From d7cd00b8fa310cb6f65226cf846ee9e5532559bd Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Fri, 12 Dec 2025 13:03:56 +0000 Subject: [PATCH 08/19] Gate desktop container wrapper behind navigation overlay experiment - Add is_overlay_experiment_enabled() helper to check experiment status - Make desktop container wrapper conditional on experiment being enabled - Maintain backward compatibility: without experiment, desktop navigation markup remains unchanged - Prevents CSS/layout issues for users without the experiment enabled - Add comprehensive comment explaining why this is gated --- .../block-library/src/navigation/index.php | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index a9eb202f56d7f9..6a19f09fc85177 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -41,6 +41,18 @@ class WP_Navigation_Block_Renderer { */ private static $seen_menu_names = array(); + /** + * Returns whether the navigation overlay experiment is enabled. + * + * @since 6.5.0 + * + * @return bool Returns whether the navigation overlay experiment is enabled. + */ + private static function is_overlay_experiment_enabled() { + $gutenberg_experiments = get_option( 'gutenberg-experiments' ); + return $gutenberg_experiments && array_key_exists( 'gutenberg-customizable-navigation-overlays', $gutenberg_experiments ); + } + /** * Returns whether or not this is responsive navigation. * @@ -712,14 +724,28 @@ private static function get_responsive_container_markup( $attributes, $inner_blo // Build the content markup with desktop and overlay containers. $content_markup = ''; + $is_overlay_experiment_enabled = static::is_overlay_experiment_enabled(); + if ( $has_overlay && ! empty( $overlay_blocks_html ) ) { // Render both desktop and overlay blocks in separate containers. - $content_markup = sprintf( - '
%1$s
- ', - $inner_blocks_html, - $overlay_blocks_html - ); + // IMPORTANT: The desktop container wrapper is gated behind the experiment to ensure + // backward compatibility. Without the experiment enabled, the desktop navigation + // should not be wrapped in a container div, maintaining the exact same markup structure + // as before this feature. This prevents any potential CSS or layout issues for users + // who don't have the experiment enabled. + if ( $is_overlay_experiment_enabled ) { + $content_markup = sprintf( + '
%1$s
+ ', + $inner_blocks_html, + $overlay_blocks_html + ); + } else { + // Experiment not enabled: Don't wrap desktop navigation to maintain backward compatibility. + // Render desktop blocks without wrapper (maintains exact same markup as before this feature). + // Note: UI prevents overlay selection when experiment is off, but handle gracefully if attribute exists. + $content_markup = $inner_blocks_html; + } } else { // No overlay selected, use existing behavior. $content_markup = $inner_blocks_html; From 06d792735cb038b24335662e1ca110afefe0e7a1 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Fri, 19 Dec 2025 10:54:26 -0600 Subject: [PATCH 09/19] Consolate overlay code for easier gating * sets default negative values for custom overlay experiment in one location * consolates code to make it harder to accidentally bleed experiment code * changes desktop class to inline so it's not screen size specific --- .../block-library/src/navigation/index.php | 89 +++++++++---------- .../block-library/src/navigation/style.scss | 32 +++---- 2 files changed, 58 insertions(+), 63 deletions(-) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 6a19f09fc85177..3d5990159d1f7b 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -643,13 +643,40 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $colors = block_core_navigation_build_css_colors( $attributes ); $modal_unique_id = wp_unique_id( 'modal-' ); - $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; - $has_custom_overlay = ! empty( $attributes['overlay'] ); + $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; + // Set-up variables for the custom overlay experiment. + // Values are set to "off" so they don't affect the default behavior. + $is_overlay_experiment_enabled = static::is_overlay_experiment_enabled(); + $has_custom_overlay = false; + $close_button_markup = ''; + $has_custom_overlay_close_block = false; + + if( $is_overlay_experiment_enabled ) { + // Check if an overlay template part is selected and render it. + // This needs to happen before building classes so we know if overlay blocks actually exist. + 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 = static::has_navigation_overlay_close_block( $overlay_blocks ); + // Render template part blocks directly without navigation container wrapper. + $overlay_blocks_html = static::get_template_part_blocks_html( $overlay_blocks ); + // 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 ); + $overlay_blocks_html = block_core_navigation_add_directives_to_overlay_close( $tags ); + } + } + + $has_custom_overlay = ! empty( $overlay_blocks_html ); + } + + // Only add the disable-default-overlay class if experiment is enabled AND overlay blocks actually rendered. $responsive_container_classes = array( 'wp-block-navigation__responsive-container', $is_hidden_by_default ? 'hidden-by-default' : '', - $has_custom_overlay ? 'has-custom-overlay' : '', + $has_custom_overlay ? 'disable-default-overlay' : '', implode( ' ', $colors['overlay_css_classes'] ), ); $open_button_classes = array( @@ -703,57 +730,25 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $overlay_inline_styles = esc_attr( safecss_filter_attr( $colors['overlay_inline_styles'] ) ); - // Check if an overlay template part is selected. - $has_overlay = ! empty( $attributes['overlay'] ); - $overlay_blocks_html = ''; - $has_overlay_close_block = false; - - if ( $has_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_overlay_close_block = static::has_navigation_overlay_close_block( $overlay_blocks ); - // Render template part blocks directly without navigation container wrapper. - $overlay_blocks_html = static::get_template_part_blocks_html( $overlay_blocks ); - // Add Interactivity API directives to the overlay close block if present. - if ( $has_overlay_close_block && $is_interactive ) { - $tags = new WP_HTML_Tag_Processor( $overlay_blocks_html ); - $overlay_blocks_html = block_core_navigation_add_directives_to_overlay_close( $tags ); - } - } - - // Build the content markup with desktop and overlay containers. + // Build the content markup with inline and overlay containers. $content_markup = ''; - $is_overlay_experiment_enabled = static::is_overlay_experiment_enabled(); - if ( $has_overlay && ! empty( $overlay_blocks_html ) ) { - // Render both desktop and overlay blocks in separate containers. - // IMPORTANT: The desktop container wrapper is gated behind the experiment to ensure - // backward compatibility. Without the experiment enabled, the desktop navigation - // should not be wrapped in a container div, maintaining the exact same markup structure - // as before this feature. This prevents any potential CSS or layout issues for users - // who don't have the experiment enabled. - if ( $is_overlay_experiment_enabled ) { - $content_markup = sprintf( - '
%1$s
- ', - $inner_blocks_html, - $overlay_blocks_html - ); - } else { - // Experiment not enabled: Don't wrap desktop navigation to maintain backward compatibility. - // Render desktop blocks without wrapper (maintains exact same markup as before this feature). - // Note: UI prevents overlay selection when experiment is off, but handle gracefully if attribute exists. - $content_markup = $inner_blocks_html; - } + if ( $has_custom_overlay ) { + // Render both inline and overlay blocks in separate containers. + $content_markup = sprintf( + '
%1$s
+ ', + $inner_blocks_html, + $overlay_blocks_html + ); } else { // No overlay selected, use existing behavior. $content_markup = $inner_blocks_html; } - // Conditionally show the default close button only if overlay doesn't have its own close block. - $close_button_markup = ''; - if ( ! $has_overlay_close_block ) { + // Show default close button for all responsive navigation, + // unless custom overlay has its own close block. + if ( ! $has_custom_overlay_close_block ) { $close_button_markup = sprintf( '', $toggle_aria_label_close, diff --git a/packages/block-library/src/navigation/style.scss b/packages/block-library/src/navigation/style.scss index 3bc881c9eb26a8..420efea9689c94 100644 --- a/packages/block-library/src/navigation/style.scss +++ b/packages/block-library/src/navigation/style.scss @@ -354,7 +354,7 @@ button.wp-block-navigation-item__content { .wp-block-navigation .wp-block-page-list, .wp-block-navigation__container, .wp-block-navigation__responsive-container-content, -.wp-block-navigation__desktop-container { +.wp-block-navigation__inline-container { gap: inherit; } @@ -500,9 +500,9 @@ button.wp-block-navigation-item__content { align-items: var(--navigation-layout-align, initial); } - // Desktop and overlay container visibility. - // By default, show desktop container and hide overlay container. - .wp-block-navigation__desktop-container { + // Inline and overlay container visibility. + // By default, show inline container and hide overlay container. + .wp-block-navigation__inline-container { display: block; } @@ -513,10 +513,10 @@ button.wp-block-navigation-item__content { // aria-hidden is set in the markup. } - // When menu is closed (not open), show desktop container and hide overlay container. + // When menu is closed (not open), show inline container and hide overlay container. &:not(.is-menu-open) { .wp-block-navigation__responsive-container-content { - .wp-block-navigation__desktop-container { + .wp-block-navigation__inline-container { display: block; } @@ -551,7 +551,7 @@ button.wp-block-navigation-item__content { // Try to inherit any root paddings set, so the X can align to a top-right aligned menu. // Don't apply default padding when custom overlay is present. - &:not(.has-custom-overlay) { + &:not(.disable-default-overlay) { padding-top: clamp(1rem, var(--wp--style--root--padding-top), 20rem); padding-right: clamp(1rem, var(--wp--style--root--padding-right), 20rem); padding-bottom: clamp(1rem, var(--wp--style--root--padding-bottom), 20rem); @@ -566,7 +566,7 @@ button.wp-block-navigation-item__content { // Add padding above to accommodate close button. // Don't apply default padding when custom overlay is present. - &:not(.has-custom-overlay) .wp-block-navigation__responsive-container-content { + &:not(.disable-default-overlay) .wp-block-navigation__responsive-container-content { padding-top: calc(2rem + #{ $navigation-icon-size }); } @@ -584,8 +584,8 @@ button.wp-block-navigation-item__content { // Inherit alignment settings from container. align-items: var(--navigation-layout-justification-setting, inherit); - // When menu is open at mobile, show overlay container and hide desktop container. - .wp-block-navigation__desktop-container { + // When menu is open at mobile, show overlay container and hide inline container. + .wp-block-navigation__inline-container { display: none; } @@ -689,9 +689,9 @@ button.wp-block-navigation-item__content { } } - // At desktop breakpoint, always show desktop container and hide overlay container. + // At desktop breakpoint, always show inline container and hide overlay container. .wp-block-navigation__responsive-container-content { - .wp-block-navigation__desktop-container { + .wp-block-navigation__inline-container { display: block; } @@ -705,12 +705,12 @@ button.wp-block-navigation-item__content { // Default menu background and font color. // Don't apply default colors when custom overlay is present. .wp-block-navigation:not(.has-background) -.wp-block-navigation__responsive-container.is-menu-open:not(.has-custom-overlay) { +.wp-block-navigation__responsive-container.is-menu-open:not(.disable-default-overlay) { background-color: #fff; } .wp-block-navigation:not(.has-text-color) -.wp-block-navigation__responsive-container.is-menu-open:not(.has-custom-overlay) { +.wp-block-navigation__responsive-container.is-menu-open:not(.disable-default-overlay) { color: #000; } @@ -778,7 +778,7 @@ button.wp-block-navigation-item__content { } // When default close button is within a custom overlay, add spacing from edges. -.has-custom-overlay .wp-block-navigation__responsive-container-close { +.disable-default-overlay .wp-block-navigation__responsive-container-close { top: clamp(1rem, var(--wp--style--root--padding-left), 20rem); right: clamp(1rem, var(--wp--style--root--padding-left), 20rem); } @@ -816,7 +816,7 @@ button.wp-block-navigation-item__content { // Adjust open dialog top margin when admin-bar is visible. // Needs to be scoped to .is-menu-open, or it will shift the position of any other navigations that may be present. // Don't apply margin when custom overlay is present. -.has-modal-open .admin-bar .is-menu-open:not(.has-custom-overlay) .wp-block-navigation__responsive-dialog { +.has-modal-open .admin-bar .is-menu-open:not(.disable-default-overlay) .wp-block-navigation__responsive-dialog { margin-top: $admin-bar-height-big; // Handle smaller admin-bar. From 081cabcf1aaafb698b7c488f7c6891a693ba945c Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Fri, 19 Dec 2025 13:12:59 -0600 Subject: [PATCH 10/19] Remove inline-container wrapper for custom overlays and simplify CSS The wrapper could cause issues with how people target their css. Sibling + selectors could break if we add a new container. To avoid this, we use the existing markup and output the navigation custom overlay at the end of the block. --- .../block-library/src/navigation/index.php | 24 +++--- .../block-library/src/navigation/style.scss | 74 ++++++------------- 2 files changed, 34 insertions(+), 64 deletions(-) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 3d5990159d1f7b..10cceb16b9e2c4 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -645,12 +645,14 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; - // Set-up variables for the custom overlay experiment. + // Set-up variables for the custom overlay experiment. // Values are set to "off" so they don't affect the default behavior. $is_overlay_experiment_enabled = static::is_overlay_experiment_enabled(); $has_custom_overlay = false; $close_button_markup = ''; $has_custom_overlay_close_block = false; + $overlay_blocks_html = ''; + $custom_overlay_markup = ''; if( $is_overlay_experiment_enabled ) { // Check if an overlay template part is selected and render it. @@ -730,20 +732,12 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $overlay_inline_styles = esc_attr( safecss_filter_attr( $colors['overlay_inline_styles'] ) ); - // Build the content markup with inline and overlay containers. - $content_markup = ''; - + if ( $has_custom_overlay ) { - // Render both inline and overlay blocks in separate containers. - $content_markup = sprintf( - '
%1$s
- ', - $inner_blocks_html, + $custom_overlay_markup = sprintf( + '', $overlay_blocks_html ); - } else { - // No overlay selected, use existing behavior. - $content_markup = $inner_blocks_html; } // Show default close button for all responsive navigation, @@ -765,12 +759,13 @@ private static function get_responsive_container_markup( $attributes, $inner_blo %13$s
%2$s + %15$s
', esc_attr( $modal_unique_id ), - $content_markup, + $inner_blocks_html, $toggle_aria_label_open, $toggle_aria_label_close, esc_attr( trim( implode( ' ', $responsive_container_classes ) ) ), @@ -782,7 +777,8 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $responsive_container_directives, $responsive_dialog_directives, $close_button_markup, - $responsive_container_content_directives + $responsive_container_content_directives, + $has_custom_overlay ? $custom_overlay_markup : '' ); } diff --git a/packages/block-library/src/navigation/style.scss b/packages/block-library/src/navigation/style.scss index 420efea9689c94..b7de079c47d58e 100644 --- a/packages/block-library/src/navigation/style.scss +++ b/packages/block-library/src/navigation/style.scss @@ -353,8 +353,7 @@ button.wp-block-navigation-item__content { .wp-block-navigation__responsive-dialog, .wp-block-navigation .wp-block-page-list, .wp-block-navigation__container, -.wp-block-navigation__responsive-container-content, -.wp-block-navigation__inline-container { +.wp-block-navigation__responsive-container-content { gap: inherit; } @@ -500,32 +499,6 @@ button.wp-block-navigation-item__content { align-items: var(--navigation-layout-align, initial); } - // Inline and overlay container visibility. - // By default, show inline container and hide overlay container. - .wp-block-navigation__inline-container { - display: block; - } - - .wp-block-navigation__overlay-container { - display: none; - width: 100%; - // Visually hidden and hidden from assistive tech by default. - // aria-hidden is set in the markup. - } - - // When menu is closed (not open), show inline container and hide overlay container. - &:not(.is-menu-open) { - .wp-block-navigation__responsive-container-content { - .wp-block-navigation__inline-container { - display: block; - } - - .wp-block-navigation__overlay-container { - display: none; - } - } - } - // If the responsive wrapper is present but overlay is not open, // overlay styles shouldn't apply. &:not(.is-menu-open.is-menu-open) { @@ -584,17 +557,6 @@ button.wp-block-navigation-item__content { // Inherit alignment settings from container. align-items: var(--navigation-layout-justification-setting, inherit); - // When menu is open at mobile, show overlay container and hide inline container. - .wp-block-navigation__inline-container { - display: none; - } - - .wp-block-navigation__overlay-container { - display: block; - width: 100%; - // Remove aria-hidden when visible (handled by JavaScript/Interactivity API). - } - // Always align the contents of the menu to the top. &, .wp-block-page-list, @@ -667,6 +629,29 @@ button.wp-block-navigation-item__content { } } + // Custom overlay container visibility. + // When there's a custom overlay, by default hide overlay container. + &.disable-default-overlay { + .wp-block-navigation__overlay-container { + display: none; + width: 100%; + } + + // When menu is open with custom overlay, hide default navigation and show overlay. + &.is-menu-open { + .wp-block-navigation__responsive-container-content { + // Hides the content of the non-overlay navigation. + > .wp-block-navigation__container { + display: none; + } + + .wp-block-navigation__overlay-container { + display: block; + } + } + } + } + @include break-small() { &:not(.hidden-by-default) { &:not(.is-menu-open) { @@ -688,17 +673,6 @@ button.wp-block-navigation-item__content { left: 0; } } - - // At desktop breakpoint, always show inline container and hide overlay container. - .wp-block-navigation__responsive-container-content { - .wp-block-navigation__inline-container { - display: block; - } - - .wp-block-navigation__overlay-container { - display: none; - } - } } } From 3c2ed86602cfe2ec4aba1faad67cd03b44d5a292 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Mon, 5 Jan 2026 14:41:08 -0600 Subject: [PATCH 11/19] Remove custom color for focus outline If the current color matches the background of the page (common) then the focus ring is not visible. Let it default to the color set by the page instead of overriding it. --- packages/block-library/src/navigation-overlay-close/style.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/block-library/src/navigation-overlay-close/style.scss b/packages/block-library/src/navigation-overlay-close/style.scss index 563a51ef086679..22d677778018d5 100644 --- a/packages/block-library/src/navigation-overlay-close/style.scss +++ b/packages/block-library/src/navigation-overlay-close/style.scss @@ -10,7 +10,6 @@ text-decoration: none; &:focus { - outline: 2px solid currentColor; outline-offset: 2px; } From 2dc49c648522903c56583d9c1cb3408c14a99617 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Mon, 5 Jan 2026 14:50:08 -0600 Subject: [PATCH 12/19] Fix focus transfer and modal focus trap On the default overlay, there are no extra focusable elements or duplicate menus. On a custom overlay, the existing menu is hidden and the custom overlay has additiona focusable elements. This was causing a problem for focus transfer and focus trap in the modal, as it was not checking for element visibilty when determining focusability. --- packages/block-library/src/navigation/view.js | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/block-library/src/navigation/view.js b/packages/block-library/src/navigation/view.js index 3d98ad53ee3988..ef91d50812aa47 100644 --- a/packages/block-library/src/navigation/view.js +++ b/packages/block-library/src/navigation/view.js @@ -18,6 +18,28 @@ const focusableSelectors = [ '[tabindex]:not([tabindex^="-"])', ]; +/** + * Gets all visible focusable elements within a container. + * Filters out elements that are hidden. + * + * @param {HTMLElement} ref - The container element to search within + * @return {HTMLElement[]} Array of visible focusable elements + */ +function getFocusableElements( ref ) { + const focusableElements = ref.querySelectorAll( focusableSelectors ); + return Array.from( focusableElements ).filter( ( element ) => { + // Use modern checkVisibility API if available (Chrome 105+, Firefox 106+, Safari 17.4+) + if ( typeof element.checkVisibility === 'function' ) { + return element.checkVisibility( { + checkOpacity: false, + checkVisibilityCSS: true, + } ); + } + // Fallback for older browsers + return element.offsetParent !== null; + } ); +} + // This is a fix for Safari in iOS/iPadOS. Without it, Safari doesn't focus out // when the user taps in the body. It can be removed once we add an overlay to // capture the clicks, instead of relying on the focusout event. @@ -198,8 +220,7 @@ const { state, actions } = store( const ctx = getContext(); const { ref } = getElement(); if ( state.isMenuOpen ) { - const focusableElements = - ref.querySelectorAll( focusableSelectors ); + const focusableElements = getFocusableElements( ref ); ctx.modal = ref; ctx.firstFocusableElement = focusableElements[ 0 ]; ctx.lastFocusableElement = @@ -209,8 +230,7 @@ const { state, actions } = store( focusFirstElement() { const { ref } = getElement(); if ( state.isMenuOpen ) { - const focusableElements = - ref.querySelectorAll( focusableSelectors ); + const focusableElements = getFocusableElements( ref ); focusableElements?.[ 0 ]?.focus(); } }, From 8bdda897af04a5fe045fa875fc504a9c2c689e50 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Mon, 5 Jan 2026 14:52:24 -0600 Subject: [PATCH 13/19] Remove aria-hidden from custom overlay This aria-hidden was not tied to the overlay state, so it was fixed as hidden. Also, this should be unnecssary, as display: none will already hide it from the accessibility tree. --- packages/block-library/src/navigation/index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 10cceb16b9e2c4..37092eb7e1fb6e 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -735,7 +735,7 @@ private static function get_responsive_container_markup( $attributes, $inner_blo if ( $has_custom_overlay ) { $custom_overlay_markup = sprintf( - '', + '
%s
', $overlay_blocks_html ); } From 57ac1d2d19b0af5f527758faa2ac9bf0daf6042b Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Tue, 6 Jan 2026 14:08:30 -0600 Subject: [PATCH 14/19] Add generic block_tree_has_block_type function We were using three separate functions that were doing the same thing - searching the block tree for the presence of a block. This refactors that into one generic function and implements it for each. --- .../block-library/src/navigation/index.php | 86 ++++++++------- ...lass-wp-navigation-block-renderer-test.php | 102 ++++++++++++++++++ 2 files changed, 148 insertions(+), 40 deletions(-) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 37092eb7e1fb6e..d73d5c11c8add2 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -170,35 +170,6 @@ private static function get_markup_for_inner_block( $inner_block ) { return $inner_block_content; } - /** - * Checks if blocks contain a core/navigation-overlay-close block. - * - * @since 6.5.0 - * - * @param WP_Block_List $blocks The list of blocks to check. - * @return bool Returns true if a navigation-overlay-close block is found. - */ - private static function has_navigation_overlay_close_block( $blocks ) { - if ( empty( $blocks ) ) { - return false; - } - - foreach ( $blocks as $block ) { - if ( 'core/navigation-overlay-close' === $block->name ) { - return true; - } - - // Recursively check inner blocks, but skip navigation blocks. - if ( 'core/navigation' !== $block->name && ! empty( $block->inner_blocks ) ) { - if ( static::has_navigation_overlay_close_block( $block->inner_blocks ) ) { - return true; - } - } - } - - return false; - } - /** * Returns the html for blocks from a template part (without navigation container wrapper). * @@ -661,7 +632,11 @@ private static function get_responsive_container_markup( $attributes, $inner_blo // 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 = static::has_navigation_overlay_close_block( $overlay_blocks ); + $has_custom_overlay_close_block = 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 ); // Add Interactivity API directives to the overlay close block if present. @@ -929,7 +904,10 @@ public static function render( $attributes, $content, $block ) { $inner_blocks = static::get_inner_blocks( $attributes, $block ); // Prevent navigation blocks referencing themselves from rendering. - if ( block_core_navigation_block_contains_core_navigation( $inner_blocks ) ) { + if ( block_tree_has_block_type( + $inner_blocks, + 'core/navigation' + ) ) { return ''; } @@ -1273,26 +1251,54 @@ static function ( $block ) { } /** - * Returns true if the navigation block contains a nested navigation block. + * Recursively checks if blocks contain a specific block type. * - * @since 6.2.0 + * @since 7.0.0 * - * @param WP_Block_List $inner_blocks Inner block instance to be normalized. - * @return bool true if the navigation block contains a nested navigation block. + * @param WP_Block_List $blocks The list of blocks to check. + * @param string $block_type The block type to search for (e.g., 'core/navigation'). + * @param array $skip_block_types Optional. Block types to skip when recursing. Default empty array. + * @return bool Returns true if the specified block type is found. */ -function block_core_navigation_block_contains_core_navigation( $inner_blocks ) { - foreach ( $inner_blocks as $block ) { - if ( 'core/navigation' === $block->name ) { +function block_tree_has_block_type( $blocks, $block_type, $skip_block_types = array() ) { + if ( empty( $blocks ) ) { + return false; + } + + foreach ( $blocks as $block ) { + if ( $block_type === $block->name ) { return true; } - if ( $block->inner_blocks && block_core_navigation_block_contains_core_navigation( $block->inner_blocks ) ) { - return true; + + // Recursively check inner blocks, skipping specified block types. + if ( ! in_array( $block->name, $skip_block_types, true ) && ! empty( $block->inner_blocks ) ) { + if ( block_tree_has_block_type( $block->inner_blocks, $block_type, $skip_block_types ) ) { + return true; + } } } return false; } +/** + * Returns true if the navigation block contains a nested navigation block. + * + * @since 6.2.0 + * @deprecated 7.0.0 Use block_tree_has_block_type() instead. + * + * @param WP_Block_List $inner_blocks Inner block instance to be normalized. + * @return bool true if the navigation block contains a nested navigation block. + */ +function block_core_navigation_block_contains_core_navigation( $inner_blocks ) { + _deprecated_function( __FUNCTION__, '7.0.0', 'block_tree_has_block_type()' ); + + return block_tree_has_block_type( + $inner_blocks, + 'core/navigation' + ); +} + /** * Retrieves the appropriate fallback to be used on the front of the * site when there is no menu assigned to the Nav block. diff --git a/phpunit/blocks/class-wp-navigation-block-renderer-test.php b/phpunit/blocks/class-wp-navigation-block-renderer-test.php index 2fac525975566e..2bcb5ed7669c65 100644 --- a/phpunit/blocks/class-wp-navigation-block-renderer-test.php +++ b/phpunit/blocks/class-wp-navigation-block-renderer-test.php @@ -177,4 +177,106 @@ public function test_gutenberg_get_inner_blocks_from_navigation_post_returns_emp $this->assertEquals( $actual, $expected ); $this->assertCount( 0, $actual ); } + + /** + * Test that block_tree_has_block_type finds a block at the top level. + * + * @group navigation-renderer + * + * @covers ::block_tree_has_block_type + */ + public function test_gutenberg_block_tree_has_block_type_finds_top_level_block() { + $parsed_blocks = parse_blocks( + '

Test

' + ); + $blocks = new WP_Block_List( $parsed_blocks, array() ); + + $result = block_tree_has_block_type( + $blocks, + 'core/navigation-overlay-close' + ); + + $this->assertTrue( $result ); + } + + /** + * Test that block_tree_has_block_type finds a deeply nested block. + * + * @group navigation-renderer + * + * @covers ::block_tree_has_block_type + */ + public function test_gutenberg_block_tree_has_block_type_finds_nested_block() { + $parsed_blocks = parse_blocks( + ' +
+ +
+ +
+ +
+ +
+ +
+ ' + ); + $blocks = new WP_Block_List( $parsed_blocks, array() ); + + $result = block_tree_has_block_type( + $blocks, + 'core/navigation-overlay-close' + ); + + $this->assertTrue( $result ); + } + + /** + * Test that block_tree_has_block_type returns false when block is not found. + * + * @group navigation-renderer + * + * @covers ::block_tree_has_block_type + */ + public function test_gutenberg_block_tree_has_block_type_returns_false_when_not_found() { + $parsed_blocks = parse_blocks( + '

Test

Title

' + ); + $blocks = new WP_Block_List( $parsed_blocks, array() ); + + $result = block_tree_has_block_type( + $blocks, + 'core/navigation-overlay-close' + ); + + $this->assertFalse( $result ); + } + + /** + * Test that block_tree_has_block_type skips searching inside specified block types. + * + * @group navigation-renderer + * + * @covers ::block_tree_has_block_type + */ + public function test_gutenberg_block_tree_has_block_type_skips_specified_blocks() { + $parsed_blocks = parse_blocks( + ' + + ' + ); + $blocks = new WP_Block_List( $parsed_blocks, array() ); + + // Should NOT find the block because it's inside a navigation block which we're skipping + $result = block_tree_has_block_type( + $blocks, + 'core/navigation-link', + array( 'core/navigation' ) + ); + + $this->assertFalse( $result ); + } } From 980b3a2c827ffea6e8445ca95ed6ad9a8a931ba6 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Tue, 6 Jan 2026 15:30:54 -0600 Subject: [PATCH 15/19] Fix php lint errors --- .../block-library/src/navigation/index.php | 29 ++++++++--------- ...lass-wp-navigation-block-renderer-test.php | 32 +++++++++---------- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index d73d5c11c8add2..9291c56e04bd66 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -614,25 +614,25 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $colors = block_core_navigation_build_css_colors( $attributes ); $modal_unique_id = wp_unique_id( 'modal-' ); - $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; + $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; // Set-up variables for the custom overlay experiment. // Values are set to "off" so they don't affect the default behavior. - $is_overlay_experiment_enabled = static::is_overlay_experiment_enabled(); - $has_custom_overlay = false; - $close_button_markup = ''; + $is_overlay_experiment_enabled = static::is_overlay_experiment_enabled(); + $has_custom_overlay = false; + $close_button_markup = ''; $has_custom_overlay_close_block = false; - $overlay_blocks_html = ''; - $custom_overlay_markup = ''; + $overlay_blocks_html = ''; + $custom_overlay_markup = ''; - if( $is_overlay_experiment_enabled ) { + if ( $is_overlay_experiment_enabled ) { // Check if an overlay template part is selected and render it. // This needs to happen before building classes so we know if overlay blocks actually exist. 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_tree_has_block_type( + $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 @@ -707,7 +707,6 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $overlay_inline_styles = esc_attr( safecss_filter_attr( $colors['overlay_inline_styles'] ) ); - if ( $has_custom_overlay ) { $custom_overlay_markup = sprintf( '
%s
', @@ -904,7 +903,7 @@ public static function render( $attributes, $content, $block ) { $inner_blocks = static::get_inner_blocks( $attributes, $block ); // Prevent navigation blocks referencing themselves from rendering. - if ( block_tree_has_block_type( + if ( block_core_navigation_block_tree_has_block_type( $inner_blocks, 'core/navigation' ) ) { @@ -1260,7 +1259,7 @@ static function ( $block ) { * @param array $skip_block_types Optional. Block types to skip when recursing. Default empty array. * @return bool Returns true if the specified block type is found. */ -function block_tree_has_block_type( $blocks, $block_type, $skip_block_types = array() ) { +function block_core_navigation_block_tree_has_block_type( $blocks, $block_type, $skip_block_types = array() ) { if ( empty( $blocks ) ) { return false; } @@ -1272,7 +1271,7 @@ function block_tree_has_block_type( $blocks, $block_type, $skip_block_types = ar // Recursively check inner blocks, skipping specified block types. if ( ! in_array( $block->name, $skip_block_types, true ) && ! empty( $block->inner_blocks ) ) { - if ( block_tree_has_block_type( $block->inner_blocks, $block_type, $skip_block_types ) ) { + if ( block_core_navigation_block_tree_has_block_type( $block->inner_blocks, $block_type, $skip_block_types ) ) { return true; } } @@ -1285,15 +1284,15 @@ function block_tree_has_block_type( $blocks, $block_type, $skip_block_types = ar * Returns true if the navigation block contains a nested navigation block. * * @since 6.2.0 - * @deprecated 7.0.0 Use block_tree_has_block_type() instead. + * @deprecated 7.0.0 Use block_core_navigation_block_tree_has_block_type() instead. * * @param WP_Block_List $inner_blocks Inner block instance to be normalized. * @return bool true if the navigation block contains a nested navigation block. */ function block_core_navigation_block_contains_core_navigation( $inner_blocks ) { - _deprecated_function( __FUNCTION__, '7.0.0', 'block_tree_has_block_type()' ); + _deprecated_function( __FUNCTION__, '7.0.0', 'block_core_navigation_block_tree_has_block_type()' ); - return block_tree_has_block_type( + return block_core_navigation_block_tree_has_block_type( $inner_blocks, 'core/navigation' ); diff --git a/phpunit/blocks/class-wp-navigation-block-renderer-test.php b/phpunit/blocks/class-wp-navigation-block-renderer-test.php index 2bcb5ed7669c65..a62acb4b2fd8e6 100644 --- a/phpunit/blocks/class-wp-navigation-block-renderer-test.php +++ b/phpunit/blocks/class-wp-navigation-block-renderer-test.php @@ -179,19 +179,19 @@ public function test_gutenberg_get_inner_blocks_from_navigation_post_returns_emp } /** - * Test that block_tree_has_block_type finds a block at the top level. + * Test that block_core_navigation_block_tree_has_block_type finds a block at the top level. * * @group navigation-renderer * - * @covers ::block_tree_has_block_type + * @covers ::block_core_navigation_block_tree_has_block_type */ - public function test_gutenberg_block_tree_has_block_type_finds_top_level_block() { + public function test_gutenberg_block_core_navigation_block_tree_has_block_type_finds_top_level_block() { $parsed_blocks = parse_blocks( '

Test

' ); $blocks = new WP_Block_List( $parsed_blocks, array() ); - $result = block_tree_has_block_type( + $result = block_core_navigation_block_tree_has_block_type( $blocks, 'core/navigation-overlay-close' ); @@ -200,13 +200,13 @@ public function test_gutenberg_block_tree_has_block_type_finds_top_level_block() } /** - * Test that block_tree_has_block_type finds a deeply nested block. + * Test that block_core_navigation_block_tree_has_block_type finds a deeply nested block. * * @group navigation-renderer * - * @covers ::block_tree_has_block_type + * @covers ::block_core_navigation_block_tree_has_block_type */ - public function test_gutenberg_block_tree_has_block_type_finds_nested_block() { + public function test_gutenberg_block_core_navigation_block_tree_has_block_type_finds_nested_block() { $parsed_blocks = parse_blocks( '
@@ -224,7 +224,7 @@ public function test_gutenberg_block_tree_has_block_type_finds_nested_block() { ); $blocks = new WP_Block_List( $parsed_blocks, array() ); - $result = block_tree_has_block_type( + $result = block_core_navigation_block_tree_has_block_type( $blocks, 'core/navigation-overlay-close' ); @@ -233,19 +233,19 @@ public function test_gutenberg_block_tree_has_block_type_finds_nested_block() { } /** - * Test that block_tree_has_block_type returns false when block is not found. + * Test that block_core_navigation_block_tree_has_block_type returns false when block is not found. * * @group navigation-renderer * - * @covers ::block_tree_has_block_type + * @covers ::block_core_navigation_block_tree_has_block_type */ - public function test_gutenberg_block_tree_has_block_type_returns_false_when_not_found() { + public function test_gutenberg_block_core_navigation_block_tree_has_block_type_returns_false_when_not_found() { $parsed_blocks = parse_blocks( '

Test

Title

' ); $blocks = new WP_Block_List( $parsed_blocks, array() ); - $result = block_tree_has_block_type( + $result = block_core_navigation_block_tree_has_block_type( $blocks, 'core/navigation-overlay-close' ); @@ -254,13 +254,13 @@ public function test_gutenberg_block_tree_has_block_type_returns_false_when_not_ } /** - * Test that block_tree_has_block_type skips searching inside specified block types. + * Test that block_core_navigation_block_tree_has_block_type skips searching inside specified block types. * * @group navigation-renderer * - * @covers ::block_tree_has_block_type + * @covers ::block_core_navigation_block_tree_has_block_type */ - public function test_gutenberg_block_tree_has_block_type_skips_specified_blocks() { + public function test_gutenberg_block_core_navigation_block_tree_has_block_type_skips_specified_blocks() { $parsed_blocks = parse_blocks( '