Jason G. Designs

WordPress Themes, Print-On-Demand Resources and more…

Hiding and Showing a WordPress Menu II- JavaScript Only, Please

30
Jun
2016
Dropdown menu with styling

Okay, so this is a continuation of a previously written tutorial, Hiding and Showing a WordPress Menu on Mobile Devices. In that tutorial, I described how to create a JS controlled dropdown menu that turns into a button in mobile devices. This tutorial assumes some knowledge of JavaScript, but since I learned this just recently, it shouldn’t be too hard to follow for my readers as well. Also, you may want to read the previous tutorial, as menu creation and setup is explained there.

In my newest theme now under construction, I created a variation of a hide/show menu using JavaScript only (with a lot of help from the _s starter theme that it uses). I learned quite a bit from how the theme has its menu code setup. Making some adjustments, I adapted the menu design in tablet view and wider to be similar to the design in the previous tutorial, but without jQuery. Without further ado, let’s get busy.

Let’s Look at Underscores’ (_s) Navigation Code

The navigation code for Underscores is what places the button onto the page in mobile view. The screenshot below shows the basic behavior of the menu toggle button. When clicked, it reveals a menu. When clicked a second time it hides the menu.

Menu dropdown system

Underscores (_s) menu dropdown shown in Firefox’s Responsive Design View

Below is the link to the latest code for navigation.js in _s.

https://github.com/Automattic/_s/blob/master/js/navigation.js?TB_iframe=true&width=921.6&height=921.6

In the code above, to display the menu, the unordered list is set to display: none. In my theme, I set the menu to display: flex instead, as the theme uses flexbox for layout. You may want to leave it as is or set the ul to a different display method, such as block if you want users with JavaScript disabled to see the menu’s contents. I adjusted the navigation.js in my theme to hide the menu on load this way:

menu.setAttribute( 'aria-expanded', 'false' );

// set initial menu state here, instead of CSS file, in case JavaScript is turned off in browser.
var windowWidth = window.innerWidth;
	
if ( windowWidth < 600 ) {
    menu.style.display = 'none';
}

...

Looking at the navigation.js file linked to above, a few explanations are in order, as this is the method we will use with our expanded menu system later.

var container, button, menu;

… Sets up variables we can define later in the script.

container = document.getElementById( 'site-navigation' );

… Finds the element with the id #site-navigation, which holds our button.

button = container.getElementsByTagName( 'button' )[0];

… Looks for the first (and only) instance of a button within the container, as denoted by the [0] array location. If you wanted to find a second button in the container, you would use [1], for example.

menu = container.getElementsByTagName( 'ul' )[0];

… Looks for the first ul tag within the container

if ( -1 === menu.className.indexOf( 'nav-menu' ) ) {
    menu.className += ' nav-menu';
}

This bit of code checks to see if the menu’s ul does not contain the string “nav-menu”. If not, it is added with the className property with the plus sign (+). The plus operator followed by the equals sign adds the class (with a space before it) to any existing classes.

button.onclick = function() {
    if ( -1 !== container.className.indexOf( 'toggled' ) ) {
        container.className = container.className.replace( ' toggled', '' );
        button.setAttribute( 'aria-expanded', 'false' );
        menu.setAttribute( 'aria-expanded', 'false' );
    } else {
        container.className += ' toggled';
        button.setAttribute( 'aria-expanded', 'true' );
        menu.setAttribute( 'aria-expanded', 'true' );
    }
};

Using the same method above to check if the menu’s container has the class toggled, it replaces the class with an empty string, otherwise (else) it adds the class toggled. The aria-expanded attributes are to assist users using screen readers.

Navigation.js also has similar functions for focus based navigation, but the code above is enough for us to work with.

Styling the Button

Underscores starts you off with a button that reads “Primary Menu”. That’s fine if you want that for the button’s style. I think a hamburger menu symbol as a replacement would work well here. For my theme, I used Google’s Material Design icons. You can use that or an embedded font icon. Below I’ll show examples for Material Design and Font Awesome. In header.php, add (or replace in Underscores based theme):

<button class="menu-toggle" aria-controls="primary-menu" aria-expanded="false" title="<?php esc_attr_e( 'Toggle the navigation menu' ) ?>"><i class="material-icons">menu</i></button>
<?php wp_nav_menu( array( ... ) ); ?>

