Accessibility Examples
Example Start
Happy Time Pizza On-line Ordering System
Example End
Example Description
Type: Best Practice
Simple example of a tab Panel widget.
Keyboard Support
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
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.
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.
Example Markup
- ARIA 1.0: [aria-controls]
- ARIA 1.0: [aria-hidden]
- ARIA 1.0: [aria-labelledby]
- ARIA 1.0: [aria-selected]
- ARIA 1.0: [role="tab"]
- ARIA 1.0: [role="tablist"]
- ARIA 1.0: [role="tabpanel"]
Browser Compatibility
- osx: Firefox 3.6 (C)
- osx: Opera 11.0 (C)
- osx: Safari 5.0 (C)
- win: Firefox 3.6 (C)
- win: Internet Explorer 8.0 (C)
- win: Opera 11.0 (C)
- win: Safari 5.0 (C)
HTML Source Code
<div role="application">
<h2>Happy Time Pizza On-line Ordering System</h2>
<form>
<div id="tabpanel1" class="tabpanel">
<ul class="tablist" role="tablist">
<li id="tab1" class="tab selected" aria-controls="panel1" aria-selected="true" role="tab" tabindex="0">Crust</li>
<li id="tab2" class="tab" aria-controls="panel2" role="tab" aria-selected="false" tabindex="-1">Veggies</li>
<li id="tab3" class="tab" aria-controls="panel3" role="tab" aria-selected="false" tabindex="-1">Carnivore</li>
<li id="tab4" class="tab" aria-controls="panel4" role="tab" aria-selected="false" tabindex="-1">Delivery</li>
</ul>
<div id="panel1" class="panel" aria-labelledby="tab1" role="tabpanel">
<h3 tabindex="0">Select Crust</h3>
<ul class="controlList">
<li><input id="p1_opt1" type="radio" name="crust" value="crust1" /><label for="p1_opt1">Deep Dish</label></li>
<li><input id="p1_opt2" type="radio" name="crust" value="crust2" checked="checked" /><label for="p1_opt2">Thick and cheesy</label></li>
<li><input id="p1_opt3" type="radio" name="crust" value="crust3" /><label for="p1_opt3">Thick and spicy</label></li>
<li><input id="p1_opt4" type="radio" name="crust" value="crust4" /><label for="p1_opt4">Thin</label></li>
</ul>
</div>
<div id="panel2" class="panel" aria-labelledby="tab2" role="tabpanel">
<h3 tabindex="0">Select Vegetables</h3>
<ul class="controlList">
<li><input id="p2_opt1" type="checkbox" name="veg" value="black olives" /><label for="p2_opt1">Black Olives</label></li>
<li><input id="p2_opt2" type="checkbox" name="veg" value="green olives" /><label for="p2_opt2">Green Olives</label></li>
<li><input id="p2_opt3" type="checkbox" name="veg" value="green peppers" /><label for="p2_opt3">Green Peppers</label></li>
<li><input id="p2_opt4" type="checkbox" name="veg" value="mushrooms" /><label for="p2_opt4">Mushrooms</label></li>
<li><input id="p2_opt5" type="checkbox" name="veg" value="onions" /><label for="p2_opt5">Onions</label></li>
<li><input id="p2_opt6" type="checkbox" name="veg" value="pineapple" /><label for="p2_opt6">Pineapple</label></li>
</ul>
</div>
<div id="panel3" class="panel" aria-labelledby="tab3" role="tabpanel">
<h3 tabindex="0">Select Carnivore Options</h3>
<ul class="controlList">
<li><input id="p3_opt1" type="checkbox" name="meat" value="pepperoni" /><label for="p3_opt1">Pepperoni</label></li>
<li><input id="p3_opt2" type="checkbox" name="meat" value="sausage" /><label for="p3_opt2">Italian Sausage</label></li>
<li><input id="p3_opt3" type="checkbox" name="meat" value="ham" /><label for="p3_opt3">Ham</label></li>
<li><input id="p3_opt4" type="checkbox" name="meat" value="hamburger" /><label for="p3_opt4">Hamburger</label></li>
</ul>
</div>
<div id="panel4" class="panel" aria-labelledby="tab4" role="tabpanel">
<h3 tabindex="0">Select Delivery Method</h3>
<ul class="controlList">
<li><input id="p4_opt1" type="radio" name="delivery" value="delivery1" checked="checked" /><label for="p4_opt1">Delivery</label></li>
<li><input id="p4_opt2" type="radio" name="delivery" value="delivery2" /><label for="p4_opt2">Eat in</label></li>
<li><input id="p4_opt3" type="radio" name="delivery" value="delivery3" /><label for="p4_opt3">Carry out</label></li>
<li><input id="p4_opt4" type="radio" name="delivery" value="delivery4" /><label for="p4_opt4">Overnight mail</label></li>
</ul>
</div>
</div>
</form>
</div>
CSS Code
.tabpanel {
margin: 20px;
padding: 0;
height: 1%; /* IE fix for float bug */
}
.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;
display: inline; /* IE float bug fix */
}
.panel {
clear: both;
display: block;
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 {
color: black;
background-color: #fff;
border-bottom: 1px solid white;
}
.focus {
margin-top: 0;
height: 1.2em;
}
.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;
}
Javascript Source Code
$(document).ready(function() {
var panel1 = new tabpanel("tabpanel1", false);
});
//
// 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 panels.
// 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 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
//
// @return N/A
//
tabpanel.prototype.switchTabs = function($curTab, $newTab) {
// Remove the highlighting from the current tab
$curTab.removeClass('selected focus');
// remove tab from the tab order and update its aria-selected attribute
$curTab.attr('tabindex', '-1').attr('aria-selected', 'false');
// update the aria attributes
// Highlight the new tab and update its aria-selected attribute
$newTab.addClass('selected').attr('aria-selected', 'true');
// 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');
}
// 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
//
// @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.slideDown(100);
$panel.attr('aria-hidden', 'false');
}
else {
$panel.slideUp(100);
$panel.attr('aria-hidden', 'true');
}
} // 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
//
// @param (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.length-1) {
// 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
//
// @param (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
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns true
//
tabpanel.prototype.handleTabClick = function($tab, e) {
// Remove the highlighting from all tabs
this.$tabs.removeClass('selected');
// remove all tabs from the tab order and reset their aria-selected attribute
this.$tabs.attr('tabindex', '-1').attr('aria-selected', 'false');
// hide all tab panels
this.$panels.hide();
// Highlight the clicked tab and update its aria-selected attribute
$tab.addClass('selected').attr('aria-selected', 'true');
// show the clicked tab panel
this.$panel.find('#' + $tab.attr('aria-controls')).show();
// make clicked tab navigable
$tab.attr('tabindex', '0');
// give the tab focus
$tab.focus();
return true;
} // 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
//
// @param (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
//
// @param (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 ($elem obj) $elem is the jquery object of the element being processed
//
// @param (e obj) e is the associated event object
//
// @return (boolean) Returns true if propagating; false if consuming event
//
tabpanel.prototype.handlePanelKeyDown = function($elem, e) {
if (e.altKey) {
// do nothing
return true;
}
switch (e.keyCode) {
case this.keys.esc: {
e.stopPropagation();
return false;
}
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 = $('#' + $elem.attr('aria-labelledby'));
// 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 = $('#' + $elem.attr('aria-labelledby'));
// get the index of the tab in the tab list
var curNdx = this.$tabs.index($tab);
if (curNdx == this.$tabs.length-1) {
// 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 ($elem obj) $elem is the jquery object of the element being processed
//
// @param (e obj) e is the associated event object
//
// @return (boolean) Returns true if propagating; false if consuming event
//
tabpanel.prototype.handlePanelKeyPress = function($elem, 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;
}
});