Example Start

Foods

  • Fruits
    • Oranges
    • Pineapples
    • Bananas
    • Pears
  • Vegetables
    • Broccoli
    • Carrots
    • Spinach
    • Squash
      • Acorn
      • Ambercup
      • Autumn Cup
      • Hubbard
      • Kabocha
      • Butternut
      • Spaghetti
      • Sweet Dumpling
      • Turban

Example End

Example Description

Type: Best Practice

Simple example of a treeview widget using ARIA CSS selectors to show visual state. Not all browsers support CSS selectors. Further, some browsers will not display background images in high contrast mode.

Keyboard Support

The following keyboard shortcuts are implemented for this example (based on recommended shortcuts specified by the DHTML Style Guide Working Group.):


  • Up: Select the previous visible tree item.

  • Down: Select next visible tree item.

  • Left: Collapse the currently selected parent node if it is expanded. Move to the previous parent node (if possible) when the current parent node is collapsed.

  • Right: Expand the currently selected parent node and move to the first child list item.

  • Enter: Toggle the expanded or collapsed state of the selected parent node.

  • Home: Select the root parent node of the tree.

  • End: Select the last visible node of the tree.

  • Tab: Navigate away from tree.

  • * (asterisk on the numpad): Expand all group nodes.

  • Double-clicking on a parent node will toggle its expanded or collapsed state.

Example Markup

Browser Compatibility

HTML Source Code


<div role="application">

<h2 id="label_1">Foods</h2>
<ul id="tree1" class="tree root-level" role="tree" aria-labelledby="label_1">
   <li id="fruits" class="tree-parent" role="treeitem" tabindex="0" aria-expanded="true"><span>Fruits</span>
      <ul role="group">
         <li id="oranges" role="treeitem" tabindex="-1"><span>Oranges</span></li>
         <li id="pinapples" role="treeitem" tabindex="-1"><span>Pineapples</span></li>
         <li id="apples" class="tree-parent" role="treeitem" tabindex="-1" aria-expanded="false"><span>Apples</span>
            <ul role="group">
               <li id="macintosh" role="treeitem" tabindex="-1"><span>Macintosh</span></li>
               <li id="granny_smith" class="tree-parent" role="treeitem" tabindex="-1" aria-expanded="false"><span>Granny Smith</span>
                  <ul role="group">
                     <li id="Washington" role="treeitem" tabindex="-1"><span>Washington State</span></li>
                     <li id="Michigan" role="treeitem" tabindex="-1"><span>Michigan</span></li>
                     <li id="New_York" role="treeitem" tabindex="-1"><span>New York</span></li>
                  </ul>
               </li>
               <li id="fuji" role="treeitem" tabindex="-1"><span>Fuji</span></li>
            </ul>
         </li>
         <li id="bananas" role="treeitem" tabindex="-1"><span>Bananas</span></li>    
         <li id="pears" role="treeitem" tabindex="-1"><span>Pears</span></li>    
      </ul>
   </li>
   <li id="vegetables" class="tree-parent" role="treeitem" tabindex="-1" aria-expanded="true"><span>Vegetables</span>
      <ul role="group">
         <li id="broccoli" role="treeitem" tabindex="-1"><span>Broccoli</span></li>
         <li id="carrots" role="treeitem" tabindex="-1"><span>Carrots</span></li>
         <li id="lettuce" class="tree-parent" role="treeitem" tabindex="-1" aria-expanded="false"><span>Lettuce</span>
         <ul role="group">
               <li id="romaine" role="treeitem" tabindex="-1"><span>Romaine</span></li>
               <li id="iceberg" role="treeitem" tabindex="-1"><span>Iceberg</span></li>
               <li id="butterhead" role="treeitem" tabindex="-1"><span>Butterhead</span></li>
         </ul>
         </li>
         <li id="spinach" role="treeitem" tabindex="-1"><span>Spinach</span></li>    
         <li id="squash" class="tree-parent" role="treeitem" tabindex="-1" aria-expanded="true"><span>Squash</span>
            <ul role="group">
               <li id="acorn" role="treeitem" tabindex="-1"><span>Acorn</span></li>
               <li id="ambercup" role="treeitem" tabindex="-1"><span>Ambercup</span></li>
               <li id="autumn_cup" role="treeitem" tabindex="-1"><span>Autumn Cup</span></li>
               <li id="hubbard" role="treeitem" tabindex="-1"><span>Hubbard</span></li>
               <li id="kobacha" role="treeitem" tabindex="-1"><span>Kabocha</span></li>
               <li id="butternut" role="treeitem" tabindex="-1"><span>Butternut</span></li>
               <li id="spaghetti" role="treeitem" tabindex="-1"><span>Spaghetti</span></li>
               <li id="sweet_dumpling" role="treeitem" tabindex="-1"><span>Sweet Dumpling</span></li>
               <li id="turban" role="treeitem" tabindex="-1"><span>Turban</span></li>
            </ul>
         </li>
      </ul>
   </li>
