Navigation (Hierarchical) with Expand/Collapse

Navigation (Hierarchical) with Expand/Collapse

This pattern looks and acts like a tree, but it is NOT an ARIA tree (role="tree") and does NOT have the same keyboard behaviors as an ARIA tree. In this pattern, all nodes are navigable with the tab key (in ARIA trees, only one node is available to the tab key; the arrow keys are used for navigating within the tree). This pattern is simpler. It consists of nested lists with expand/collapse buttons to expose or hide child items, and standard HTML links one the final nodes of the branch.

Note:

Even though it is possible to create a navigation menu using an ARIA tree (role="tree"), ARIA trees are not yet fully supported across all screen readers. Because site navigation is so critical to basic website operation, using ARIA trees for navigation is NOT currently recommended.



Turn on a screen reader to experience this example in action.

Initial HTML Markup

<div class="deque-hierarchical-menu-group">
  <div id="navigationRoot">
    <ul class="deque-hierarchical-menu">
      <li>
        <button data-menu-id="navigationRoot" id="School Activities">
          <span>School Activities</span>
        </button>
        <ul>
          <li>
            <button data-menu-id="navigationRoot" id="Performance" data-parent-id="School Activities">
              <span>Performance</span>
            </button>
            <ul>
              <li>
                <a href="http://www.bing.com/search?q=band" data-menu-id="navigationRoot" data-parent-id="Performance" id="Band" class="external" target="_blank">
                  "Band"
                </a>
              </li>
              <li>
                <a href="http://www.bing.com/search?q=choir" data-menu-id="navigationRoot" data-parent-id="Performance" id="Choir" class="external" target="_blank">
                  "Choir"
                </a>
              </li>
              <li>
                <a href="http://www.bing.com/search?q=theater" data-menu-id="navigationRoot" data-parent-id="Performance" id="Theater" class="external" target="_blank">
                  "Theater"
                </a>
              </li>
            </ul>
          </li>
          <li>
            <button data-menu-id="navigationRoot" id="Sports" data-parent-id="School Activities">
              <span>Sports</span>
            </button>
            <ul>
              <li>
                <button data-menu-id="navigationRoot" id="Fall" data-parent-id="Sports">
                  <span>Fall</span>
                </button>
                <ul>
                  <li>
                    <a href="http://www.bing.com/search?q=football" data-menu-id="navigationRoot" data-parent-id="Fall" id="Football" class="external" target="_blank">
                      "Football"
                    </a>
                  </li>
                  <li>
                    <a href="http://www.bing.com/search?q=soccer" data-menu-id="navigationRoot" data-parent-id="Fall" id="Soccer" class="external" target="_blank">
                      "Soccer"
                    </a>
                  </li>
                  <li>
                    <a href="http://www.bing.com/search?q=cross-country" data-menu-id="navigationRoot" data-parent-id="Fall" id="Cross Country" class="external" target="_blank">
                      "Cross Country"
                    </a>
                  </li>
                  <li>
                    <a href="http://www.bing.com/search?q=cheerleading" data-menu-id="navigationRoot" data-parent-id="Fall" id="Cheerleading-Sports" class="external" target="_blank">
                      "Cheerleading"
                    </a>
                  </li>
                </ul>
              </li>
              <li>
                <button data-menu-id="navigationRoot" id="Winter" data-parent-id="Sports">
                  <span>Winter</span>
                </button>
                <ul>
                  <li>
                    <a href="http://www.bing.com/search?q=basketball" data-menu-id="navigationRoot" data-parent-id="Winter" id="Basketball" class="external" target="_blank">
                      "Basketball"
                    </a>
                  </li>
                  <li>
                    <a href="http://www.bing.com/search?q=wrestling" data-menu-id="navigationRoot" data-parent-id="Winter" id="Wrestling" class="external" target="_blank">
                      "Wrestling"
                    </a>
                  </li>
                  <li>
                    <a href="http://www.bing.com/search?q=cheerleading" data-menu-id="navigationRoot" data-parent-id="Winter" id="Cheerleading-Winter" class="external" target="_blank">
                      "Cheerleading"
                    </a>
                  </li>
                </ul>
              </li>
              <li>
                <button data-menu-id="navigationRoot" id="Spring" data-parent-id="Sports">
                  <span>Spring</span>
                </button>
                <ul>
                  <li>
                    <a href="http://www.bing.com/search?q=baseball" data-menu-id="navigationRoot" data-parent-id="Spring" id="Baseball" class="external" target="_blank">
                      "Baseball"
                    </a>
                  </li>
                  <li>
                    <a href="http://www.bing.com/search?q=track" data-menu-id="navigationRoot" data-parent-id="Spring" id="Track" class="external" target="_blank">
                      "Track"
                    </a>
                  </li>
                </ul>
              </li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </div>
