Accessibility Examples
Example Start
| Sel | Msg | Status | Att | Pri | From | Subject | Date | Size |
|---|
Example End
Example Description
Type: Best Practice
Simple example of a grid widget to implement an email application. Inline images are used to display the visual state of the sort order of the cells.
Keyboard Support
- Tab: Move focus between web 2.0 widgets, links and form controls.
- Up Arrow: Move focus to previous message
- Down Arrow: Move focus to next message
- Left Arrow: Move focus one column to the left.
- Right Arrow: Move focus one column to the right.
- Space Bar: When in message list, it selects or unselects the current message.
- Space Bar: When the headers, it reorders the list of messages based on that column data.
Example Markup
- ARIA 1.0: [aria-labelledby]
- ARIA 1.0: [aria-readonly]
- ARIA 1.0: [aria-selected]
- ARIA 1.0: [aria-sort]
- ARIA 1.0: [role="button"]
- ARIA 1.0: [role="grid"]
- ARIA 1.0: [role="gridcell"]
- ARIA 1.0: [role="presentation"]
- ARIA 1.0: [role="row"]
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">
<table id="grid1" class="email" role="grid" aria-labelledby="grid1_label" aria-readonly="true">
<caption id="grid1_label">Inbox</caption>
<thead>
<tr>
<th id="grid1_sel" tabindex="-1"><abbr title="Selected">Sel</abbr></th>
<th id="grid1_msg" tabindex="-1"><abbr title="Message">Msg</abbr></th>
<th id="grid1_stat" tabindex="-1">Status</th>
<th id="grid1_att" tabindex="-1"><abbr title="Attachment">Att</abbr></th>
<th id="grid1_pri" tabindex="-1"><abbr title="Priority">Pri</abbr></th>
<th id="grid1_from" tabindex="-1">From</th>
<th id="grid1_sub" tabindex="-1">Subject</th>
<th id="grid1_date" tabindex="-1">Date</th>
<th id="grid1_size" tabindex="-1">Size</th>
</tr>
</thead>
<tbody id="data">
</tbody>
</table>
</div>
CSS Code
body {
font-size: medium;
font-family: sans-serif;
}
.warning {
color: red;
font-weight: bold;
}
#grid1 {
padding: 0;
text-align: center;
border-collapse: collapse;
border: 2px solid black;
}
thead th {
padding: 2px 8px;
background-color: #eef;
border: 1px solid black;
}
th.hover {
border: 2px solid black;
background-color: #9bf;
}
tbody th, td {
padding: 2px 5px;
border: 1px solid black;
}
button.sort-button {
margin: 5px;
padding: 0px;
width: 18px;
height: 19px;
border: none;
background-color: transparent;
}
img.sort-image {
margin: 0;
padding: 0;
width: 14px;
height: 15px;
position: relative;
top: 0px;
left: -1px;
}
.focus {
border: 2px solid black;
background-color: #79e;
}
.selected {
color: #fff;
background-color: #800 !important;
}
.odd {
background-color: #fcfcec;
}
.even {
background-color: transparent;
}
.hidden {
position: absolute;
top: -30em;
left: -300em;
}
Javascript Source Code
$(document).ready(function() {
var emailApp = new email('grid1');
}); // end ready
//
// 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
/*
* function email() is a class to implement an email application. email() loads example email into a
* simple data table, making the table columns sortable.
*
* email() must be passed the id of the html table to attach to. It assumes that the email data is in
* inbox.json.
*
*/
function email(id) {
var thisObj = this;
this.$id = $('#' + id); // the jquery object for the table to attach to.
this.$headers = $('#' + id + ' thead').find('th'); // an array of jquery objects for the header cells
this.$data = this.$id.find('#data'); // the jquery object for the table tbody
this.$cells; // an array of jquery objects for the table cells - including the header column
// create arrays to give human-readable labels to the columns and grid cells
this.labels = new Array('Selected', 'Message Number', 'Status', 'Attachment', 'Priority', 'From', 'Subject', 'Date', 'Size');
this.priorities = new Array('lowest', 'low', 'none', 'high', 'highest');
this.stat = new Array('unread', 'read', 'reply');
this.keys = new keyCodes();
this.isSortable = false; // Set to true if data loads correctly and table has sortable columns
// connect to the json file and load the data
$.getJSON('http://www.oaa-accessibility.org/media/examples/js/inbox.json', function(data, rslt) {
if (rslt == 'success') {
// Load the data from the json file
thisObj.loadData(data);
// make the table sortable
thisObj.makeSortable(thisObj.$data);
thisObj.$cells = thisObj.$data.find('th,td');
// bind event handlers to the application table
thisObj.bindHandlers();
}
});
} // end email() constructor
//
//
// function loadData() loads the email data from the json file
//
// @return N/A
//
email.prototype.loadData = function(data) {
var thisObj = this;
var row;
var tmpStr;
var count = 1;
var id = thisObj.$id.attr('id');
var label;
$.each(data.emails, function(ndx, record) {
var msgID = id + '_msg' + count;
row = '<tr id="' + msgID + '" role="row" aria-selected="false">';
label = msgID + ' ' + id + '_sel"';
row += '<th id="' + msgID + '_sel" role="gridcell" aria-labelledby="' + label
+ '" tabindex="0"><input name="email_' + count
+ '" type="checkbox" tabindex="-1"/></th>';
row += '<td id="' + msgID + '_msg" role="gridcell" headers="' + msgID + '_sel ' + id + '_msg" tabindex="-1">' + count + '</td>';
// set tmpstr to be the value of the message status (unread, read, reply)
tmpStr = thisObj.stat[record.stat];
row += '<td id="' + msgID + '_stat" role="gridcell" headers="' + msgID + '_sel ' + id + '_stat" tabindex="-1">'
+ '<span class="hidden">' + tmpStr + '</span><img src="http://www.oaa-accessibility.org/media/examples/images/' + tmpStr
+ '.gif" alt="' + tmpStr + '" role="presentation"></td>';
// set tmpstr to indicate an attachment or no attachment
tmpStr = (record.att == true ? 'attachment': 'no attachment');
row += '<td id="' + msgID + '_att" role="gridcell" headers="' + msgID + '_sel ' + id + '_att" tabindex="-1">'
+ '<span class="hidden">' + tmpStr + '</span><img src="http://www.oaa-accessibility.org/media/examples/images/'
+ (record.att == true ? 'attach': 'noattach') + '.gif" alt="' + tmpStr + '" role="presentation"></td>';
// set tmpStr to be the value of the message priority
tmpStr = thisObj.priorities[record.pri - 1];
row += '<td id="' + msgID + '_pri" role="gridcell" headers="' + msgID + '_sel ' + id + '_pri" tabindex="-1">'
+'<span class="hidden">Priority ' + record.pri + '</span><img src="http://www.oaa-accessibility.org/media/examples/images/priority_'
+ tmpStr + '.gif" alt="' + tmpStr + '" role="presentation"></td>';
row += '<td id="' + msgID + '_from" role="gridcell" headers="' + msgID + '_sel ' + id + '_from" tabindex="-1">'
+ record.from + '</td>';
row += '<td id="' + msgID + '_sub" role="gridcell" headers="' + msgID + '_sel ' + id + '_sub" tabindex="-1">'
+ record.sub + '</td>';
row += '<td id="' + msgID + '_date" role="gridcell" headers="' + msgID + '_sel ' + id + '_date" tabindex="-1">'
+ record.date + '</td>';
row += '<td id="' + msgID + '_size" role="gridcell" headers="' + msgID + '_sel ' + id + '_size" tabindex="-1">'
+ record.siz + '</td></tr>';
thisObj.$data.append(row);
count++;
});
} // end loadData()
//
//
// function makeSortable() applies properties to a table necessary to allow for alpha-numeric sorting
//
// @return N/A
//
email.prototype.makeSortable = function() {
var thisObj = this;
// iterate through the table elements
this.$id.each(function () {
thisObj.$id.alternateRowColors();
// iterate through the table header
thisObj.$headers.each(function(column) {
var $header = $(this);
var findSortKey;
// Add the sort-alpha class
$header.addClass('sort-alpha');
// find the sort keys
findSortKey = function ($cell) {
return $cell.find('.sort-key').text().toUpperCase()
+ ' ' + $cell.text().toUpperCase();
};
if (findSortKey) {
// Define the header highlighting handler
$header.find('.sort-button').addClass('clickable');
$header.append('<button class="sort-button" role="button" type="button" tabindex="-1">'
+ '<img class="sort-image" src="http://www.oaa-accessibility.org/media/examples/images/sortable.png" title="Sort column" role="presentation">'
+ '<span class="hidden">Sort ' + $header.text() + ' column</span>'
+ '</button>');
thisObj.isSortable = true;
} // end if
}); // end each
}); // end each
} // end makeSortable();
//
// function sortTable() is a member function to sort the table rows alpha-numerically based on the active column
//
// @param($id object) $id is the jQuery object of the header cell of the column to sort by
//
// return N/A
//
email.prototype.sortTable = function($id) {
var thisObj = this;
var colNdx = this.$headers.index($id);
var sortDirection = 1;
// if the table is sorted in ascending order, reverse
// the sort order flag
if ($id.is('.sorted-asc')) {
sortDirection = -1;
}
// create an array of the table rows
var rows = this.$data.find('tr').get();
// iterate through the table rows and store the sort keys for each
// in an attached expando
$.each(rows, function (index, row) {
var $cell;
// The first column is marked as a header row for screen readers
// Need to treat it seperately
if (colNdx == 0) {
$cell = $(row).children('th').eq(colNdx);
row.sortKey = $cell.find('.sort-key') + ' '
+ $cell.find('input').attr('checked');
}
else {
$cell = $(row).children('td').eq(colNdx - 1);
row.sortKey = $cell.find('.sort-key').text().toUpperCase()
+ ' ' + $cell.text().toUpperCase();
}
});
// sort the rows
rows.sort(function(a, b) {
if (a.sortKey < b.sortKey) {
// move row a before row b
// (according to the sort direction)
return -sortDirection;
}
else if (a.sortKey > b.sortKey) {
// move row a after row b
return sortDirection;
}
// the rows are equal--do nothing
return 0;
});
// iterate through the rows array and reinsert them into the table
// according to the new sort order
$.each(rows, function(index, row) {
// insert the row
thisObj.$data.append(row);
// remove the expando
row.sortKey = null;
});
// remove the previous sorted class and reset the button images
this.$headers
.removeClass('sorted-asc')
.removeClass('sorted-desc')
.removeAttr('aria-sorted')
.not($id).find('img').attr('src', 'http://www.oaa-accessibility.org/media/examples/images/sortable.png');
if (sortDirection == 1) {
$id.addClass('sorted-asc');
$id.attr('aria-sort', 'ascending');
$id.find('img').attr('src', 'http://www.oaa-accessibility.org/media/examples/images/sorted-asc.png');
}
else {
$id.addClass('sorted-desc');
$id.attr('aria-sort', 'descending');
$id.find('img').attr('src', 'http://www.oaa-accessibility.org/media/examples/images/sorted-desc.png');
}
// reapply the sorted class
// the first column is th for screen readers
if (this.$headers.index($id) == 0) {
// remove the sorted class from the other columns
this.$data.find('td').removeClass('sorted')
// apply the class to the browser column
this.$data.find('th').addClass('sorted');
}
else {
// remove the class from the browser column
this.$data.find('th').removeClass('sorted');
// apply the class to the appropriate column
this.$data.find('td').removeClass('sorted')
.filter(':nth-child(' + (colNdx + 1) + ')')
.addClass('sorted');
}
// Modify the table caption to reflect the new sort column
this.$id.find('caption').html('Inbox sorted by '
+ this.labels[colNdx] + ': '
+ ($id.is('.sorted-asc') ? 'Ascending' : 'Descending') + ' order');
// reapply the row striping
this.$id.alternateRowColors();
} // end sortTable()
//
// function bindHandlers() is a member function to bind event handlers to the application table
//
// return N/A
//
email.prototype.bindHandlers = function() {
var thisObj = this;
////////////////// bind header handlers ////////////////////
this.$headers.keydown(function(e) {
return thisObj.handleHeaderKeyDown($(this), e);
});
this.$headers.keypress(function(e) {
return thisObj.handleHeaderKeyPress($(this), e);
});
this.$headers.click(function(e) {
return thisObj.handleHeaderClick($(this), e);
});
this.$headers.hover(function() {
$(this).addClass('hover');
}, function() {
$(this).removeClass('hover');
});
this.$headers.focus(function(e) {
return thisObj.handleHeaderFocus($(this), e);
});
this.$headers.blur(function(e) {
return thisObj.handleHeaderBlur($(this), e);
});
////////////////// bind tbody handlers ////////////////////
// bind a keydown handler
this.$data.delegate('th,td', 'keydown', function (e) {
return thisObj.handleCellKeyDown($(this), e);
}); // end edit box keydown handler
// bind a keypress handler - consume events for Opera
this.$data.delegate('th,td', 'keypress', function (e) {
return thisObj.handleCellKeyPress($(this), e);
}); // end edit box keyup handler
// bind a click handler
this.$data.delegate('th,td', 'click', function (e) {
return thisObj.handleCellClick($(this), e);
}); // end edit box keyup handler
// bind a focus handler
this.$data.delegate('th,td', 'focus', function (e) {
return thisObj.handleCellFocus($(this), e);
}); // end edit box focus handler
// bind a blur handler
this.$data.delegate('th,td', 'blur', function (e) {
return thisObj.handleCellBlur($(this), e);
}); // end edit box blurhandler
////////////////// bind checkbox click handler ////////////////////
// bind a click handler
this.$data.delegate('input', 'mousedown', function (e) {
return thisObj.handleCheckboxMouseDown($(this), e);
}); // end edit box keydown handler
} // end bindHandlers();
//
// Function handleHeaderKeyDown() is a member function to process keydown events for the table header cells
//
// @param ($id object) $id is the jquery object of the cell generating the event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns false if consuming event; true of not processing
//
email.prototype.handleHeaderKeyDown = function($id, e) {
var curNdx = this.$headers.index($id);
if (e.altKey || e.ctrlKey || e.shiftKey) {
// do nothing
return true;
}
switch (e.keyCode) {
case this.keys.space: {
$id.focus();
this.sortTable($id);
e.stopPropagation();
return false;
}
case this.keys.left: {
if (curNdx > 0) {
var $prev = this.$headers.eq(curNdx - 1);
$prev.focus();
}
e.stopPropagation();
return false;
}
case this.keys.right: {
if (curNdx < this.$headers.length - 1) {
var $next = this.$headers.eq(curNdx + 1);
$next.focus();
}
e.stopPropagation();
return false;
}
case this.keys.down: {
// set focus on the cell in this columen of the first table row
this.$data.find('tr').first().children().eq(curNdx).focus();
e.stopPropagation();
return false;
}
}
return true;
} // end handleHeaderKeyDown()
//
// Function handleHeaderKeyPress() is a member function to process keypress events for the table header
//
// @param ($id object) $id is the jquery object of the cell generating the event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns false if consuming event; true of not processing
//
email.prototype.handleHeaderKeyPress = function($id, e) {
if (e.altKey || e.ctrlKey || e.shiftKey) {
// do nothing
return true;
}
switch (e.keyCode) {
case this.keys.space:
case this.keys.down: {
e.stopPropagation();
return false;
}
}
return true;
} // end handleHeaderKeyPress()
//
// Function handleHeaderClick() is a member function to process click events for the table header cells
//
// @param ($id object) $id is the jquery object of the cell generating the event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns false if consuming event; true of not processing
//
email.prototype.handleHeaderClick = function($id, e) {
$id.focus();
this.sortTable($id);
e.stopPropagation();
return false;
} // end handleHeaderClick()
//
// Function handleHeaderFocus() is a member function to process blur events for the table header cells
//
// @return (boolean) returns true
//
email.prototype.handleHeaderFocus = function($id, e) {
// remove the other headers from the tab order and remove the focus highlighting
this.$headers.attr('tabindex', '-1').removeClass('focus');
// remove the cells from the tab order and remove the focus highlighting
this.$cells.attr('tabindex', '-1').removeClass('focus');
// add the focus class and make the current header navigable
$id.addClass('focus').attr('tabindex', '0');
return true;
} // end handleHeaderFocus()
//
// Function handleHeaderBlur() is a member function to process blur events for the table header cells
//
// @return (boolean) returns true
//
email.prototype.handleHeaderBlur = function($id, e) {
// remove the focus class
$id.removeClass('focus');
return true;
} // end handleHeaderBlur()
//
// Function handleCellKeyDown() is a member function to process keydown events for the table cells
//
// @param ($id object) $id is the jquery object of the cell generating the event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns false if consuming event; true of not processing
//
email.prototype.handleCellKeyDown = function($id, e) {
var $curRow = $id.parent();
var $rows = this.$data.find('tr');
var rowNdx = $rows.index($curRow);
if (e.altKey || e.ctrlKey || e.shiftKey) {
// do nothing
return true;
}
switch (e.keyCode) {
case this.keys.space: {
var $input = $curRow.find('input');
if ($input.attr('checked') == true) {
$input.attr('checked', false);
$curRow.removeClass('selected').attr('aria-selected', 'false');
}
else {
$input.attr('checked', true);
$curRow.addClass('selected').attr('aria-selected', 'true');
}
e.stopPropagation();
return false;
}
case this.keys.left: {
// move left one cell
var $prev = $id.prev();
if ($prev.length > 0) {
$prev.focus();
}
e.stopPropagation();
return false;
}
case this.keys.right: {
// move right one cell
var $next = $id.next();
if ($next.length > 0) {
$next.focus();
}
e.stopPropagation();
return false;
}
case this.keys.up: {
// move up one row
var colNdx = $curRow.children().filter($id).index();
if (rowNdx > 0) {
var $newCell = $rows.eq(rowNdx-1).children().eq(colNdx);
$newCell.focus();
}
else {
// move to the table header
this.$headers.eq(colNdx).focus();
// remove the cells from the tab order
this.$cells.attr('tabindex', '-1');
}
e.stopPropagation();
return false;
}
case this.keys.down: {
// move down one row
if (rowNdx < $rows.length-1) {
var colNdx = $curRow.children().filter($id).index();
var $newCell = $rows.eq(rowNdx+1).children().eq(colNdx);
$newCell.focus();
}
e.stopPropagation();
return false;
}
}
return true;
} // end handleCellKeyDown()
//
// Function handleCellKeyPress() is a member function to process keypress events for the table cells
//
// @param ($id object) $id is the jquery object of the cell generating the event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns false if consuming event; true of not processing
//
email.prototype.handleCellKeyPress = function($id, e) {
if (e.altKey || e.ctrlKey || e.shiftKey) {
// do nothing
return true;
}
switch (e.keyCode) {
case this.keys.space:
case this.keys.left:
case this.keys.up:
case this.keys.right:
case this.keys.down: {
e.stopPropagation();
return false;
}
}
return true;
} // end handleCellKeyPress()
//
// Function handleCellClick() is a member function to process click events for the table cells
//
// @param ($id object) $id is the jquery object of the cell generating the event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns false if consuming event; true of not processing
//
email.prototype.handleCellClick = function($id, e) {
$id.focus()
e.stopPropagation();
return false;
} // end handleCellClick()
//
// Function handleCellFocus() is a member function to process blur events for the table cells
//
// @return (boolean) returns true
//
email.prototype.handleCellFocus = function($id, e) {
// remove the headers from the tab order and remove the focus highlighting
this.$headers.attr('tabindex', '-1').removeClass('focus');
// remove the other cells from the tab order and remove the focus highlighting
this.$cells.attr('tabindex', '-1').removeClass('focus');
// add the focus class and make the current cell navigable
$id.addClass('focus').attr('tabindex', '0');
return true;
} // end handleCellFocus()
//
// Function handleCellBlur() is a member function to process blur events for the table cells
//
// @return (boolean) returns true
//
email.prototype.handleCellBlur = function($id, e) {
// remove the focus class
$id.removeClass('focus');
return true;
} // end handleCellBlur()
//
// Function handleCheckboxMouseDown() is a member function to process mousedown for the checkboxes
//
// @param ($id object) $id is the jquery object of the cell generating the event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns false;
//
email.prototype.handleCheckboxMouseDown = function($id, e) {
var $curRow = $id.parent().parent();
if ($id.attr('checked') == true) {
$id.attr('checked', false);
$curRow.removeClass('selected').attr('aria-selected', 'false');
}
else {
$id.attr('checked', true);
$curRow.addClass('selected').attr('aria-selected', 'true');
}
this.$cells.removeClass('focus');
$id.parent().focus().addClass('focus');
e.stopPropagation();
return false;
} // end handleCheckboxMouseDown()
// Define a small jQuery extension to alternate table row colors
jQuery.fn.alternateRowColors = function() {
$('tbody tr:odd', this).removeClass('even').addClass('odd');
$('tbody tr:even', this).removeClass('odd').addClass('even');
return this;
};