</ul>

</div>

CSS Code


h2#label_1 {
margin: .5em 0 !important;
padding: 0 !important;
font-size: 1.6em !important;
}
ul.tree {
  width: 17em;
  font-size: 100% !important;
}
ul.tree, ul.tree ul {
  list-style: none;
  margin: 0 !important;
  padding-left: 20px !important;
  font-weight: normal;
  font-size: 100% !important;
  background-color: #f9f9f9;
  color: black;
}

ul.tree li {
  margin-left: 17px !important;
}

li.tree-focus {
  color: white;
  background-color: black !important;
}

li.tree-parent {
  font-weight: bold;
  margin-left: 0;
  background: url('http://www.oaa-accessibility.org/media/examples/images/contracted.gif') no-repeat 2px 3px;
}
li.tree-parent[aria-expanded="true"] {
  background: url('http://www.oaa-accessibility.org/media/examples/images/expanded.gif') no-repeat 2px 3px;
}

li.tree-focus[aria-expanded="false"] {
  background: url('http://www.oaa-accessibility.org/media/examples/images/contracted-focus.gif') no-repeat 2px 3px;
}
li.tree-focus[aria-expanded="true"] {
  background: url('http://www.oaa-accessibility.org/media/examples/images/expanded-focus.gif') no-repeat 2px 3px;
}

li.tree-parent span {
   margin-left: 17px;
}

ul[aria-hidden="true"] {
   display: none;
}

Javascript Source Code


$(document).ready(function() {

  var treeviewApp = new treeview('tree1');

}); // end ready

//
// Function treeview() is a class constructor for a treeview widget. The widget binds to an
// unordered list. The top-level <ul> must have role='tree'. All list items must have role='treeitem'.
//
// Tree groups must be embedded lists within the listitem that heads the group. the top <ul> of a group
// must have role='group'. aria-expanded is used to indicate whether a group is expanded or collapsed. This
// property must be set on the listitem the encapsulates the group.
//
// parent nodes must be given the class tree-parent.
//
// @param (treeID string) treeID is the html id of the top-level <ul> of the list to bind the widget to
//
// @return N/A
//
function treeview(treeID) {

  // define the object properties
  this.$id = $('#' + treeID);
  this.$items = this.$id.find('li'); // jQuery array of list items
  this.$parents = this.$id.find('.tree-parent'); // jQuery array of parent nodes
   this.$visibleItems = null; // holds a jQuery array of the currently visible items in the tree
   this.$activeItem = null; // holds the jQuery object for the active item

  this.keys = {
            tab:      9,
            enter:    13,
            space:    32,
            pageup:   33,
            pagedown: 34,
            end:      35,
            home:     36,
            left:     37,
            up:       38,
            right:    39,
            down:     40,
            asterisk: 106
   };


  // initialize the treeview
  this.init();

  // bind event handlers
  this.bindHandlers();

} // end treeview() constructor

//
// Function init() is a member function to initialize the treeview widget. It traverses the tree, identifying
// which listitems are headers for groups and applying initial collapsed are expanded styling
//
// @return N/A
//
treeview.prototype.init = function() {

   // insert the header image. Note: this method allows the widget to degrade gracefully
   // if javascript is disabled or there is some other error.

   // If the aria-expanded is false, hide the group and display the collapsed state image
   this.$parents.each(function() {
      if ($(this).attr('aria-expanded') == 'false') {
         $(this).children('ul').attr('aria-hidden', 'true');
      }
      else {
         $(this).children('ul').attr('aria-hidden', 'false');
      }
   });

   this.$visibleItems = this.$id.find('li:visible');

} // end init()

//
// Function expandGroup() is a member function to expand a collapsed group
//
// @param($item object) $item is the jquery id of the parent item of the group
//
// @param(hasFocus boolean) hasFocus is true if the parent has focus, false otherwise
//
// @return N/A
//
treeview.prototype.expandGroup = function($item, hasFocus) {

  var $group = $item.children('ul'); // find the first child ul node

  // display the group
  $group.attr('aria-hidden', 'false');

   // set the aria-expanded property
  $item.attr('aria-expanded', 'true');

   // update the list of visible items
   this.$visibleItems = this.$id.find('li:visible');

} // end expandGroup()

