Menus: Remove .menu-item-has-children on wp_nav_menu last level menu items when $depth arg is used.

This changeset prevents `wp_nav_menu` last level menu items from having the `.menu-item-has-children` class when the `$depth` argument is used. It adds a loop in `wp_nav_menu()` to calculate the depth of each menu item with children to make sure the class is added only when applicable.

Props slobodanmanic, kucrut, iCaspar, mdgl, petitphp, audrasjb, costdev.
Fixes #28620.


git-svn-id: https://develop.svn.wordpress.org/trunk@54478 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Jb Audras 2022-10-11 14:02:55 +00:00
parent d136e57d63
commit 2414439208
2 changed files with 190 additions and 3 deletions

View File

@ -196,24 +196,38 @@ function wp_nav_menu( $args = array() ) {
_wp_menu_item_classes_by_context( $menu_items );
$sorted_menu_items = array();
$menu_items_tree = array();
$menu_items_with_children = array();
foreach ( (array) $menu_items as $menu_item ) {
$sorted_menu_items[ $menu_item->menu_order ] = $menu_item;
$menu_items_tree[ $menu_item->ID ] = $menu_item->menu_item_parent;
if ( $menu_item->menu_item_parent ) {
$menu_items_with_children[ $menu_item->menu_item_parent ] = true;
$menu_items_with_children[ $menu_item->menu_item_parent ] = 1;
}
// Calculate the depth of each menu item with children
foreach ( $menu_items_with_children as $menu_item_key => &$menu_item_depth ) {
$menu_item_parent = $menu_items_tree[ $menu_item_key ];
while ( $menu_item_parent ) {
$menu_item_depth = $menu_item_depth + 1;
$menu_item_parent = $menu_items_tree[ $menu_item_parent ];
}
}
}
// Add the menu-item-has-children class where applicable.
if ( $menu_items_with_children ) {
foreach ( $sorted_menu_items as &$menu_item ) {
if ( isset( $menu_items_with_children[ $menu_item->ID ] ) ) {
if (
isset( $menu_items_with_children[ $menu_item->ID ] ) &&
( $args->depth <= 0 || $menu_items_with_children[ $menu_item->ID ] < $args->depth )
) {
$menu_item->classes[] = 'menu-item-has-children';
}
}
}
unset( $menu_items, $menu_item );
unset( $menu_items_tree, $menu_items_with_children, $menu_items, $menu_item );
/**
* Filters the sorted list of menu item objects before generating the menu's HTML.

View File

@ -0,0 +1,173 @@
<?php
/**
* @group menu
*
* @covers ::wp_nav_menu
*/
class Tests_Menu_wpNavMenu extends WP_UnitTestCase {
static $menu_id = 0;
static $lvl0_menu_item = 0;
static $lvl1_menu_item = 0;
static $lvl2_menu_item = 0;
public static function set_up_before_class() {
parent::set_up_before_class();
// Create nav menu.
self::$menu_id = wp_create_nav_menu( 'test' );
// Create lvl0 menu item.
self::$lvl0_menu_item = wp_update_nav_menu_item(
self::$menu_id,
0,
array(
'menu-item-title' => 'Root menu item',
'menu-item-url' => '#',
'menu-item-status' => 'publish',
)
);
// Create lvl1 menu item.
self::$lvl1_menu_item = wp_update_nav_menu_item(
self::$menu_id,
0,
array(
'menu-item-title' => 'Lvl1 menu item',
'menu-item-url' => '#',
'menu-item-parent-id' => self::$lvl0_menu_item,
'menu-item-status' => 'publish',
)
);
// Create lvl2 menu item.
self::$lvl2_menu_item = wp_update_nav_menu_item(
self::$menu_id,
0,
array(
'menu-item-title' => 'Lvl2 menu item',
'menu-item-url' => '#',
'menu-item-parent-id' => self::$lvl1_menu_item,
'menu-item-status' => 'publish',
)
);
/**
* This filter is used to prevent reusing a menu item ID more that once. It cause the tests to failed
* after the first one since the IDs are missing from the HTML generated by `wp_nav_menu`.
*
* To allow the tests to pass, we remove the filter before running them and add it back after
* they ran ({@see Tests_Menu_wpNavMenu::tear_down_after_class()}).
*/
remove_filter( 'nav_menu_item_id', '_nav_menu_item_id_use_once' );
}
public static function tear_down_after_class() {
wp_delete_nav_menu( self::$menu_id );
/**
* This filter was removed to let the tests pass and need to be added back ({@see Tests_Menu_wpNavMenu::set_up_before_class}).
*/
add_filter( 'nav_menu_item_id', '_nav_menu_item_id_use_once', 10, 2 );
parent::tear_down_after_class();
}
/**
* Test all menu items containing children have the CSS class `menu-item-has-children` when displaying the menu
* without specifying a custom depth.
*
* @ticket 28620
*/
public function test_wp_nav_menu_should_have_has_children_class_without_custom_depth() {
// Render the menu with all its hierarchy.
$menu_html = wp_nav_menu(
array(
'menu' => self::$menu_id,
'echo' => false,
)
);
// Level 0 should be present in the HTML output and have the `menu-item-has-children` class.
$this->assertStringContainsString(
sprintf(
'<li id="menu-item-%1$d" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-has-children menu-item-%1$d">',
self::$lvl0_menu_item
),
$menu_html,
'Level 0 should be present in the HTML output and have the menu-item-has-children class'
);
// Level 1 should be present in the HTML output and have the `menu-item-has-children` class.
$this->assertStringContainsString(
sprintf(
'<li id="menu-item-%1$d" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-has-children menu-item-%1$d">',
self::$lvl1_menu_item
),
$menu_html,
'Level 1 should be present in the HTML output and have the menu-item-has-children class'
);
// Level 2 should be present in the HTML output and not have the `menu-item-has-children` class since it has no
// children.
$this->assertStringContainsString(
sprintf(
'<li id="menu-item-%1$d" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-%1$d">',
self::$lvl2_menu_item
),
$menu_html,
'Level 2 should be present in the HTML output and not have the `menu-item-has-children` class since it has no children'
);
}
/**
* Tests that when displaying a menu with a custom depth, the last menu item doesn't have the CSS class
* `menu-item-has-children` even if it's the case when displaying the full menu.
*
* @ticket 28620
*/
public function test_wp_nav_menu_should_not_have_has_children_class_with_custom_depth() {
// Render the menu limited to 1 level of hierarchy (Lvl0 + Lvl1).
$menu_html = wp_nav_menu(
array(
'menu' => self::$menu_id,
'depth' => 2,
'echo' => false,
)
);
// Level 0 should be present in the HTML output and have the `menu-item-has-children` class.
$this->assertStringContainsString(
sprintf(
'<li id="menu-item-%1$d" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-has-children menu-item-%1$d">',
self::$lvl0_menu_item
),
$menu_html,
'Level 0 should be present in the HTML output and have the menu-item-has-children class'
);
// Level 1 should be present in the HTML output and not have the `menu-item-has-children` class since its the
// last item to be rendered.
$this->assertStringContainsString(
sprintf(
'<li id="menu-item-%1$d" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-%1$d">',
self::$lvl1_menu_item
),
$menu_html,
'Level 1 should be present in the HTML output and not have the `menu-item-has-children` class since its the last item to be rendered'
);
// Level 2 should not be present in the HTML output.
$this->assertStringNotContainsString(
sprintf(
'<li id="menu-item-%d"',
self::$lvl2_menu_item
),
$menu_html,
'Level 2 should not be present in the HTML output'
);
}
}