or…

<button class="menu-toggle" aria-controls="primary-menu" aria-expanded="false" title="<?php esc_attr_e( 'Toggle the navigation menu' ) ?>"><i class="fa fa-bars" aria-hidden="true"></i></button> 
<?php wp_nav_menu( array( ... ) ); ?>

The button should go directly above your wp_nav_menu generated code.

If you are using Underscores, button styles are created for you. If not, you can set your button to a finger friendly design with a CSS width and height of 72px or 4.5em. Any other styling is probably best left to your theme.

The Navigation Code

The code that controls the expanded horizontal menu can be placed in a separate script or in the provided navigation.js script.

First, it may be a good idea to comment out the code that hides the menu ul on page load, so we can see what we will be styling.

if ( windowWidth < 600 ) {
    //menu.style.display = 'none';
}

Your theme may have some breakpoints setup already. Increase your browser to a width larger than the breakpoint for tablets (in my theme it is ~600px). Firefox’s Responsive Design View or Chrome’s Developer Tools are good to use for this purpose. In your style.css breakpoint for tablets, change the code as shown in the pseudocode:

For Flexbox:

@media screen and (min-width: 600px) {
    .menu-container or #menu-container ul {
        display: flex;
        align-items: flex-end;
        flex-flow: row wrap;
    }
}

With Inline-Block:

@media screen and (min-width: 600px) {
    .menu-container or #menu-container ul li {
        display: inline-block;
        vertical-align: top;
    }
}

The code above is very simple baseline CSS to get your list items aligned horizontally. Any other theming you do is dependent on how you would like your theme’s menu to look. I set a min-height and a dark background on the parent container for my theme.

Now on to the JavaScript. In either navigation.js or a new script (perhaps named your-theme-name-scripts.js), add the following and save to your js or scripts directory:

/* Add buttons to the navigation menu */

// setup variables
var windowWidth = window.innerWidth;
var hasChildren = document.querySelectorAll( '.main-navigation .page_item_has_children' );
var hasChildrenLink = document.querySelectorAll( '.main-navigation .page_item_has_children > a' );
var customHasChildren = document.querySelectorAll( '.main-navigation .menu-item-has-children' );
var customHasChildrenLink = document.querySelectorAll( '.main-navigation .menu-item-has-children > a' );

Setting up some variables, we’re using document.querySelectorAll to choose all of the elements on the page that have a certain class attached to it. querySelectorAll can be used to select multiple elements with specified class names. For instance, in WordPress you may have article with a class of post. You can select this for muliple page types by comma separating each class Ex. document.querySelectorAll( '.blog .post, .archive .post, ... ');

querySelectorAll returns a node list, which is unusable for applying manipulations to each element returned, so we must first loop over each element and then apply some manipulations. Right after the previous code, add:

if ( windowWidth >= 600 ) {

    // For custom menus
    for ( var i = 0; i < customHasChildren.length; i++ ) {
        // Add button HTML after each link that has the class .menu-item-has-children
        customHasChildrenLink[i].insertAdjacentHTML( 'afterend', '<button class="menu-down-arrow"><i class="material-icons">arrow_drop_down</i></button>' );
    }

    // For page menu fallback
    for ( var i2 = 0; i2 < hasChildren.length; i2++ ) {
        // Add button HTML after each link that has the class .page_item_has_children
        hasChildrenLink[i2].insertAdjacentHTML( 'afterend', '<button class="menu-down-arrow"><i class="material-icons">arrow_drop_down</i></button>' );
    }

}  // closes windowWidth if statement

The above code uses the insertAdjacentHTML method to add the html string for a button after customHasChildrenLink (a link with the class .menu-item-has-children).

