Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
text-decoration: none;

&:focus {
outline: 2px solid currentColor;
outline-offset: 2px;
}

Expand Down
284 changes: 271 additions & 13 deletions packages/block-library/src/navigation/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -158,6 +170,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.
*
Expand Down Expand Up @@ -272,6 +300,127 @@ 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.
*
* @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 ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't worked with template parts much. This function is getting the blocks for a template by ID with sensible fallbacks, right? I'm surprised this isn't a util already.

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 );
// 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 );
}

// 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 );

// 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 );
}

/**
* Gets the inner blocks for the navigation block.
*
Expand Down Expand Up @@ -467,9 +616,44 @@ 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is getting quite long. We might want to consider breaking this into smaller functions in a followup.

// 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.
// 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_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 );
// 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 ? 'disable-default-overlay' : '',
implode( ' ', $colors['overlay_css_classes'] ),
);
$open_button_classes = array(
Expand Down Expand Up @@ -523,14 +707,33 @@ 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(
'<div class="wp-block-navigation__overlay-container">%s</div>',
$overlay_blocks_html
);
}

// 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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about using an overlay close block here instead of the old close button markup?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would also mean that we could keep the close button inside the overlay

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That may be a safer option. Otherwise, we run into issues like this. (Right aligned menu with floating fixed position button). By auto-inserting the overlay close block, we make sure it's positioned outside of the content flow.

close button overlapping navigation content

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do it in a follow-up though. I think we should bring this in sooner.

'<button %1$s class="wp-block-navigation__responsive-container-close" %2$s>%3$s</button>',
$toggle_aria_label_close,
$close_button_directives,
$toggle_close_button_content
);
}

return sprintf(
'<button aria-haspopup="dialog" %3$s class="%6$s" %10$s>%8$s</button>
<div class="%5$s" %7$s id="%1$s" %11$s>
<div class="wp-block-navigation__responsive-close" tabindex="-1">
<div class="wp-block-navigation__responsive-dialog" %12$s>
<button %4$s class="wp-block-navigation__responsive-container-close" %13$s>%9$s</button>
%13$s
<div class="wp-block-navigation__responsive-container-content" %14$s id="%1$s-content">
%2$s
%15$s
</div>
</div>
</div>
Expand All @@ -547,8 +750,9 @@ private static function get_responsive_container_markup( $attributes, $inner_blo
$open_button_directives,
$responsive_container_directives,
$responsive_dialog_directives,
$close_button_directives,
$responsive_container_content_directives
$close_button_markup,
$responsive_container_content_directives,
$has_custom_overlay ? $custom_overlay_markup : ''
);
}

Expand Down Expand Up @@ -699,7 +903,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_core_navigation_block_tree_has_block_type(
$inner_blocks,
'core/navigation'
) ) {
return '';
}

Expand Down Expand Up @@ -798,6 +1005,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.
Expand Down Expand Up @@ -1020,26 +1250,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_core_navigation_block_tree_has_block_type( $blocks, $block_type, $skip_block_types = array() ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this have a more generic name?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to do 'block_tree_has_block_type' but the linter says I have to prefix with the navigation stuff. Maybe we can move it outside of the navigation block for wider use?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i think we should, but maybe in a followup

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_core_navigation_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_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_core_navigation_block_tree_has_block_type()' );

return block_core_navigation_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.
Expand Down
Loading
Loading