//
// Function collapseGroup() is a member function to collapse an expanded group
//
// @param($item object) $item is the jquery id of the parent item of the group to collapse
//
// @param(hasFocus boolean) hasFocus is true if the parent item has focus, false otherwise
//
// @return N/A
//
treeview.prototype.collapseGroup = function($item, hasFocus) {

  var $group = $item.children('ul');

  // hide the group
  $group.attr('aria-hidden', 'true');

   // update the aria-expanded property
  $item.attr('aria-expanded', 'false');

   // update the list of visible items
   this.$visibleItems = this.$id.find('li:visible');

} // end collapseGroup()

//
// Function toggleGroup() is a member function to toggle the display state of a group
//
// @param($item object) $item is the jquery id of the parent item of the group to toggle
//
// @param(hasFocus boolean) hasFocus is true if the parent item has focus, false otherwise
//
// @return N/A
//
treeview.prototype.toggleGroup = function($item, hasFocus) {

  var $group = $item.children('ul');

  if ($item.attr('aria-expanded') == 'true') {
    // collapse the group
    this.collapseGroup($item, hasFocus);
  }
  else {
    // expand the group
    this.expandGroup($item, hasFocus);
  }

} // end toggleGroup()

//
// Function bindHandlers() is a member function to bind event handlers to the listitems
//
// return N/A
//
treeview.prototype.bindHandlers = function() {

  var thisObj = this;

  // bind a dblclick handler to the parent items
  this.$parents.dblclick(function(e) {
    return thisObj.handleDblClick($(this), e);
  });

  // bind a click handler
  this.$items.click(function(e) {
    return thisObj.handleClick($(this), e);
  });

  // bind a keydown handler
  this.$items.keydown(function(e) {
    return thisObj.handleKeyDown($(this), e);
  });

  // bind a keypress handler
  this.$items.keypress(function(e) {
    return thisObj.handleKeyPress($(this), e);
  });

  // bind a focus handler
  this.$items.focus(function(e) {
    return thisObj.handleFocus($(this), e);
  });

  // bind a blur handler
  this.$items.blur(function(e) {
    return thisObj.handleBlur($(this), e);
  });

   // bind a document click handler
   $(document).click(function(e) {

         if (thisObj.$activeItem != null) {
            // remove the focus styling
            thisObj.$activeItem.removeClass('tree-focus');

            // set activeItem to null
            thisObj.$activeItem = null;
         }

         return true;
   });

} // end bindHandlers()

//
// Function updateStyling() is a member function to update the styling for the tree items
//
// @param ($item object) $item is the jQuery object of the item to highlight
//
// @return N/A
//
treeview.prototype.updateStyling = function($item) {

  // remove the focus and highlighting from the treeview items
  // and remove them from the tab order.
  this.$items.removeClass('tree-focus').attr('tabindex', '-1');


  // apply the focus and styling and place the element in the tab order
  $item.addClass('tree-focus').attr('tabindex', '0');

} // end updateStyling()

//
// Function handleKeyDown() is a member function to process keydown events for the treeview items
//
// @param ($item object) $id is the jQuery id of the item firing the event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns false if consuming event; true if not
//
treeview.prototype.handleKeyDown = function($item, e) {

  var curNdx = this.$visibleItems.index($item);

  if ((e.altKey || e.ctrlKey)
       || (e.shiftKey && e.keyCode != this.keys.tab)) {
    // do nothing
    return true;
  }

  switch (e.keyCode) {
      case this.keys.tab: {
         // set activeItem to null
         this.$activeItem = null;

         // remove the focus styling
         $item.removeClass('tree-focus');

         return true;
      }
    case this.keys.home: { // jump to first item in tree

         // store the active item
         this.$activeItem = this.$parents.first();

         // set focus on the active item
      this.$activeItem.focus();

      e.stopPropagation();
      return false;
    }
    case this.keys.end: { // jump to last visible item

         // store the active item
         this.$activeItem = this.$visibleItems.last();

         // set focus on the active item
      this.$activeItem.focus();

      e.stopPropagation();
      return false;
    }
    case this.keys.enter:
    case this.keys.space: {

      if (!$item.is('.tree-parent')) {
        // do nothing
      }
         else {
            // toggle the child group open or closed
         this.toggleGroup($item, true);
         }

      e.stopPropagation();
      return false;
    }
    case this.keys.left: {
      
      if ($item.is('.tree-parent') && $item.attr('aria-expanded') == 'true') {
            // collapse the group and return

            this.collapseGroup($item, true);
         }
         else {
            // move up to the parent
            var $itemUL = $item.parent();
            var $itemParent = $itemUL.parent();

            // store the active item
            this.$activeItem = $itemParent;

            // set focus on the parent
            this.$activeItem.focus();
         }

      e.stopPropagation();
      return false;
    }
    case this.keys.right: {
      
      if (!$item.is('.tree-parent')) {
        // do nothing

         }
         else if ($item.attr('aria-expanded') == 'false') {
        this.expandGroup($item, true);
      }
         else {
            // move to the first item in the child group
            this.$activeItem = $item.children('ul').children('li').first();

            this.$activeItem.focus();
         }

      e.stopPropagation();
      return false;
    }
    case this.keys.up: {

      if (curNdx > 0) {
        var $prev = this.$visibleItems.eq(curNdx - 1);

            // store the active item
            this.$activeItem = $prev;

        $prev.focus();
      }

      e.stopPropagation();
      return false;
    }
    case this.keys.down: {

      if (curNdx < this.$visibleItems.length - 1) {
        var $next = this.$visibleItems.eq(curNdx + 1);

            // store the active item
            this.$activeItem = $next;

        $next.focus();
      }
      e.stopPropagation();
      return false;
    }
    case this.keys.asterisk: {
      // expand all groups

      var thisObj = this;

         this.$parents.each(function() {
            if (thisObj.$activeItem[0] == $(this)[0]) {
               thisObj.expandGroup($(this), true);
            }
            else {
               thisObj.expandGroup($(this), false);
            }
      });

      e.stopPropagation();
      return false;
    }
  }

  return true;

} // end handleKeyDown