Next, we add buttons to the submenus. Add the following right below the previous for loop:

    /* The code below roughly follows the Vanilla JS method in the article "Lose the jQuery Bloat" */
    /* https://www.sitepoint.com/dom-manipulation-with-nodelist-js/ */
    // loop through each element that has .sub-menu
    var customSubmenuButton = document.querySelectorAll( '.main-navigation .menu-down-arrow' );
    for ( var iSub = 0, customSubmenuButton; iSub < customSubmenuButton.length; iSub++ ) {
        // Add click event to the button to show ul.sub-menu
        customSubmenuButton[iSub].addEventListener( 'click', function () {
            // this refers to the current loop iteration of customSubmenuButton
            // nextElementSibling refers to the neighboring ul with .sub-menu class
            if ( this.nextElementSibling.className.indexOf( 'toggled-submenu' ) !== -1 ) { // if .sub-menu has .toggled-submenu class
                this.nextElementSibling.className = this.nextElementSibling.className.replace( ' toggled-submenu', '' ); // remove it
                this.nextElementSibling.setAttribute( 'aria-expanded', 'false' );
                //console.log( 'button.' + this.className + ' is not toggled' );
            } else {
                this.nextElementSibling.className += ' toggled-submenu'; // otherwise, add it
                this.nextElementSibling.setAttribute( 'aria-expanded', 'true' );
            }
				//console.log( 'button.' + this.className + ' is toggled' );
        } );
    } // ends for loop
    // console.log( customSubmenuButton[iSub] );

I was having trouble getting the submenu to work as I wasn’t figuring out that I needed to use the this keyword to target each dropdown. I came across the website in the comment above. On a side note, nodelist.js is something I might consider in a future project.

What this code does is assign a menu item with the class .menu-down-arrow (that is attached to a button) to the variable customSubmenuButton, loops through each instance like before, then adds an event listener for clicks. The this keyword refers to customSubmenuButton. .nextElementSibling refers to the item next in order, an unordered list. If the ul contains .toggled-submenu class replace it with an empty string, else add the string ‘ toggled-submenu’ to the class list. I added some console logs that aided me along while I was learning to show what is being toggled when you click the button.

The CSS

Finally, we need to add the CSS that is attached to the classes that hide and show the menus. Here, I will use psuedocode because each theme’s menu code might differ slightly. Replace .menu-container with the name of the container that holds your menu in your theme.

For Underscores, I had to make some more adjustments to the style sheet because by default, it gives you a hover based dropdown menu. If you are using Underscores, remove or comment out this snippet:

.main-navigation ul ul li:hover > ul,
.main-navigation ul ul li.focus > ul {
    left: 100%;
}

In your tablet size and up media query (or desktop size if you are using max-width), add:

.menu-container {
    position: relative;
}

@media screen and (min-width: 600px) {
    .menu-toggle {
        display: none;
    }

    .menu-container {
        min-height: pixel or em value;
    }

    /* If using flexbox */
    .menu-container ul .menu-item-has-children,
    .menu-container ul .page_item_has_children {
        display: flex;
    }

    /* Hide sub menus */
    .menu-container ul ul.sub-menu,
    .menu-container ul ul.children {
        display: block;
        position: absolute;
        left: -9999px;
        top: min-height of parent container;
    }

    .menu-container ul ul.sub-menu.toggled-submenu,
    .menu-container ul ul.children.toggled-submenu {
        left: 0;
    }

    /* Hide sub sub menus */
    .menu-container ul ul ul {
        left: -9999em;
        top: 0;
    }

    .menu-container ul ul li > ul.sub-menu.toggled-submenu,
    .menu-container ul ul li.focus > ul.children.toggled-submenu {
        left: 25%; /* or any percentage from 0 to 100 horizontally */
    }
}

Finishing Up

A lot of the properties here, especially the JavaScript ones are meant to work in modern browsers. For Internet Explorer, that usually means version 10 and up. There are workarounds and polyfills for older browsers, though. There is a resources section below for all of the methods listed in this tutorial.

Below is a screenshot from my upcoming theme The M.X. that shows how the JS only dropdown menu can look with some theme specific styling applied to it. Even though this theme does use jQuery for other purposes, if you want to include a menu and are not using jQuery, try these methods. Thanks for reading and see ya’ on the next tut.

Dropdown menu with styling

The M.X. dropdown menu

Resources

From MDN:

Tutorials:

Tags for this post: , ,

2 Comments

  1. notmypt says:

    Can you send me the navigation.js and Css file which were adjusted in this post?
    Thank you

    1. Jason Gonzalez says:

      I sent you an email with the files.

Leave a Reply

Your email address will not be published. Required fields are marked *