diff --git a/src/wp-includes/nav-menu-template.php b/src/wp-includes/nav-menu-template.php index 592522878f..df478a5fa4 100644 --- a/src/wp-includes/nav-menu-template.php +++ b/src/wp-includes/nav-menu-template.php @@ -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. diff --git a/tests/phpunit/tests/menu/wp-nav-menu.php b/tests/phpunit/tests/menu/wp-nav-menu.php new file mode 100644 index 0000000000..ac7c815b89 --- /dev/null +++ b/tests/phpunit/tests/menu/wp-nav-menu.php @@ -0,0 +1,173 @@ + '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( + '