//
// Function handleKeyPress() is a member function to process keypress events for the treeview items
// This function is needed for browsers, such as Opera, that perform window manipulation on kepress events
// rather than keydown. The function simply consumes the event.
//
// @param ($item object) $id is the jQuery id of the parent item firing event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns false if consuming event; true if not
//
treeview.prototype.handleKeyPress = function($item, e) {

  if (e.altKey || e.ctrlKey || e.shiftKey) {
    // do nothing
    return true;
  }

  switch (e.keyCode) {
      case this.keys.tab: {
         return true;
      }
    case this.keys.enter:
    case this.keys.home:
    case this.keys.end:
    case this.keys.left:
    case this.keys.right:
    case this.keys.up:
    case this.keys.down: {
      e.stopPropagation();
      return false;
    }
      default : {
         var chr = String.fromCharCode(e.which);
         var bMatch = false;
         var itemNdx = this.$visibleItems.index($item);
         var itemCnt = this.$visibleItems.length;
         var curNdx = itemNdx + 1;

         // check if the active item was the last one on the list
         if (curNdx == itemCnt) {
            curNdx = 0;
         }

         // Iterate through the menu items (starting from the current item and wrapping) until a match is found
         // or the loop returns to the current menu item
         while (curNdx != itemNdx)  {

            var titleChr = this.$visibleItems.eq(curNdx).find('span').text().charAt(0);

            if (titleChr.toLowerCase() == chr) {
               bMatch = true;
               break;
            }

            curNdx = curNdx+1;

            if (curNdx == itemCnt) {
               // reached the end of the list, start again at the beginning
               curNdx = 0;
            }
         }

         if (bMatch == true) {
            this.$activeItem = this.$visibleItems.eq(curNdx);
            this.$activeItem.focus();
         }

         e.stopPropagation();
         return false;
      }
  }

  return true;

} // end handleKeyPress

//
// Function handleDblClick() is a member function to process double-click events for parent items.
// Double-click expands or collapses a group.
//
// @param ($id object) $item is the jQuery object of the tree parent item firing event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns false if consuming event; true if not
//
treeview.prototype.handleDblClick = function($id, e) {

  if (e.altKey || e.ctrlKey || e.shiftKey) {
    // do nothing
    return true;
  }

   // update the active item
   this.$activeItem = $id;

  // apply the focus highlighting
  this.updateStyling($id);

  // expand or collapse the group
  this.toggleGroup($id, true);

  e.stopPropagation();
  return false;

} // end handleDblClick

//
// Function handleClick() is a member function to process click events.
//
// @param ($id object) $id is the jQuery id of the parent item firing event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns false if consuming event; true if not
//
treeview.prototype.handleClick = function($id, e) {

  if (e.altKey || e.ctrlKey || e.shiftKey) {
    // do nothing
    return true;
  }

   // update the active item
   this.$activeItem = $id;

  // apply the focus highlighting
  this.updateStyling($id);

  e.stopPropagation();
  return false;

} // end handleClick

//
// Function handleFocus() is a member function to process focus events.
//
// @param ($item object) $item is the jQuery id of the parent item firing event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns true
//
treeview.prototype.handleFocus = function($item, e) {

   if (this.$activeItem == null) {
      this.$activeItem = $item;
   }

   this.updateStyling(this.$activeItem);

  return true;

} // end handleFocus

//
// Function handleBlur() is a member function to process blur events.
//
// @param ($id object) $id is the jQuery id of the parent item firing event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns true
//
treeview.prototype.handleBlur = function($id, e) {

  return true;

} // end handleBlur