</div>

JavaScript

Required: The complete JavaScript file (for all patterns in the library): deque-patterns.min.js

Key parts of the JavaScript file

Note: The code below is functional only in the context of the complete JavaScript file.

In the @menu-hierarchical section:


      function activateBranch(branch) {
        branch.setAttribute('aria-expanded', 'false');

        branch.addEventListener('click', function () {
          if (nu.isOpen(branch)) {
            nu.closeNode(branch);
            return true;
          } else {
            nu.openNode(branch);
            return false;
          }
        });
      }

      function createHierarchicalMenu(hierarchicalMenu) {
        var holder = hierarchicalMenu.querySelector('#navigationRoot');
        var branches = hierarchicalMenu.querySelectorAll('button');

        for (var i = 0; i < branches.length; i++) {
          activateBranch(branches[i]);
        }

        (0, _behavior2.default)(holder);
      }

      function activateAllhierarchicalMenus() {
        var hierarchicalMenus = document.querySelectorAll('.deque-hierarchical-menu-group');

        for (var i = 0; i < hierarchicalMenus.length; i++) {
          createHierarchicalMenu(hierarchicalMenus[i]);
        }
      }

      activateAllhierarchicalMenus();
      

In the @hierarchicalMenu-NodeUtils section:


      function isOpen(node) {
        return node.getAttribute('aria-expanded') === 'true';
      }

      function closeNode(node) {
        node.setAttribute('aria-expanded', 'false');
        return node;
      }

      function openNode(node) {
        node.setAttribute('aria-expanded', 'true');
        return node;
      }

      function setFocus(nodes, toFocus) {
        nodes.forEach(function (n) {
          if (n === toFocus) {
            n.tabIndex = 0;
            n.focus();
          } else {
            n.tabIndex = -1;
          }
        });
      }

      function getNextVisibleNode(items, node) {
        var visibles = items.filter(function (item) {
          return isVisible(item, true, true);
        });
        var idx = visibles.indexOf(node);
        return visibles[idx + 1];
      }

      function getPreviousVisibleNode(items, node) {
        var visibles = items.filter(function (item) {
          return isVisible(item, true, true);
        });
        var idx = visibles.indexOf(node);
        return visibles[idx - 1];
      }

      function isVisible(el, screenReader, recursed) {
        var style = void 0;
        var nodeName = el.nodeName.toUpperCase();
        var parent = el.parentNode;

        // 9 === Node.DOCUMENT
        if (el.nodeType === 9) {
          return true;
        }

        style = window.getComputedStyle(el, null);
        if (style === null) {
          return false;
        }

        var isDisplayNone = style.getPropertyValue('display') === 'none';
        var isInvisibleTag = nodeName.toUpperCase() === 'STYLE' || nodeName.toUpperCase() === 'SCRIPT';
        var srHidden = screenReader && el.getAttribute('aria-hidden') === 'true';
        var isInvisible = !recursed && style.getPropertyValue('visibility') === 'hidden';

        if (isDisplayNone || isInvisibleTag || srHidden || isInvisible) {
          return false;
        }

        if (parent) {
          return isVisible(parent, screenReader, true);
        }

        return false;
      }
      

In the @guidUtils section:


      /*
        note - not a true guid. I prepend 'g' because 
        the ID of an element cannot start with a numeral
      */

      function generateGuid() {
        var S4 = function S4() {
          return ((1 + Math.random()) * 0x10000 | 0).toString(16).substring(1);
        };
        return 'g' + (S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4());
      }
      

