Tree Navigation Menu
Tree View Example 2: Using aria-owns
Animal, Mineral, or Vegetable
Birds
Chihuahua
Italian Greyhound
Japanese Chin
Beagle
Cocker Spaniel
Pit Bull
Afghan
Great Dane
Mastiff
Keyboard Shortcuts
- 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.
ARIA Roles and Properties
-
Roles:
role="application"
role="tree"
role="treeitem"
role="group"
- States and properties:
aria-labelledby
aria-expanded
HTML Source Code
Show HTML Source Code: tree2.inc
<div role="application">
<h2 id="label_1">Animal, Mineral, or Vegetable</h2>
<div id="tree1" class="tree" role="tree" tabindex="-1">
<div id="animals" class="groupHeader" role="presentation" aria-owns="animalGroup" aria-expanded="true">
<img class="headerImg" role="presentation" tabindex="-1" src="images/treeExpanded.gif" />
<span role="treeitem" tabindex="0">Animals</span>
</div>
<div id="animalGroup" class="group" role="group">
<div id="birds" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">Birds</span>
</div>
<div id="cats" class="groupHeader" role="presentation" aria-owns="catGroup" aria-expanded="false">
<img class="headerImg" role="presentation" tabindex="-1" src="images/treeContracted.gif" />
<span role="treeitem" tabindex="0">Cats</span>
</div>
<div id="catGroup" class="group" role="group">
<div id="siamese" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">Siamese</span>
</div>
<div id="tabby" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">Tabby</span>
</div>
</div>
<div id="dogs" class="groupHeader" role="presentation" aria-owns="dogGroup" aria-expanded="true">
<img class="headerImg" role="presentation" tabindex="-1" src="images/treeExpanded.gif" />
<span role="treeitem" tabindex="0">Dogs</span>
</div>
<div id="dogGroup" class="group" role="group">
<div id="smallBreeds" class="groupHeader" role="presentation" aria-owns="smallBreedGroup" aria-expanded="true">
<img class="headerImg" role="presentation" tabindex="-1" src="images/treeExpanded.gif" />
<span role="treeitem" tabindex="0">Small Breeds</span>
</div>
<div id="smallBreedGroup" class="group" role="group">
<div id="chihuahua" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">Chihuahua</span>
</div>
<div id="italian_greyhound" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">Italian Greyhound</span>
</div>
<div id="Japanese_chin" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">Japanese Chin</span>
</div>
</div>
<div id="mediumBreeds" class="groupHeader" role="presentation" aria-owns="mediumBreedGroup" aria-expanded="false">
<img class="headerImg" role="presentation" tabindex="-1" src="images/treeContracted.gif" />
<span role="treeitem" tabindex="0">Medium Breeds</span>
</div>
<div id="mediumBreedGroup" class="group" role="group">
<div id="beagle" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">Beagle</span>
</div>
<div id="cocker_spaniel" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">Cocker Spaniel</span>
</div>
<div id="pit_bull" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">Pit Bull</span>
</div>
</div>
<div id="largeBreeds" class="groupHeader" role="presentation" aria-owns="largeBreedGroup" aria-expanded="false">
<img class="headerImg" role="presentation" tabindex="-1" src="images/treeContracted.gif" />
<span role="treeitem" tabindex="0">Large Breeds</span>
</div>
<div id="largeBreedGroup" class="group" role="group">
<div id="afghan" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">Afghan</span>
</div>
<div id="great_dane" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">Great Dane</span>
</div>
<div id="mastiff" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">Mastiff</span>
</div>
</div>
</div>
</div>
<div id="minerals" class="groupHeader" role="presentation" aria-owns="mineralGroup" aria-expanded="true">
<img class="headerImg" role="presentation" tabindex="-1" src="images/treeExpanded.gif" />
<span role="treeitem" tabindex="0">Minerals</span>
</div>
<div id="mineralGroup" class="group" role="group">
<div id="zinc" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">Zinc</span>
</div>
<div id="gold" class="groupHeader" role="presentation" aria-owns="goldGroup" aria-expanded="false">
<img class="headerImg" role="presentation" tabindex="-1" src="images/treeContracted.gif" />
<span role="treeitem" tabindex="0">Gold</span>
</div>
<div id="goldGroup" class="group" role="group">
<div id="yellow_gold" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">Yellow Gold</span>
</div>
<div id="white_gold" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">White Gold</span>
</div>
</div>
<div id="silver" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">Silver</span>
</div>
</div>
<div id="vegetables" class="groupHeader" role="presentation" aria-owns="vegetableGroup" aria-expanded="true">
<img class="headerImg" role="presentation" tabindex="-1" src="images/treeExpanded.gif" />
<span role="treeitem" tabindex="0">Vegetables</span>
</div>
<div id="vegetableGroup" class="group" role="group">
<div id="carrot" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">Carrot</span>
</div>
<div id="tomato" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">Tomato</span>
</div>
<div id="lettuce" class="treeitem" role="presentation">
<span role="treeitem" tabindex="-1">Lettuce</span>
</div>
</div>
</div>
</div>
Javascript Source Code
Show Javascript Source Code: tree2.js
<script type="text/javascript">
var g_focusHandled = false; // set to true if focus is handled
//
// Function resetFocusFlag() is a callback to reset the focusHandled flag. This is
// called by the timer set in the treeview focus handler
//
function resetFocusFlag() {
g_focusHandled = false;
}
$(document).ready(function() {
var treeviewApp = new treeview('tree1');
}); // end ready
//
// Function keyCodes() is an object to define keycodes for the application
//
function keyCodes() {
this.enter = 13;
this.space = 32;
this.pageup = 33;
this.pagedown = 34;
this.end = 35;
this.home = 36;
this.left = 37;
this.up = 38;
this.right = 39;
this.down = 40;
this.asterisk = 106;
} // end keyCodes()
//
// 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.
//
// @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.$listitems = this.$id.find('[role=treeitem]'); // an array of list items
this.$groups = this.$id.find('[class=groupHeader]'); // an array of the group headers
this.$visibleItems = undefined; // an array of currently visible listitems (including headers)
this.focusHandled = false; // Set to true when a focus event is handled and reset after a small delay
this.keys = new keyCodes();
// 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() {
var thisObj = this;
// iterate through the tree and apply the styling to the group headers
this.$groups.each (function(index) {
var $group = $('#' + $(this).attr('aria-owns'));
// If the aria-expanded is false, hide the group and display the collapsed state image
if ($(this).attr('aria-expanded') == 'false') {
$group.hide();
$(this).find('img').attr('src', 'images/treeContracted.gif');
}
});
// create the initial visible item array
this.$visibleItems = this.$listitems.filter(':visible');
} // end init()
//
// Function expandGroup() is a member function to expand a collapsed group
//
// @param($id object) $id is the jquery id of the group header of the group to expand
//
// @param(focus boolean) focus is true if the group header has focus, false otherwise
//
// @return N/A
//
treeview.prototype.expandGroup = function($id, focus) {
var $group = $('#' + $id.attr('aria-owns'));
// expand the group
$group.show();
$id.attr('aria-expanded', 'true');
if (focus == true) {
$id.children('img').attr('src', 'images/treeExpandedFocus.gif');
}
else {
$id.children('img').attr('src', 'images/treeExpanded.gif');
}
// refresh the list of visible items
this.$visibleItems = this.$listitems.filter(':visible');
} // end expandGroup()
//
// Function collapseGroup() is a member function to collapse an expanded group
//
// @param($id object) $id is the jquery id of the group header of the group to collapse
//
// @param(focus boolean) focus is true if the group header has focus, false otherwise
//
// @return N/A
//
treeview.prototype.collapseGroup = function($id, focus) {
var $group = $('#' + $id.attr('aria-owns'));
// collapse the group
$group.hide();
$id.attr('aria-expanded', 'false');
if (focus == true) {
$id.children('img').attr('src', 'images/treeContractedFocus.gif');
}
else {
$id.children('img').attr('src', 'images/treeContracted.gif');
}
// refresh the list of visible items
this.$visibleItems = this.$listitems.filter(':visible');
} // end collapseGroup()
//
// Function toggleGroup() is a member function to toggle the display state of a group
//
// @param($id object) $id is the jquery id of the group header of the group to toggle
//
// @param(focus boolean) focus is true if the group header has focus, false otherwise
//
// @return N/A
//
treeview.prototype.toggleGroup = function($id, focus) {
if ($id.attr('aria-expanded') == 'true') {
// collapse the group
this.collapseGroup($id, focus);
}
else {
// expand the group
this.expandGroup($id, focus);
}
} // 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 group headers
this.$groups.dblclick(function(e) {
return thisObj.handleDblClick($(this), e);
});
// bind a click handler
this.$listitems.click(function(e) {
return thisObj.handleClick($(this), e);
});
// bind a keydown handler
this.$listitems.keydown(function(e) {
return thisObj.handleKeyDown($(this), e);
});
// bind a keypress handler
this.$listitems.keypress(function(e) {
return thisObj.handleKeyPress($(this), e);
});
// bind a focus handler
this.$listitems.focus(function(e) {
return thisObj.handleFocus($(this), e);
});
// bind a blur handler
this.$listitems.blur(function(e) {
return thisObj.handleBlur($(this), e);
});
} // end bindHandlers()
//
// Function doHighlight() is a member function to remove the highlighting from
// other treeview items and apply it to the passed element
//
// @param ($id object) $id is the jQuery object of the element to highlight
//
// @param (isHeader boolean) isHeader is true if $id points to a group header
//
// @return N/A
//
treeview.prototype.doHighlight = function($id, isHeader) {
// remove the focus highlighting from the treeview items
// and remove them from the tab order.
this.$listitems.removeClass('focus').attr('tabindex', '-1');
// remove the focus image from group headers
this.$groups.each(function() {
// add the focus image
if ($(this).attr('aria-expanded') == 'true') {
$(this).children('img').attr('src', 'images/treeExpanded.gif');
}
else {
$(this).children('img').attr('src', 'images/treeContracted.gif');
}
});
if (isHeader == true) {
// add the focus image
if ($id.attr('aria-expanded') == 'true') {
$id.children('img').attr('src', 'images/treeExpandedFocus.gif');
}
else {
$id.children('img').attr('src', 'images/treeContractedFocus.gif');
}
}
// apply the focus highlighting and place the element in the tab order
$id.children('span').addClass('focus').attr('tabindex', '0');
} // end doHighlight()
//
// Function handleKeyDown() is a member function to process keydown events for the treeview items
//
// @param ($id object) $id is the jQuery id of the group header firing event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns false if consuming event; true if not
//
treeview.prototype.handleKeyDown = function($id, e) {
var $itemParent = $id.parent();
var curNdx = this.$visibleItems.index($id);
var isHeader = false;
if (e.altKey || e.ctrlKey || e.shiftKey) {
// do nothing
return true;
}
switch (e.keyCode) {
case this.keys.home: {
this.$listitems.first().focus();
e.stopPropagation();
return false;
}
case this.keys.end: {
this.$visibleItems.last().focus();
e.stopPropagation();
return false;
}
case this.keys.enter: {
if ($itemParent.hasClass('groupHeader') == false) {
// do nothing
return true;
}
this.toggleGroup($itemParent, true);
e.stopPropagation();
return false;
}
case this.keys.left: {
if ($itemParent.hasClass('groupHeader') == false) {
// do nothing
return true;
}
if ($itemParent.attr('aria-expanded') == 'true') {
this.collapseGroup($itemParent, true);
}
else {
// move to previous group header
var prevNdx = this.$groups.index($itemParent) - 1;
if (prevNdx >= 0) {
var groupID = $itemParent.parent().attr('id');
var $prev = this.$groups.filter('[aria-owns=' + groupID + ']');
$prev.children('span').focus();
}
}
e.stopPropagation();
return false;
}
case this.keys.right: {
if ($itemParent.hasClass('groupHeader') == false) {
// do nothing
return true;
}
if ($itemParent.attr('aria-expanded') == 'false') {
this.expandGroup($itemParent, true);
}
e.stopPropagation();
return false;
}
case this.keys.up: {
if (curNdx > 0) {
var $prev = this.$visibleItems.eq(curNdx - 1);
$prev.focus();
}
e.stopPropagation();
return false;
}
case this.keys.down: {
if (curNdx < this.$visibleItems.length - 1) {
var $next = this.$visibleItems.eq(curNdx + 1);
$next.focus();
}
e.stopPropagation();
return false;
}
case this.keys.asterisk: {
// expand all groups
var thisObj = this;
this.$groups.each(function() {
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 ($id object) $id is the jQuery id of the group header 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($id, e) {
if (e.altKey || e.ctrlKey || e.shiftKey) {
// do nothing
return true;
}
switch (e.keyCode) {
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;
}
}
return true;
} // end handleKeyPress
//
// Function handleDblClick() is a member function to process double-click events for group headers.
// Double-click expands or collapses a group.
//
// @param ($id object) $id is the jQuery id of the group header 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;
}
// apply the focus highlighting
this.doHighlight($id, true);
// 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 group header 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) {
$itemParent = $id.parent();
if (e.altKey || e.ctrlKey || e.shiftKey) {
// do nothing
return true;
}
if (this.$groups.index($itemParent) == -1) {
// this is a list item
// apply the focus highlighting
this.doHighlight($itemParent, false);
}
else {
// this is a group header
// apply the focus highlighting
this.doHighlight($itemParent, true);
}
e.stopPropagation();
return false;
} // end handleClick
//
// Function handleFocus() is a member function to process focus events.
//
// @param ($id object) $id is the jQuery id of the group header firing event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns true
//
treeview.prototype.handleFocus = function($id, e) {
$itemParent = $id.parent();
// only process the event if the focusHandled flag is false
if (g_focusHandled == false) {
// prevent encapsulating group headers from responding to
// the focus event.
g_focusHandled = true;
if (this.$groups.index($itemParent) == -1) {
this.doHighlight($itemParent, false);
}
else {
// this is a group header
this.doHighlight($itemParent, true);
}
window.setTimeout(resetFocusFlag, 10);
}
return true;
} // end handleFocus
//
// Function handleBlur() is a member function to process blur events.
//
// @param ($id object) $id is the jQuery id of the group header firing event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns true
//
treeview.prototype.handleBlur = function($id, e) {
$itemParent = $id.parent();
if (this.$groups.index($itemParent) != -1) {
// this is a group header
// remove the focus image
if ($itemParent.attr('aria-expanded') == 'true') {
$itemParent.children('img').attr('src', 'images/treeExpanded.gif');
}
else {
$itemParent.children('img').attr('src', 'images/treeContracted.gif');
}
}
// remove the focus highlighting
$id.removeClass('focus');
return true;
} // end handleBlur
</script>
CSS Source Code
Show CSS Source Code: tree2.css
<style type="text/css">
div.tree {
margin-left: 20px;
padding: 0;
width: 15em;
}
div.group {
padding-left: 22px;
}
div.treeitem {
padding-left: 22px;
}
div.groupHeader {
font-weight: bold;
}
img.headerImg {
margin-right: 5px;
}
span.focus {
color: white;
background: black;
}
</style>
W3C Validation of HTML5