Tabpanel Navigation Menu
Tabpanel Example 2: Accordian
Happy Time Pizza On-line Ordering System
Keyboard Shortcuts
The following keyboard shortcuts are implemented for this example (based on recommended shortcuts specified by the DHTML Style Guide Working Group.):
If focus is on a tab button:
- Left / Up Arrow: Show the previous tab
- Right / Down Arrow: Show the next tab
- Home: Show the first tab
- End: Show the last tab
- Enter/Space: Expand / Collapse panel
If focus is on an element in a tab panel:
- Control + Up Arrow/Left Arrow: Set focus on the tab button for the currently displayed tab.
- Control + Page Up: Show the previous tab and set focus on its corresponding tab button. Shows the last tab in the panel if current tab is the first one.
- Control + Page Down: Show the next tab and set focus on its corresponding tab button. Shows the first tab in the panel if current tab is the last one.
- Tab: Move focus to next focusable element in panel. If focus is on last focusable element, move focus to first focusable element of next expanded panel or, if no more expanded panels or focusable elements, to first focusable element following tab panel in the page.
- Shift+Tab: The reverse of Tab.
NOTE: Google Chrome does not propagate Control+ Page Up or Control+ Page Down to the web page when multiple tabs are open. This key combination will not function correctly in that case.
ARIA Roles and Properties
- NOTE: The selected tab panel is maintained by the browser and communicated to assistive technology through accessibility APIs. The browser communicates this information based on which
tab in the tablist had focus last. A aria-labelledby attribute needs to be defined on every tabpanel that points to the corresponding tab for the selected state to be calculated by the browser correctly.
-
Roles:
role="application"
role="tablist"
role="tab"
role="tabpanel"
- States and properties:
aria-labelledby
aria-hidden
HTML Source Code
Show HTML Source Code: tabpanel2.inc
<div role="application">
<h2>Happy Time Pizza On-line Ordering System</h2>
<form>
<div id="accordian1" class="tabpanel">
<h3 id="tab1" class="tab accordian selected" aria-controls="panel1" role="tab" tabindex="0">
<img src="images/expanded.gif" alt="expanded" />
Crust
</h3>
<div id="panel1" class="panel accordian selected" aria-labeledby="tab1" role="tabpanel">
<h3>Select Crust</h3>
<ul class="controlList">
<li><label><input type="radio" name="crust" value="crust1" />Deep Dish</label></li>
<li><label><input type="radio" name="crust" value="crust2" checked="checked" />Thick and cheesy</label></li>
<li><label><input type="radio" name="crust" value="crust3" />Thick and spicy</label></li>
<li><label><input type="radio" name="crust" value="crust4" />Thin</label></li>
</ul>
</div>
<h3 id="tab2" class="tab accordian" aria-controls="panel2" role="tab" tabindex="-1">
<img src="images/contracted.gif" alt="collapsed" />
Veggies
</h3>
<div id="panel2" class="panel accordian" aria-labeledby="tab2" role="tabpanel">
<h3>Select Vegetables</h3>
<ul class="controlList">
<li><label><input type="checkbox" name="veg" value="black olives" />Black Olives</label></li>
<li><label><input type="checkbox" name="veg" value="green olives" />Green Olives</label></li>
<li><label><input type="checkbox" name="veg" value="green peppers" />Green Peppers</label></li>
<li><label><input type="checkbox" name="veg" value="mushrooms" />Mushrooms</label></li>
<li><label><input type="checkbox" name="veg" value="onions" />Onions</label></li>
<li><label><input type="checkbox" name="veg" value="pineapple" />Pineapple</label></li>
</ul>
</div>
<h3 id="tab3" class="tab accordian" aria-controls="panel3" role="tab" tabindex="-1">
<img src="images/contracted.gif" alt="collapsed" />
Carnivore
</h3>
<div id="panel3" class="panel accordian" aria-labeledby="tab3" role="tabpanel">
<h3>Select Carnivore Options</h3>
<ul class="controlList">
<li><label><input type="checkbox" name="meat" value="pepperoni" />Pepperoni</label></li>
<li><label><input type="checkbox" name="meat" value="sausage" />Italian Sausage</label></li>
<li><label><input type="checkbox" name="meat" value="ham" />Ham</label></li>
<li><label><input type="checkbox" name="meat" value="hamburger" />Hamburger</label></li>
</ul>
</div>
<h3 id="tab4" class="tab accordian" aria-controls="panel4" role="tab" tabindex="-1">
<img src="images/contracted.gif" alt="collapsed" />
Delivery
</h3>
<div id="panel4" class="panel accordian" aria-labeledby="tab4" role="tabpanel">
<h3>Select Delivery Method</h3>
<ul class="controlList">
<li><label><input type="radio" name="delivery" value="delivery1" checked="checked" />Delivery</label></li>
<li><label><input type="radio" name="delivery" value="delivery2" />Eat in</label></li>
<li><label><input type="radio" name="delivery" value="delivery3" />Carry out</label></li>
<li><label><input type="radio" name="delivery" value="delivery4" />Overnight mail</label></li>
</ul>
</div>
</div>
</form>
</div>
Javascript Source Code
Show Javascript Source Code: tabpanel2.js
<script type="text/javascript">
$(document).ready(function() {
var panel1 = new tabpanel("accordian1", true);
});
//
// keyCodes() is an object to contain keycodes needed for the application
//
function keyCodes() {
// Define values for keycodes
this.tab = 9;
this.enter = 13;
this.esc = 27;
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;
} // end keyCodes
//
// tabpanel() is a class constructor to create a ARIA-enabled tab panel widget.
//
// @param (id string) id is the id of the div containing the tab panel.
//
// @param (accordian boolean) accordian is true if the tab panel should operate
// as an accordian; false if a tab panel
//
// @return N/A
//
// Usage: Requires a div container and children as follows:
//
// 1. tabs/accordian headers have class 'tab'
//
// 2. panels are divs with class 'panel'
//
function tabpanel(id, accordian) {
// define the class properties
this.panel_id = id; // store the id of the containing div
this.accordian = accordian; // true if this is an accordian control
this.$panel = $('#' + id); // store the jQuery object for the panel
this.keys = new keyCodes(); // keycodes needed for event handlers
this.$tabs = this.$panel.find('.tab'); // Array of panel tabs.
this.$panels = this.$panel.children('.panel'); // Array of panel.
// Bind event handlers
this.bindHandlers();
// Initialize the tab panel
this.init();
} // end tabpanel() constructor
//
// Function init() is a member function to initialize the tab/accordian panel. Hides all panels. If a tab
// has the class 'selected', makes that panel visible; otherwise, makes first panel visible.
//
// @return N/A
//
tabpanel.prototype.init = function() {
var $tab; // the selected tab - if one is selected
// add aria attributes to the panel container
this.$panel.attr('aria-multiselectable', this.accordian);
// add aria attributes to the panels
this.$panels.attr('aria-hidden', 'true');
// hide all the panels
this.$panels.hide();
// get the selected tab
$tab = this.$tabs.filter('.selected');
if ($tab == undefined) {
$tab = this.$tabs.first();
$tab.addClass('selected');
}
// show the panel that the selected tab controls and set aria-hidden to false
this.$panel.find('#' + $tab.attr('aria-controls')).show().attr('aria-hidden', 'false');
} // end init()
//
// Function switchTabs() is a member function to give focus to a new tab or accordian header.
// If it's a tab panel, the currently displayed panel is hidden and the panel associated with the new tab
// is displayed.
//
// @param ($curTab obj) $curTab is the jQuery object of the currently selected tab
//
// @param ($newTab obj) $newTab is the jQuery object of new tab to switch to
//
// @param (activate boolean) activate is true if focus should be set on an element in the panel; false if on tab
//
// @return N/A
//
tabpanel.prototype.switchTabs = function($curTab, $newTab) {
// Remove the highlighting from the current tab
$curTab.removeClass('selected');
$curTab.removeClass('focus');
// remove tab from the tab order
$curTab.attr('tabindex', '-1');
// update the aria attributes
// Highlight the new tab
$newTab.addClass('selected');
// If this is a tab panel, swap displayed tabs
if (this.accordian == false) {
// hide the current tab panel and set aria-hidden to true
this.$panel.find('#' + $curTab.attr('aria-controls')).hide().attr('aria-hidden', 'true');
// show the new tab panel and set aria-hidden to false
this.$panel.find('#' + $newTab.attr('aria-controls')).show().attr('aria-hidden', 'false');
// get new list of focusable elements
this.$focusable.length = 0;
this.$panels.find(':focusable');
}
// Make new tab navigable
$newTab.attr('tabindex', '0');
// give the new tab focus
$newTab.focus();
} // end switchTabs()
//
// Function togglePanel() is a member function to display or hide the panel
// associated with an accordian header. Function also binds a keydown handler to the focusable items
// in the panel when expanding and unbinds the handlers when collapsing.
//
// @param ($tab obj) $tab is the jQuery object of the currently selected tab
//
// @return N/A
//
tabpanel.prototype.togglePanel = function($tab) {
$panel = this.$panel.find('#' + $tab.attr('aria-controls'));
if ($panel.attr('aria-hidden') == 'true') {
$panel.attr('aria-hidden', 'false');
$panel.slideDown(100);
$tab.find('img').attr('src', 'images/expanded.gif').attr('alt', 'expanded');
}
else {
$panel.attr('aria-hidden', 'true');
$panel.slideUp(100);
$tab.find('img').attr('src', 'images/contracted.gif').attr('alt', 'collapsed');
}
} // end togglePanel()
//
// Function bindHandlers() is a member function to bind event handlers for the tabs
//
// @return N/A
//
tabpanel.prototype.bindHandlers = function() {
var thisObj = this; // Store the this pointer for reference
//////////////////////////////
// Bind handlers for the tabs / accordian headers
// bind a tab keydown handler
this.$tabs.keydown(function(e) {
return thisObj.handleTabKeyDown($(this), e);
});
// bind a tab keypress handler
this.$tabs.keypress(function(e) {
return thisObj.handleTabKeyPress($(this), e);
});
// bind a tab click handler
this.$tabs.click(function(e) {
return thisObj.handleTabClick($(this), e);
});
// bind a tab focus handler
this.$tabs.focus(function(e) {
return thisObj.handleTabFocus($(this), e);
});
// bind a tab blur handler
this.$tabs.blur(function(e) {
return thisObj.handleTabBlur($(this), e);
});
/////////////////////////////
// Bind handlers for the panels
// bind a keydown handlers for the panel focusable elements
this.$panels.keydown(function(e) {
return thisObj.handlePanelKeyDown($(this), e);
});
// bind a keypress handler for the panel
this.$panels.keypress(function(e) {
return thisObj.handlePanelKeyPress($(this), e);
});
} // end bindHandlers()
//
// Function handleTabKeyDown() is a member function to process keydown events for a tab
//
// @param ($tab obj) $tab is the jquery object of the tab being processed
//
// @paran (e obj) e is the associated event object
//
// @return (boolean) Returns true if propagating; false if consuming event
//
tabpanel.prototype.handleTabKeyDown = function($tab, e) {
if (e.altKey) {
// do nothing
return true;
}
switch (e.keyCode) {
case this.keys.enter:
case this.keys.space: {
// Only process if this is an accordian widget
if (this.accordian == true) {
// display or collapse the panel
this.togglePanel($tab);
e.stopPropagation();
return false;
}
return true;
}
case this.keys.left:
case this.keys.up: {
var thisObj = this;
var $prevTab; // holds jQuery object of tab from previous pass
var $newTab; // the new tab to switch to
if (e.ctrlKey) {
// Ctrl+arrow moves focus from panel content to the open
// tab/accordian header.
}
else {
var curNdx = this.$tabs.index($tab);
if (curNdx == 0) {
// tab is the first one:
// set newTab to last tab
$newTab = this.$tabs.last();
}
else {
// set newTab to previous
$newTab = this.$tabs.eq(curNdx - 1);
}
// switch to the new tab
this.switchTabs($tab, $newTab);
}
e.stopPropagation();
return false;
}
case this.keys.right:
case this.keys.down: {
var thisObj = this;
var foundTab = false; // set to true when current tab found in array
var $newTab; // the new tab to switch to
var curNdx = this.$tabs.index($tab);
if (curNdx == this.$tabs.last().index()) {
// tab is the last one:
// set newTab to first tab
$newTab = this.$tabs.first();
}
else {
// set newTab to next tab
$newTab = this.$tabs.eq(curNdx + 1);
}
// switch to the new tab
this.switchTabs($tab, $newTab);
e.stopPropagation();
return false;
}
case this.keys.home: {
// switch to the first tab
this.switchTabs($tab, this.$tabs.first());
e.stopPropagation();
return false;
}
case this.keys.end: {
// switch to the last tab
this.switchTabs($tab, this.$tabs.last());
e.stopPropagation();
return false;
}
}
} // end handleTabKeyDown()
//
// Function handleTabKeyPress() is a member function to process keypress events for a tab.
//
//
// @param ($tab obj) $tab is the jquery object of the tab being processed
//
// @paran (e obj) e is the associated event object
//
// @return (boolean) Returns true if propagating; false if consuming event
//
tabpanel.prototype.handleTabKeyPress = function($tab, e) {
if (e.altKey) {
// do nothing
return true;
}
switch (e.keyCode) {
case this.keys.enter:
case this.keys.space:
case this.keys.left:
case this.keys.up:
case this.keys.right:
case this.keys.down:
case this.keys.home:
case this.keys.end: {
e.stopPropagation();
return false;
}
case this.keys.pageup:
case this.keys.pagedown: {
// The tab keypress handler must consume pageup and pagedown
// keypresses to prevent Firefox from switching tabs
// on ctrl+pageup and ctrl+pagedown
if (!e.ctrlKey) {
return true;
}
e.stopPropagation();
return false;
}
}
return true;
} // end handleTabKeyPress()
//
// Function handleTabClick() is a member function to process click events for tabs
//
// @param ($tab object) $tab is the jQuery object of the tab being processed
//
// @paran (e object) e is the associated event object
//
// @return (boolean) returns true
//
tabpanel.prototype.handleTabClick = function($tab, e) {
// remove all tabs from the tab order
this.$tabs.attr('tabindex', '-1');
// make clicked tab navigable
$tab.attr('tabindex', '0');
// Expand the new panel
this.togglePanel($tab);
e.stopPropagation();
return false;
} // end handleTabClick()
//
// Function handleTabFocus() is a member function to process focus events for tabs
//
// @param ($tab object) $tab is the jQuery object of the tab being processed
//
// @paran (e object) e is the associated event object
//
// @return (boolean) returns true
//
tabpanel.prototype.handleTabFocus = function($tab, e) {
// Add the focus class to the tab
$tab.addClass('focus');
return true;
} // end handleTabFocus()
//
// Function handleTabBlur() is a member function to process blur events for tabs
//
// @param ($tab object) $tab is the jQuery object of the tab being processed
//
// @paran (e object) e is the associated event object
//
// @return (boolean) returns true
//
tabpanel.prototype.handleTabBlur = function($tab, e) {
// Remove the focus class to the tab
$tab.removeClass('focus');
return true;
} // end handleTabBlur()
/////////////////////////////////////////////////////////
// Panel Event handlers
//
//
// Function handlePanelKeyDown() is a member function to process keydown events for a panel
//
// @param ($panel obj) $panel is the jquery object of the panel being processed
//
// @paran (e obj) e is the associated event object
//
// @return (boolean) Returns true if propagating; false if consuming event
//
tabpanel.prototype.handlePanelKeyDown = function($panel, e) {
if (e.altKey) {
// do nothing
return true;
}
switch (e.keyCode) {
case this.keys.tab: {
var $focusable = $panel.find(':focusable');
var curNdx = $focusable.index($(e.target));
var panelNdx = this.$panels.index($panel);
var numPanels = this.$panels.length
if (e.shiftKey) {
// if this is the first focusable item in the panel
// find the preceding expanded panel (if any) that has
// focusable items and set focus to the last one in that
// panel. If there is no preceding panel or no focusable items
// do not process.
if (curNdx == 0 && panelNdx > 0) {
// Iterate through previous panels until we find one that
// is expanded and has focusable elements
//
for (var ndx = panelNdx - 1; ndx >= 0; ndx--) {
var $prevPanel = this.$panels.eq(ndx);
// get the focusable items in the panel
$focusable.length = 0;
$focusable = $prevPanel.find(':focusable');
if ($focusable.length > 0) {
// there are focusable items in the panel.
// Set focus to the last item.
$focusable.last().focus();
e.stopPropagation;
return false;
}
}
}
}
else if (panelNdx < numPanels) {
// if this is the last focusable item in the panel
// find the nearest following expanded panel (if any) that has
// focusable items and set focus to the first one in that
// panel. If there is no preceding panel or no focusable items
// do not process.
if (curNdx == $focusable.length - 1) {
// Iterate through following panels until we find one that
// is expanded and has focusable elements
//
for (var ndx = panelNdx + 1; ndx < numPanels; ndx++) {
var $nextPanel = this.$panels.eq(ndx);
// get the focusable items in the panel
$focusable.length = 0;
$focusable = $nextPanel.find(':focusable');
if ($focusable.length > 0) {
// there are focusable items in the panel.
// Set focus to the first item.
$focusable.first().focus();
e.stopPropagation;
return false;
}
}
}
}
break;
}
case this.keys.left:
case this.keys.up: {
if (!e.ctrlKey) {
// do not process
return true;
}
// get the jQuery object of the tab
var $tab = $('#' + $panel.attr('aria-labeledby'));
// Move focus to the tab
$tab.focus();
e.stopPropagation();
return false;
}
case this.keys.pageup: {
var $newTab;
if (!e.ctrlKey) {
// do not process
return true;
}
// get the jQuery object of the tab
var $tab = this.$tabs.filter('.selected');
// get the index of the tab in the tab list
var curNdx = this.$tabs.index($tab);
if (curNdx == 0) {
// this is the first tab, set focus on the last one
$newTab = this.$tabs.last();
}
else {
// set focus on the previous tab
$newTab = this.$tabs.eq(curNdx - 1);
}
// switch to the new tab
this.switchTabs($tab, $newTab);
e.stopPropagation();
e.preventDefault();
return false;
}
case this.keys.pagedown: {
var $newTab;
if (!e.ctrlKey) {
// do not process
return true;
}
// get the jQuery object of the tab
var $tab = $('#' + $panel.attr('aria-labeledby'));
// get the index of the tab in the tab list
var curNdx = this.$tabs.index($tab);
if (curNdx == this.$tabs.last().index()) {
// this is the last tab, set focus on the first one
$newTab = this.$tabs.first();
}
else {
// set focus on the next tab
$newTab = this.$tabs.eq(curNdx + 1);
}
// switch to the new tab
this.switchTabs($tab, $newTab);
e.stopPropagation();
e.preventDefault();
return false;
}
}
return true;
} // end handlePanelKeyDown()
//
// Function handlePanelKeyPress() is a member function to process keypress events for a panel
//
// @param ($panel obj) $panel is the jquery object of the panel being processed
//
// @paran (e obj) e is the associated event object
//
// @return (boolean) Returns true if propagating; false if consuming event
//
tabpanel.prototype.handlePanelKeyPress = function($panel, e) {
if (e.altKey) {
// do nothing
return true;
}
if (e.ctrlKey && (e.keyCode == this.keys.pageup || e.keyCode == this.keys.pagedown)) {
e.stopPropagation();
e.preventDefault();
return false;
}
switch (e.keyCode) {
case this.keys.esc: {
e.stopPropagation();
e.preventDefault();
return false;
}
}
return true;
} // end handlePanelKeyPress()
// focusable is a small jQuery extension to add a :focusable selector. It is used to
// get a list of all focusable elements in a panel. Credit to ajpiano on the jQuery forums.
//
$.extend($.expr[':'], {
focusable: function(element) {
var nodeName = element.nodeName.toLowerCase();
var tabIndex = $(element).attr('tabindex');
// the element and all of its ancestors must be visible
if (($(element)[(nodeName == 'area' ? 'parents' : 'closest')](':hidden').length) == true) {
return false;
}
// If tabindex is defined, its value must be greater than 0
if (!isNaN(tabIndex) && tabIndex < 0) {
return false;
}
// if the element is a standard form control, it must not be disabled
if (/input|select|textarea|button|object/.test(nodeName) == true) {
return !element.disabled;
}
// if the element is a link, href must be defined
if ((nodeName == 'a' || nodeName == 'area') == true) {
return (element.href.length > 0);
}
// this is some other page element that is not normally focusable.
return false;
}
});
</script>
CSS Source Code
Show CSS Source Code: tabpanel2.css
<style type="text/css">
.tabpanel {
margin: 20px;
padding: 0;
}
.tablist {
margin: 0 0px;
padding: 0;
list-style: none;
}
.tab {
margin: .2em 1px 0 0;
padding: 10px;
height: 1em;
font-weight: bold;
background-color: #ec9;
border: 1px solid black;
-webkit-border-radius-topright: 5px;
-webkit-border-radius-topleft: 5px;
-moz-border-radius-topright: 5px;
-moz-border-radius-topleft: 5px;
border-radius-topright: 5px;
border-radius-topleft: 5px;
float: left;
}
.panel {
clear: both;
margin: 0 0 0 0;
padding: 10px;
width: 600px;
border: 1px solid black;
-webkit-border-radius-topright: 10px;
-webkit-border-radius-bottomleft: 10px;
-webkit-border-radius-bottomright: 10px;
-moz-border-radius-topright: 10px;
-moz-border-radius-bottomleft: 10px;
-moz-border-radius-bottomright: 10px;
border-radius-topright: 10px;
border-radius-bottomleft: 10px;
border-radius-bottomright: 10px;
}
ul.controlList {
list-style-type: none;
}
li.selected {
border-bottom: 1px solid white;
}
.focus {
color: black;
border-top: 2px solid black;
border-bottom: 2px solid black;
background-color: #fff;
margin-top: 0;
}
.accordian {
margin: 0;
float: none;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
width: 600px;
}
.hidden {
position: absolute;
left: -300em;
top: -30em;
}
</style>
W3C Validation of HTML5