Note: No additional JavaScript initialization code is necessary for this pattern. All elements with class="deque-hierarchical-menu-group" will be initialized automatically by the external JavaScript file.

CSS

Required: The complete CSS file (for all patterns in the library): deque-patterns.min.css

Key styles within the CSS file (other styles may also be necessary):


.deque-hierarchical-menu-group .deque-hierarchical-menu {
  list-style: none;
  line-height: 1.8em;
  margin: 0;
  padding: 0;
  max-width: 350px;
  overflow: hidden;
  width: 100%;
  background: #f3f3f3;
}
.deque-hierarchical-menu-group .deque-hierarchical-menu ul {
  list-style-type: none;
  margin-left: 20px;
  padding: 0;
}
.deque-hierarchical-menu-group .deque-hierarchical-menu button[aria-expanded='false'] + ul {
  display: none;
}
.deque-hierarchical-menu-group .deque-hierarchical-menu li {
  list-style: none;
  list-style-type: none;
  padding: 0;
}
.deque-hierarchical-menu-group .deque-hierarchical-menu li button,
.deque-hierarchical-menu-group .deque-hierarchical-menu li a {
  user-select: none;
  display: block;
  background: #f3f3f3;
  padding: 11px 12px 13px;
  outline: 0;
  cursor: pointer;
  white-space: normal;
  color: #000000;
  border: none;
  width: 100%;
  text-align: left;
  margin: 0;
  font: unset;
}
.deque-hierarchical-menu-group .deque-hierarchical-menu li button[aria-expanded='false']:before,
.deque-hierarchical-menu-group .deque-hierarchical-menu li a[aria-expanded='false']:before {
  content: '\E76C';
  font-family: 'mwf-glyphs';
  margin-right: 10px;
}
.deque-hierarchical-menu-group .deque-hierarchical-menu li button[aria-expanded='true'],
.deque-hierarchical-menu-group .deque-hierarchical-menu li a[aria-expanded='true'] {
  border-bottom: 1px solid #dddddd;
}
.deque-hierarchical-menu-group .deque-hierarchical-menu li button[aria-expanded='true']:before,
.deque-hierarchical-menu-group .deque-hierarchical-menu li a[aria-expanded='true']:before {
  content: '\E70D';
  font-family: 'mwf-glyphs';
  margin-right: 10px;
}
.deque-hierarchical-menu-group .deque-hierarchical-menu li button:focus,
.deque-hierarchical-menu-group .deque-hierarchical-menu li a:focus,
.deque-hierarchical-menu-group .deque-hierarchical-menu li button:hover,
.deque-hierarchical-menu-group .deque-hierarchical-menu li a:hover {
  outline: 1px dashed;
  background: rgba(0, 0, 0, 0.04);
}

Implementation Instructions

Step 1: Add Dependencies

Add deque-patterns.min.css in the <head> of the document.

<link rel="stylesheet" type="text/css" href="deque-patterns.min.css">

Add a script link to deque-patterns.min.js to the bottom of the page.

<script type="text/javascript" src="deque-patterns.min.js"></script>

Fonts: Font glyphs are required for this pattern (as currently styled). The CSS file refers to them in a folder called "_fonts" as a child element of the folder in which the CSS file resides. The fonts, from the Microsoft Web Framework (MWF) are available at these links:

Step 2: Add HTML

  • Create a <div> container with class="deque-hierarchical-menu-group" so that the javascript can find and activate each menu.
    • Add a container with a unique ID that will serve as your root.
      • Create a <ul> container with class="deque-hierarchical-menu", for styling purposes. Within this create a <li> container that will hold your menu item.
        • For each item on menu that has children create a <button> container with a unique ID, data-parent-id="X", and data-menu-id="Y", X being your root ID and Y being the ID of the item's parent.
        • Continue this pattern, each item being contained within a <ul> and a <li> tag.
        • For each item without children add a <a> container with a unique ID, data-parent-id="X", and data-menu-id="Y", X being your root ID and Y being the ID of the item's parent. Also include class="external", target="_blank" (opens the link in a new window or tab), and the href attribute, which contains the actual link.