Accessibility Examples
Example Start
Tic Tac Toe Game
|
Top Left Cell is empty |
Top Center Cell is empty |
Top Right Cell is empty |
|
Middle Left Cell is empty |
Center Cell is empty |
Middle Right Cell is empty |
|
Bottom Left Cell is empty |
Bottom Center Cell is empty |
Bottom Right Cell is empty |
|
|
Example End
Example Description
Type: Best Practice
Simple example of drag and drop widgets. The grid widget bound to the play board uses aria-activedescendant to manage focus.
Keyboard Support
The following keyboard shortcuts are implemented for this example (based on recommended shortcuts specified by the DHTML Style Guide Working Group.):
- Tab / Shift-Tab: Move focus between draggable objects and grid
- Up Arrow: Move focus up one grid cell
- Down Arrow: Move focus down one grid cell
- Left Arrow: Move focus one grid cell to the left
- Right Arrow: Move focus one grid cell to the right
- ENTER or Space: Pick up / Drop game piece
Example Markup
- ARIA 1.0: [aria-activedescendant]
- ARIA 1.0: [aria-dropeffect]
- ARIA 1.0: [aria-grabbed]
- ARIA 1.0: [aria-labelledby]
- ARIA 1.0: [aria-selected]
- ARIA 1.0: [role="alert"]
- ARIA 1.0: [role="application"]
- ARIA 1.0: [role="button"]
- ARIA 1.0: [role="grid"]
- ARIA 1.0: [role="gridcell"]
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">
<h3 id="grid1_label">Tic Tac Toe Game</h3>
<input id="startButton" value="START" type="button" />
<ul class="draggables">
<span class="x100">
<table border="1" tabindex="0" role="grid" id="grid1" aria-labelledby="grid1_label" aria-activedescendant="cell1x1">
<tr role="row">
<td id="cell1x1" role="gridcell" class="target" aria-labelledby="cell1_label" aria-dropeffect="none" aria-selected="false">
<p class="hidden" id="cell1_label">Top Left Cell is empty</p>
</td>
<td id="cell2x1" role="gridcell" class="target" aria-labelledby="cell2_label" aria-dropeffect="none" aria-selected="false">
<p class="hidden" id="cell2_label">Top Center Cell is empty</p>
</td>
<td id="cell3x1" role="gridcell" class="target" aria-labelledby="cell3_label" aria-dropeffect="none" aria-selected="false">
<p class="hidden" id="cell3_label">Top Right Cell is empty</p>
</td>
</tr>
<tr role="row">
<td id="cell1x2" role="gridcell" class="target" aria-labelledby="cell4_label" aria-dropeffect="none" aria-selected="false">
<p class="hidden" id="cell4_label">Middle Left Cell is empty</p>
</td>
<td id="cell2x2" role="gridcell" class="target" aria-labelledby="cell5_label" aria-dropeffect="none" aria-selected="false">
<p class="hidden" id="cell5_label">Center Cell is empty</p>
</td>
<td id="cell3x2" role="gridcell" class="target" aria-labelledby="cell6_label" aria-dropeffect="none" aria-selected="false">
<p class="hidden" id="cell6_label">Middle Right Cell is empty</p>
</td>
</tr>
<tr role="row">
<td id="cell1x3" role="gridcell" class="target" aria-labelledby="cell7_label" aria-dropeffect="copy" aria-selected="false">
<p class="hidden" id="cell7_label">Bottom Left Cell is empty</p>
</td>
<td id="cell2x3" role="gridcell" class="target" aria-labelledby="cell8_label" aria-dropeffect="copy" aria-selected="false">
<p class="hidden" id="cell8_label">Bottom Center Cell is empty</p>
</td>
<td id="cell3x3" role="gridcell" class="target" aria-labelledby="cell9_label" aria-dropeffect="copy" aria-selected="false">
<p class="hidden" id="cell9_label">Bottom Right Cell is empty</p>
</td>
</tr>
</table>
</span>
<table border="0">
<tr>
<td id="p1" width="100" height=100>
<div id="ex" class="ex piece" aria-grabbed="false" role="button" tabindex="0">
<img src="http://www.oaa-accessibility.org/media/examples/images/game-piece-ex.png" alt="Grabable X for Tic Tac Toe">
</div>
</td>
<td width="160" height=100 valign="top">
<div id="out" role="alert"></div>
</td>
<td id="p2" width="100" height=100>
<div id="oh" class="oh piece" aria-grabbed="false" role="button" tabindex="0">
<img src="http://www.oaa-accessibility.org/media/examples/images/game-piece-oh.png" alt="Grabable O for Tic Tac Toe">
</div>
</td>
</tr>
</table>
</ul>
</div>
CSS Code
/* CSS Document */
.hidden {
visibility: hidden;
position: absolute;
}
.piece {
margin: 10px;
padding: 0;
width: 75px;
height: 75px;
position: relative;
}
h3#grid1_label {
width: 366px;
margin-left: 35px;
text-align: center;
}
ul.draggables {
font-size: 100% !important;
margin: 10px 35px !important;
padding 0 !important;
}
table {
empty-cells: show;
}
input#startButton {
margin-left: 180px;
padding-left: .25em;
padding-right: .25em;
text-align: center;
font-weight: bold;
}
div#out {
margin-top: 10px;
padding: 2px;
border: 1px solid black;
background-color: #eee;
min-height: 1.2em;
font-weight: bold;
}
.target {
margin: 0;
padding: 10px;
width: 97px;
height: 119px;
position: relative;
}
.target-available {
background-color: #FFFFCC;
}
.target-hover {
padding: 8px;
background-color: #FFCCCC;
border: 2px solid black;
}
td.win {
background-color: #88FF88;
}
.helper {
filter: alpha(opacity=50);
-moz-opacity: 0.5;
-khtml-opacity: 0.5;
opacity: 0.5;
border: 2px solid blue;
z-index: 300;
}
div.piece:focus,
div.piece:active {
border: 1px solid #880000;
}
.grabbed {
border: 2px solid red !important;
}
Javascript Source Code
$(document).ready(function() {
var game = new tictactoe('grid1', 'startButton', 'out', 3, 3);
}); // end ready()
//
// Function tictactoe() defines a class to implement a tic-tac-toe game. The board is
// an ARIA grid widget. The game listens to the grab and drop events triggered by
// the draggables and the targetEnter and targetLeave events triggered by the droppables.
//
// @param (boardID string) boardID is the html id of the table to attach to.
//
// @param (startID string) startID is the html id of the start button to use.
//
// @param (msgID string) msgID is the html id of the message box to use.
//
// @param (numRows integer) numRows is the number of rows in the grid
//
// @param (numCols integer) numCols is the number of columns in the grid
//
// @return N/A
//
function tictactoe(boardID, startID, msgID, numRows, numCols) {
// define widget properties
this.$id = $('#' + boardID);
this.$startButton = $('#' + startID);
this.$msgBox = $('#' + msgID);
this.keys = new keyCodes();
this.$rows = this.$id.find('tr');
this.$cells = this.$id.find('td');
this.$active = $('#' + this.$id.attr('aria-activedescendant')); // the cell to treat as active (e.g. focus)
this.targets = []; // an array of droppable widgets
var thisObj = this;
// create droppable instances for each dropTarget found
this.$cells.each(function(index) {
thisObj.targets[index] = new droppable($(this).attr('id'), 1, 'copy', false);
});
this.numRows = numRows;
this.numCols = numCols;
this.player1 = new draggable('ex', this.$id, true, 10, 10, false, false);
this.player2 = new draggable('oh', this.$id, true, 10, 10, false, false);
this.curPlayer = null; // set to the current player piece widget
this.activeDraggable = null; // set to the grabbed draggable upon receiving a drag event
this.activeTarget = null; // set to the droppable under the draggable
this.rowLabels = ['Top', 'Middle', 'Bottom'];
this.colLabels = ['Left', 'Center', 'Right'];
this.moveCount = 0;
this.winner = '';
// bind event handlers
this.bindHandlers();
this.updateUser('Press START to play.');
} // end tictactoe() constructor
//
// Function updateUser() is a member function to output a status message to the message box
//
// @param (msg string) msg is the message to output
//
// @return N/A
//
tictactoe.prototype.updateUser = function(msg) {
this.$msgBox.html(msg);
} // end updateUser
//
// Function clearBoard() is a member function to remove any pieces from the game board and to
// reset the labels for the grid cells.
//
// @return N/A
//
tictactoe.prototype.resetBoard = function() {
var thisObj = this;
// empty the cells
this.$cells.find('*').not('p').remove();
// set the active descendant to be the first cell
this.$id.attr('aria-activedescendent', this.$cells.first().attr('id'));
// For each row, find the label for each cell and reset its contents
this.$rows.each(function(rowIndex) {
$(this).find('p').each(function(colIndex) {
if (rowIndex == 1 && colIndex == 1) {
// the center cell should only be labelled as 'center'
$(this).text(thisObj.colLabels[colIndex] + ' Cell is empty');
}
else {
$(this).text(thisObj.rowLabels[rowIndex] + ' ' + thisObj.colLabels[colIndex] + ' Cell is empty');
}
});
});
// remove win highlight from cells
this.$cells.removeClass('win');
// reset the moveCount and end game flags
this.moveCount = 0;
this.winner = '';
// reset stored target and draggable
this.activeTarget = null;
this.activeDraggable = null;
} // end resetBoard()
//
// Function endGameCheck() is a member function to check for an end of game condition. A player may only win
// if his/her last move makes 3 in a row; therefor, this function checks possible wins from the last position
// played. The worst case scenario is if the last move is the center square.
//
// @return N/A
//
tictactoe.prototype.endGameCheck = function() {
var $row = this.activeTarget.$id.parent(); // the row containing the square played in
var rowNdx = $row.index(); // the index of the row played in
var colNdx = this.activeTarget.$id.index(); // the index of the column played in
var lastPiece = ''; // the name of the piece that was played ('ex' or 'oh')
var $squares = null; // contains a list of squares that contain matching pieces
// Determine which piece was played last
if (this.activeDraggable.$id.hasClass('ex') == true) {
lastPiece = 'ex';
}
else {
lastPiece = 'oh';
}
/////////////// check the row //////////////////////
// Do a find for all matching pieces in the row
// played in. If the count equals the number of columns,
// we have a winner.
if ($row.find('div.' + lastPiece).length == this.numCols) {
// add the win styling to the row cells
$row.find('td').addClass('win');
// set winner
this.winner = lastPiece;
}
///////////// check the column ////////////////////
// Iterate through the rows, building a list of
// matching squares in the column last played in.
this.$rows.each(function(index) {
var $curSquare = $(this).find('td').eq(colNdx);
var $div = $curSquare.find('div');
// if there is a piece in the square, and that
// piece matches the last one played, add the square
// to the list of squares.
if ($div) {
if ($div.hasClass(lastPiece) == true) {
if ($squares) {
$squares = $squares.add($curSquare);
}
else {
$squares = $curSquare;
}
}
}
});
// if the count equals the number of rows, we have a winner
if ($squares.length == this.numRows) {
// add the win styling to the squares
$squares.addClass('win');
// set winner
this.winner = lastPiece;
}
// reset $squares
$squares = null;
/////////// check the diagonal (ul to lr) ////////////////////
// check the three square where the row and column indices match.
// If the square contains a matching piece, add it to the list.
for (ndx = 0; ndx < this.numRows; ndx++) {
var $curSquare = this.$rows.eq(ndx).find('td').eq(ndx);
var $div = $curSquare.find('div');
// if there is a piece in the square, and that
// piece matches the last one played, add the square
// to the list of squares.
if ($div) {
if ($div.hasClass(lastPiece) == true) {
if ($squares) {
$squares = $squares.add($curSquare);
}
else {
$squares = $curSquare;
}
}
}
}
// if the count equals the number of rows, we have a winner
if ($squares) {
if ($squares.length == this.numRows) {
// add the win styling to the squares
$squares.addClass('win');
// set winner
this.winner = lastPiece;
}
// reset $squares
$squares = null;
}
/////////// check the other diagonal (ur to ll) ////////////////////
// check the three square where the row and column are opposites.
// If the square contains a matching piece, add it to the list.
for (ndx = 0; ndx < this.numRows; ndx++) {
var $curSquare = this.$rows.eq(ndx).find('td').eq(2 - ndx);
var $div = $curSquare.find('div');
// if there is a piece in the square, and that
// piece matches the last one played, add the square
// to the list of squares.
if ($div) {
if ($div.hasClass(lastPiece) == true) {
if ($squares) {
$squares = $squares.add($curSquare);
}
else {
$squares = $curSquare;
}
}
}
}
// if the count equals the number of rows, we have a winner
if ($squares) {
if ($squares.length == this.numRows) {
// add the win styling to the squares
$squares.addClass('win');
// set winner
this.winner = lastPiece;
// the game is over
return true;
}
}
if (this.winner.length > 0) {
return true;
}
// there is a draw if the piece count is equal to
// the number of cells in the grid
if (this.moveCount == this.$cells.length) {
// the game is over
return true;
}
// game is not over yet
return false;
} // end endGameCheck()
//
// Function bindHandlers() is a member function to bind event handlers for the board.
//
// @return N/A
//
tictactoe.prototype.bindHandlers = function() {
var thisObj = this;
// bind a keydown handler
this.$id.keydown(function(e) {
return thisObj.handleKeyDown(e, $(this));
});
// bind a keyup handler
this.$id.keyup(function(e) {
return thisObj.handleKeyUp(e, $(this));
});
// bind a keypress handler
this.$id.keypress(function(e) {
return thisObj.handleKeyPress(e, $(this));
});
////////////// bind event handlers for draggable events /////////////////
this.$id.bind('grab', function(e, draggable) {
return thisObj.handleGrab(e, draggable);
});
this.$id.bind('drop', function(e, draggable) {
return thisObj.handleDrop(e, draggable);
});
////////////// bind event handlers for droppable events /////////////////
this.$id.bind('targetEnter', function(e, droppable) {
return thisObj.handleTargetEnter(e, droppable);
});
this.$id.bind('targetLeave', function(e, droppable) {
return thisObj.handleTargetLeave(e, droppable);
});
// bind a click handler for the start button
this.$startButton.click(function(e) {
return thisObj.handleStartClick(e);
});
} // end bindHandlers()
//
// Function handleKeyDown() is a member function to process keydown events for
// the gameboard
//
// @param (e object) e is the event object
//
// @param ($id object) $id is the jquery object of the cell triggering event
//
// @return (boolean) Returns false if consuming event; true if propagating
//
tictactoe.prototype.handleKeyDown = function(e, $id) {
if (e.altKey || e.ctrlKey || e.shiftKey) {
// do nothing
return true;
}
switch (e.keyCode) {
case this.keys.tab: {
// update the aria-selected property of the active cell
this.$active.attr('aria-selected','false');
if (this.activeDraggable) {
this.activeDraggable.abandonDrag();
}
// tab must propagate
return true;
}
case this.keys.esc: {
if (this.activeDraggable) {
this.activeDraggable.abandonDrag();
}
e.stopPropagation();
return false;
}
case this.keys.left: {
if (this.$active.index() > 0) {
// update the aria-selected property of the active cell
this.$active.attr('aria-selected','false');
this.$active = this.$active.prev();
this.$id.attr('aria-activedescendant', this.$active.attr('id'));
// set the aria-selected property of the new active cell
this.$active.attr('aria-selected','true');
if (this.activeDraggable) {
this.activeDraggable.doDrag(this.$active.offset().left, this.$active.offset().top);
}
}
e.stopPropagation();
return false;
}
case this.keys.up: {
var $row = this.$active.parent();
var cellNdx = this.$active.index();
if ($row.index() > 0) {
// update the aria-selected property of the active cell
this.$active.attr('aria-selected','false');
this.$active = $row.prev().find('td').eq(cellNdx);
this.$id.attr('aria-activedescendant', this.$active.attr('id'));
// set the aria-selected property of the new active cell
this.$active.attr('aria-selected','true');
if (this.activeDraggable) {
this.activeDraggable.doDrag(this.$active.offset().left, this.$active.offset().top);
}
}
e.stopPropagation();
return false;
}
case this.keys.right: {
if (this.$active.index() < this.numCols - 1) {
// update the aria-selected property of the active cell
this.$active.attr('aria-selected','false');
this.$active = this.$active.next();
this.$id.attr('aria-activedescendant', this.$active.attr('id'));
// set the aria-selected property of the new active cell
this.$active.attr('aria-selected','true');
if (this.activeDraggable) {
this.activeDraggable.doDrag(this.$active.offset().left, this.$active.offset().top);
}
}
e.stopPropagation();
return false;
}
case this.keys.down: {
var $row = this.$active.parent();
var cellNdx = this.$active.index();
if ($row.index() < this.numRows - 1) {
// update the aria-selected property of the active cell
this.$active.attr('aria-selected','false');
this.$active = $row.next().find('td').eq(cellNdx);
this.$id.attr('aria-activedescendant', this.$active.attr('id'));
// set the aria-selected property of the new active cell
this.$active.attr('aria-selected','true');
if (this.activeDraggable) {
this.activeDraggable.doDrag(this.$active.offset().left, this.$active.offset().top);
}
}
e.stopPropagation();
return false;
}
}
return true;
} // end handleKeyDown()
//
// Function handleKeyUp() is a member function to process keyup events for the game board.
// The function will only respond to keyup events from the enter or space bar and will process
// drops.
//
// @param (e object) e is the event object
//
// @param ($id object) $id is the jquery object of the cell triggering event
//
// @return (boolean) Returns false if consuming event; true if propagating
//
tictactoe.prototype.handleKeyUp = function(e, $id) {
if (e.altKey || e.ctrlKey || e.shiftKey) {
// do nothing
return true;
}
switch (e.keyCode) {
case this.keys.enter:
case this.keys.space: {
if (this.activeDraggable) {
if (this.activeTarget == null) {
// player attempted to drop a piece on
// and occupied space
this.updateUser('That space is occupied!');
}
else {
this.activeDraggable.doDrop();
}
}
e.stopPropagation();
return false;
}
}
return true;
} // end handleKeyUp()
//
// Function handleKeyPress() is a member function to consume keypress events for
// the gameboard. This handler is necessary to prevent unwanted window manipulation
// in browsers that process on keypress rather than keydown (such as Opera).
//
// @param (e object) e is the event object
//
// @param ($id object) $id is the jquery object of the cell triggering event
//
// @return (boolean) Returns false if consuming event; true if propagating
//
tictactoe.prototype.handleKeyPress = function(e, $id) {
if (e.altKey || e.ctrlKey || e.shiftKey) {
// do nothing
return true;
}
switch (e.keyCode) {
case this.keys.esc:
case this.keys.enter:
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 handleKeyPress()
//
// Function handleGrab() is a member function to process grab events triggered by
// a draggable. This function stores the active draggable so the grid may manipulate
// its position.
//
// @param (e object) e is the event object
//
// @param (draggable object) draggable is the draggable object triggering event
//
// @return (boolean) Returns true
//
tictactoe.prototype.handleGrab = function(e, draggable) {
this.activeDraggable = draggable;
return true;
} // end handleGrab()
//
// Function handleDrop() is a member function to process Drop events triggered by
// a draggable. This function sets the stored draggable to null.
//
// @param (e object) e is the event object
//
// @param (draggable object) draggable is the draggable object triggering event
//
// @return (boolean) Returns true
//
tictactoe.prototype.handleDrop = function(e, draggable) {
if (draggable.validDrop == true) {
// drop was valid, modify the cell label
var piece = this.activeDraggable.$id.attr('id');
var row = this.activeTarget.$id.parent().index();
var col = this.activeTarget.$id.index();
this.$active = this.activeTarget.$id;
// Make this cell the active descendant
this.$id.attr('aria-activedescendant', this.$active.attr('id'));
// modify the label
if (row == 1 && col == 1) {
this.activeTarget.$id.find('p').text(this.colLabels[col]
+ ' Cell contains an ' + piece);
}
else {
this.activeTarget.$id.find('p').text(this.rowLabels[row] + ' '
+ this.colLabels[col] + ' Cell contains an ' + piece);
}
// Remove the id attribute of the piece, as it is now non-unique (i.e. invalid markup)
// and is not necessary. Also remove the piece copy from the tab order. Use jQuery
// chaining for speed.
this.activeTarget.$id.find('div.piece').removeAttr('id').removeAttr('tabindex');
//increment the moveCount
this.moveCount++;
// check for end of game
if (this.endGameCheck() == false) {
// swap players
if (this.curPlayer == this.player1) {
this.curPlayer = this.player2;
this.player2.enable();
this.player1.disable();
}
else {
this.curPlayer = this.player1;
this.player1.enable();
this.player2.disable();
}
this.updateUser(this.curPlayer.$id.attr('id') + '\'s turn to play');
this.curPlayer.$id.focus();
this.activeDraggable = null;
this.activeTarget = null;
}
else {
// disable the pieces
this.player1.disable();
this.player2.disable();
if (this.winner.length > 0) {
this.updateUser(this.winner + ' has won!');
}
else {
this.updateUser('Tie game.');
}
this.activeDraggable = null;
this.activeTarget = null;
// set focus on start button
this.$startButton.focus();
}
}
else {
this.updateUser('You must place an \'' + this.curPlayer.$id.attr('id') + '\' in an empty space.');
}
// Update the aria-selected attribute of the active grid cell
this.$active.attr('aria-selected', 'false');
return true;
} // end handleDrop()
//
// Function handleTargetEnter() is a member function to process a targetEnter event triggered
// by a droppable. This function stores the active droppable so the grid may manipulate
// it.
//
// @param (e object) e is the event object
//
// @param (droppable object) droppable is the droppable object triggering event
//
// @return (boolean) Returns true
//
tictactoe.prototype.handleTargetEnter = function(e, droppable) {
this.activeTarget = droppable;
return true;
} // end handleTargetEnter()
//
// Function handleTargetLeave() is a member function to process a targetLeave event triggered
// by a droppable. This function resets the stored activeTarget.
//
// @param (e object) e is the event object
//
// @param (droppable object) droppable is the droppable object triggering event
//
// @return (boolean) Returns true
//
tictactoe.prototype.handleTargetLeave = function(e, droppable) {
this.activeTarget = null;
return true;
} // end handleTargetLeave()
//
// Function handleStartClick() is a member function to process click events for the start
// button. This function resets the board and sets focus on the first player's piece.
//
// @param (e object) e is the event object
//
// @return (boolean) Returns false
//
tictactoe.prototype.handleStartClick = function(e) {
var thisObj = this;
// clear the game board
this.resetBoard();
// make the drop targets available
for (var ndx = 0; ndx < this.targets.length; ndx++) {
thisObj.targets[ndx].reset();
}
// set the current player to be player 1
this.curPlayer = this.player1;
// enable the player1 piece and disable player2
this.player1.enable();
this.player2.disable();
// notify user that player one should take a turn
this.updateUser(this.curPlayer.$id.attr('id') + '\'s turn to play');
// set focus on the current player piece
this.player1.$id.focus();
e.stopPropagation();
return false;
} // end handleStartClick()