Building a Sub-menu: Display Parent Category/Page’s Children When Viewing Children

Funny, I just ran into this same conundrum a few months back…

Background

I found it quite bizarre that WordPress applies all manner of CSS classes to menu-items in order to distinguish the current item and its ancestors, but then completely ignores the sub-menus and their relationship with the current item. I wanted a .current-sub CSS class so that I could hide all .sub-menus except .current-subs.


Theory

After much research and wading through source, the solution that I ended up embracing was to extend the Walker_Nav_Menu class and modify its behavior at two distinct points in order to obtain my imagined .current-sub CSS class:

  1. When processing a menu-item ( see Walker_Nav_Menu->start_el() ), examine the classes that WordPress assigned to the menu-item in order to determine if it corresponds to the current page or is a menu ancestor of the current page.
  2. When processing a new menu-lvl (a.k.a “sub-menu”; see Walker_Nav_Menu->start_lvl() ), if the previous menu-item processed was either the current menu-item or an ancestor of it, apply a new class to the sub-menu element in order to distinguish it as a current submenu.

Aside

Note that I’ve emboldened menu ancestor above. In my situation, it turned out that I didn’t actually care about posts’, pages’, or categories’ hierarchy or relationships with one another (i.e. it didn’t matter whether or not page B was a child of page A). The only thing that I needed to worry about was the hierarchy of their corresponding menu-items.

After the Walker was implemented, all I needed was a new call in a template file to wp_nav_menu() with an instance of my new menu Walker as an argument.


Implementation

For convenience and brevity I’ve provided my implementation in a procedural-style plugin. For the most part the code is identical to the native Walker_Nav_Menu – my comments denote my additions.

//Register a new theme location for a custom menu
function after_setup_theme_97226() {
    register_nav_menu( 'menu-97226', 'Primary Navigation (uses Menu_Walker_97226)' );
}
add_action( 'after_setup_theme', 'after_setup_theme_97226' );

//Queue up CSS modifications
function enqueue_styles_97226() {
    wp_enqueue_style( 'style-97226', plugins_url('style.css', __FILE__) );
}
add_action( 'wp_enqueue_scripts', 'enqueue_styles_97226' );

//Now walk it out.
class Menu_Walker_97226 extends Walker_Nav_Menu {
    const CURRENT_SUBMENU_CLASS = 'current-sub';    //The CSS class to apply to the active submenu
    var $is_current_submenu     = FALSE;            //Whether or not the next sub-menu to be processed is related to the current-menu-item.

    function start_lvl( &$output, $depth = 0, $args = array() ) {
        $indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';

        //If we've begun processing an "active" submenu, add a class to indicate as much.
        $current_sub_class = ( $this->is_current_submenu ) ? Menu_Walker_97226::CURRENT_SUBMENU_CLASS : '';

        //Add the new sub-menu ul element to the markup. Always give sub-menus the standard WordPress "sub-menu" class.
        $output .= "\n$indent<ul class=\"sub-menu $current_sub_class\">\n";

        //We've now processed the sub-menu; reset the flag
        $this->is_current_submenu = FALSE;
    }

    function start_el( &$output, $item, $depth = 0, $args = array() ) {
        //Reset the flag such that "active" menu-items without sub-menus don't pass their "current" status on to the sub-menus of their siblings
        $this->is_current_submenu = FALSE;

        $indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';

        $class_names = $value="";
        $classes = empty( $item->classes ) ? array() : (array) $item->classes;
        $classes[] = 'menu-item-' . $item->ID;
        $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args ) );
        $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';

            //Check to see if this element is "current" or a menu ancestor of "current" so that the "current" status may be passed on to any immediate sub-menu of the item.
            //You should add or remove conditions here in order to fit your application.
            //Also see "Final Notes" section below regarding performance
        if (strpos($class_names, 'current-menu-item')
            || strpos($class_names, 'current-menu-parent')
            || strpos($class_names, 'current-menu-ancestor')
            || strpos($class_names, 'current_page_parent'))
            $this->is_current_submenu = TRUE;

        $id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args );
        $id = $id ? ' id="' . esc_attr( $id ) . '"' : '';

        $output .= $indent . '<li' . $id . $value . $class_names .'>';

        $attributes  = ! empty( $item->attr_title ) ? ' title="'  . esc_attr( $item->attr_title ) .'"' : '';
        $attributes .= ! empty( $item->target )     ? ' target="' . esc_attr( $item->target     ) .'"' : '';
        $attributes .= ! empty( $item->xfn )        ? ' rel="'    . esc_attr( $item->xfn        ) .'"' : '';
        $attributes .= ! empty( $item->url )        ? ' href="'   . esc_attr( $item->url        ) .'"' : '';

        $item_output = $args->before;
        $item_output .= '<a'. $attributes .'>';
        $item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after;
        $item_output .= '</a>';
        $item_output .= $args->after;

        $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
    }
}

The entire contents of the referenced style.css file are as follows:

ul.sub-menu {
    display:none;
}
ul.current-sub {
    display:block;
}

And finally, placing a call to wp_nav_menu() within a template file where the menu is to be displayed in order to make use of the new Walker (see the documentation for additional arguments):

wp_nav_menu( array(
    'theme_location' => 'menu-97226',
    'walker' => new Menu_Walker_97226()
) );

Final Notes

  • I have not run benchmarks, but it is entirely possible that may be quicker to check a menu-item’s classes using in_array() calls before the $classes array is joined rather than using strpos() calls afterwards. There’s also the possibility of using array_flip() and subsequently using isset( $classes[ $class_name ] ), though I suspect that the flip would defeat any performance gained from using isset().
  • Some part of my implementation was heavily inspired by a blog post that I’ve since lost track of in the months between. If I manage to locate it I will return to link it.
  • If any menu-items appear multiple times in your menu you may run into obscure behavior as all occurrences of a “current” menu item will maintain “current” status.
  • I’ve used the terms “current” and “active” somewhat interchangeably to refer to menu-items and sub-menus that should be displayed… My terminology is somewhat fuzzy and my head muddled at the moment – I’ll try to return later to clean it up 😉

This may not be the exact solution you were looking for (and there may well exist better approaches to the issue), but I hope it’s enough to set you on the right path!

Leave a Comment