Getting screen reader to read new content added with JavaScript
When a web page is loaded, screen readers (like the one that comes with OS X, or JAWS on Windows) will read the content of the whole page. But say your page is dynamic, and as users performing an action, new content gets added to the page. For the sake of simplicity, say you display a message somewhere in a <span>
. How can you get the screen reader to read that new message?
The WAI-ARIA specification defines several ways by which screen readers can "watch" a DOM element. The best supported method is the aria-live
attribute. It has modes off
, polite
,assertive
and rude
. The higher the level of assertiveness, the more likely it is to interrupt what is currently being spoken by the screen reader.
The following has been tested with NVDA under Firefox 3 and Firefox 4.0b9:
<!DOCTYPE html>
<html>
<head>
<script src="js/jquery-1.4.2.min.js"></script>
</head>
<body>
<button onclick="$('#statusbar').html(new Date().toString())">Update</button>
<div id="statusbar" aria-live="assertive"></div>
</body>
The same thing can be accomplished with WAI-ARIA roles role="status"
and role="alert"
. I have had reports of incompatibility, but have not been able to reproduce them.
<div id="statusbar" role="status">...</div>
Here is an adapted real world example -- this up-level markup has already been converted from an unordered list with links into a select menu via JS. The real code is a lot more complex and obviously could not be included in its entirety, so remember this will have to be rethought for production use. For the select menu to be made keyboard accessible, we registered the keypress & onchange events and fired the AJAX call when users tabbed off of the list (beware of browser differences in timing of the onchange event). This was a serious PITA to make accessible, but it IS possible.
// HTML
<!-- select element with content URL -->
<label for="select_element">State</label>
<select id="select_element">
<option value="#URL_TO_CONTENT_PAGE#" rel="alabama">Alabama</option>
</select>
<p id="loading_element">Content Loading</p>
<!-- AJAX content loads into this container -->
<div id="results_container"></div>
// JAVASCRIPT (abstracted from a Prototype class, DO NOT use as-is)
var selectMenu = $('select_element');
var loadingElement = $('loading_element');
var resultsContainer = $('results_container');
// listen for keypress event (omitted other listeners and support test logic)
this.selectMenu.addEventListener('keypress', this.__keyPressDetector, false);
/* event callbacks */
// Keypress listener
__keyPressDetector:function(e){
// if we are arrowing through the select, enable the loading element
if(e.keyCode === 40 || e.keyCode === 38){
if(e.target.id === 'select_element'){
this.loadingElement.setAttribute('tabIndex','0');
}
}
// if we tab off of the select, send focus to the loading element
// while it is fetching data
else if(e.keyCode === 9){
if(targ.id === 'select_element' && targ.options[targ.selectedIndex].value !== ''){
this.__changeStateDetector(e);
this.loadingElement.focus();
}
}
}
// content changer (also used for clicks)
__changeStateDetector:function(e){
// only execute if there is a state change
if(this.selectedState !== e.target.options[e.target.selectedIndex].rel){
// get state name and file path
var stateName = e.target.options[e.target.selectedIndex].rel;
var stateFile = e.target.options[e.target.selectedIndex].value;
// get the state file
this.getStateFile(stateFile);
this.selectedState = stateName;
}
}
getStateFile:function(stateFile){
new Ajax.Request(stateFile, {
method: 'get',
onSuccess:function(transport){
// insert markup into container
var markup = transport.responseText;
// NOTE: select which part of the fetched page you want to insert,
// this code was written to grab the whole page and sort later
this.resultsContainer.update(markup);
var timeout = setTimeout(function(){
// focus on new content
this.resultsContainer.focus();
}.bind(this), 150);
}.bind(this)
});
}