Chapter 14. Creating Interactive and Accessible Effects with JavaScript, CSS, and ARIA

14.0. Introduction

Using JavaScript with HTML and CSS to enable more interactive web pages has been around for over a decade. We’ve used the techniques to provide just-in-time help when people are submitting forms, highlight page sections, cue the users that something important has happened, or respond to mouse hover events.

These venerable techniques are now joined with a new capability: Accessible Rich Internet Applications (ARIA), a set of accessibility attributes that add accessibility to Rich Internet Applications.

This chapter is going to focus on classic interactive JavaScript techniques used across most of the Web, and requiring only basic HTML, CSS, and JavaScript skills. They’ll be freshened for modern times with the use of the ARIA functionality.

Byte for byte, the effects described in this chapter can provide the most impact with the least effort. Best of all, the effects described in this chapter are probably the most cross-browser-friendly techniques in use today. They’re certainly the most user-friendly techniques available.

The examples in this chapter work in all of the book’s target browsers. To test the screen-reader-specific accessibility components, you can install a screen reader that provides a free testing option (such as Windows-Eyes, which allows 30 minutes of testing between reboots), or a free, fully functional screen reader such as NVDA, which unfortunately only works on Windows. The Mac has built-in screen reader support with VoiceOver, but it has only limited ARIA support at this time. The Linux environment has Orca, another free option. When testing for this chapter, I used Firefox 3.5 and NVDA in Windows XP.

I’ve long known that accessibility is an important feature for web applications, but I didn’t realize until I was working on the examples for this chapter how fun developing for accessibility can be. Seeing previously silent, verbally unresponsive applications suddenly come alive when I turned on NVDA was rather amazing, and I must confess to going a little crazy trying out as many ARIA roles, relationships, and statuses as I could given the book deadline and size considerations.

Note

The ARIA capabilities are described in a set of documents created via the WAI-ARIA (Web Accessibility Initiative-Accessible Rich Internet Applications), an initiative under the auspices of the W3C. For more information on WAI-ARIA, see the WAI-ARIA overview page.

Though much of the effort for accessibility has been focused on those needing to use screen readers, the concept of accessibility covers a broad range of impairments:

  • Visual impairment, covering complete loss of vision, but can also include partial loss of vision, eye strain, monitors that are too large or too small, color blindness, and fatigue.

  • Hearing loss, ranging from difficulty in hearing to complete deafness.

  • Motor impairment, covering complete immobility, where the use of a mouse is impossible, to some fine-motor impairment, where the use of the mouse in coordination with the mouse button can be a problem.

  • Cognitive impairment, covering a range of issues related to reading comprehension, reading ability and experience, memory, as well as experience with devices and with terminology.

As we go through the recipes, I’ll try to tie what I’ve covered back to these broad groupings.

See Also

Recipe 7.7 also discusses ARIA related to the drag-and-drop event handling, and the new HTML5 native drag-and-drop support.

Two Windows-based commercial screen readers are JAWS and Window-Eyes, both of which provide limited time-based testing without having to purchase the product. NVDA also works only on Windows, and is free. Orca, which works on Linux, can be found at http://live.gnome.org/Orca. VoiceOver is built-into the Mac environment, and can be controlled via System PreferencesSystemUniversal Access.

A nice overview in how to work with Firefox and NVDA can be found at Marco’s Accessibility Blog. A good overview on setting up a screen reader test environment can be found at http://www.iheni.com/screen-reader-testing/.

The WebAIM website provides a good overview of the different forms of disability types.

14.1. Displaying a Hidden Page Section

Problem

You want to hide or show a page section, as needed.

Solution

If there’s a good likelihood of the page section being needed, you can embed it in the page and display it when needed:

var msg = document.getElementById("msg");
msg.style.display="block";
msg.setAttribute("aria-hidden", "false");

and hide it, when not:

var msg = document.getElementById("msg");
msg.style.display="none";
msg.setAttribute("aria-hidden", "true");

Discussion

Elements have different CSS display settings, depending on the type of element, and to some extent, the user agent. For browsers like Opera or Firefox, a span has an inline display value, while a div element has a block display value. Regardless of element type, though, setting the display to none removes the element completely from the document layout. Visually, this means the element is removed from view and does not take up any page space or affect how any other element displays, other than its own child elements.

Assistive Technology devices also understand and support the display property, though there are some inconsistencies in AT support. The aria-hidden attribute is a way of providing precise instructions to the AT device: when true, don’t display/speak the text; if false, display/speak the text. It can be used in combination with the CSS display property to ensure the same result in visual and nonvisual environments.

Using aria-hidden with the CSS display property doesn’t require any additional work. If the browser supports attribute selectors, you can set the element’s visual display using the aria-hidden attribute:

#msg[aria-hidden=true]
{
   display: none;
}

With this CSS, when you change an element’s aria-hidden attribute, you’re also changing the CSS display property.

Note

Using this CSS style setting causes IE8 to lock up when it’s running in IE7 compatibility mode.

Being able to hide sections of a page is useful not only for hiding and displaying messages, but also for hiding form elements, especially in a form that has a lot of elements. By hiding sections of the page and displaying them only as needed, you help to create an uncluttered web page. An uncluttered page helps everyone accessing your page, but is especially helpful for those who have cognitive disabilities and who may have challenges comprehending a mess of words and form controls all at once.

However, use this functionality with care. Providing a page with labels that a person can click to reveal the form elements underneath is helpful; unexpectedly popping up new controls can both surprise and irritate the user.

To ensure the page is always accessible even with JavaScript turned off, set the material you want available to be displayed by default, and hide the sections using JavaScript when the page is loaded. Set the messages you don’t want displayed if JavaScript is disabled to be hidden by default.

See Also

See Recipe 14.5 for an example of a good implementation of hidden form content.

14.2. Creating an Alert Message

Problem

You want to alert the reader to important information.

Solution

Create a visually distinctive alert message in the web page, and ensure it’s noticed by AT devices:

function addPopUp(txt) {
   // remove old alert, in case
   var msg = document.getElementById("msg");
   if (msg)
      document.body.removeChild(msg);

   // create new text and div elements and set
   // ARIA and class values and id
   var txtNd = document.createTextNode(txt);
   msg = document.createElement("div");
   msg.setAttribute("role","alert");
   msg.setAttribute("id","msg");
   msg.setAttribute("class","alert");

   // append text to div, div to document
   msg.appendChild(txtNd);
   document.body.appendChild(msg);
}

Discussion

Unless a message is going to be displayed frequently, in my opinion it’s better to create the message and add it to the page only when needed. That way your web page is uncluttered both physically and visually, and isn’t taking up more bandwidth than necessary.

Creating a new text message is a simple four-step process:

  1. Create the new textNode.

  2. Create the parent element.

  3. Append the new textNode to the parent element.

  4. Append the parent element to the document.

In the solution, the message is going to be incorporated into a newly created div element. After the div element is created, three attributes are added. The first is the ARIA attribute role, set to alert. This should cause an AT device to interrupt what it’s doing and immediately speak the message. The second attribute is the element id, so the element can be accessed at a later time. The last is a CSS style class name. The class ensures that the message is very easy to spot in the page visually. An example of a CSS setting could be:

.alert
{
     background-color: #ffcccc; // pink
     font-weight: bold;
     padding: 5px;
     border: 1px dashed #000;
}

You can also use an attribute selector on the role attribute to style the CSS, and forgo the class name:

[role="alert"]
{
  background-color: #ffeeee;
  font-weight: bold;
  padding: 5px;
  border: 1px dashed #000;
  width: 300px;
  margin: 20px;
}

Now the element’s visual appearance matches the urgency of the ARIA role’s setting.

To ensure that messages don’t visually pile up in the page before a new alert is created, any previously created alert is removed using the removeChild method on the message element’s parent (in this example, the body element).

An alert lets people know that there’s something they need to pay attention to, whether it’s incorrect data or a session that’s about to time out. Providing simple, specific messages to the user is also going to be helpful for those who use visual assistance devices such as screen magnifiers, or have some cognitive difficulties when it comes to web form use. This includes the inexperienced user, as well as those who may be fatigued or have other comprehension problems.

Another key aspect to messages such as these is ensuring that they can be easily dismissed using both keyboard and mouse movements, as we’ll examine in more detail in later recipes.

See Also

The function to add the text demonstrated in the solution was derived from a tip in Marco’s Accessibility blog.

See Recipe 14.4 for a discussion about integrating keyboard access into mouse-based applications.

14.3. Highlighting Form Field with Missing or Incorrect Data

Problem

You want to highlight missing or incorrectly entered information in a form field.

Solution

For the fields that need to be validated, assign a function to the form field’s onchange event handler that checks whether the field value is valid. If the value is invalid, pop up an alert with information about the error, and highlight the field using a contrasting color:

document.getElementById("elemid").onchange=validateField;
...
function validateField() {

  // check for number
  if (isNaN(parseFloat(this.value))) {
     this.parentNode.setAttribute("style",
"background-color: #ffcccc");
     this.setAttribute("aria-invalid", "true");
     generateAlert("You entered an invalid value in Third Field.
Only numeric values such as 105 or 3.54 are allowed");
  }
}

For the fields that need a required value, assign a function to the field’s onblur event handler that checks whether a value has been entered:

document.getElementById("field").onblur=checkMandator;
...
function checkMandatory() {
  // check for data
  if (this.value.length === 0) {
     this.parentNode.setAttribute("style",
"background-color: #ffcccc");
     this.setAttribute("aria-invalid", "true");
     generateAlert("A value is required in this field");
  }
}

If any of the validation checks are performed as part of the form submittal, make sure to cancel the submittal event if the validation fails.

Discussion

You can’t depend on visual indicators to highlight errors, but they can be a useful extra courtesy.

Highlighting an error with color is good, but you should avoid colors that are hard to differentiate from the background. If the form background is white, and you use a dark yellow, gray, red, blue, green, or other color, there’s enough contrast that it doesn’t matter if the person viewing the page is color blind or not. In the example, I used a darker pink around the incorrect field, against a white page.

Using a color highlight for form errors is good, but it isn’t enough: you also need to provide a text description of the error, so there’s no question in the user’s mind about what the problem is.

How you display the information is also an important consideration. None of us really like to use alert boxes, if we can avoid them. Alert boxes can obscure the form, and the only way to access the form element is to dismiss the alert, with its error message. A better approach is to embed the information in the page, near the form. Fortunately, with the new ARIA roles, we can create an alert message and assign it an ARIA role of alert, alerting those using screen readers or other AT devices.

One final touch: to ensure there’s no confusion about which field is invalid, the solution also sets the aria-invalid attribute to true on the field. Not only does this provide useful information for AT device users immediately, it can be used to discover all incorrect fields when the form is submitted.

Example 14-1 demonstrates how to highlight an invalid entry on one of the form elements, and highlight missing data in another. The example also traps the form submit, and checks whether there are any invalid form field flags still set. Only if everything is clears is the form submittal allowed to proceed.

Example 14-1. Providing visual and other cues when validating form fields
<!DOCTYPE html>
<head>
<title>Validating Forms</title>
<style>
[role="alert"]
{
  background-color: #ffcccc;
  font-weight: bold;
  padding: 5px;
  border: 1px dashed #000;
}
div
{
  margin: 10px 0;
  padding: 5px;
  width: 400px;
  background-color: #ffffff;
}
</style>
<script>

window.onload=function() {

  document.getElementById("thirdfield").onchange=validateField;
  document.getElementById("firstfield").onblur=mandatoryField;
  document.getElementById("testform").onsubmit=finalCheck;
}

function removeAlert() {

   var msg = document.getElementById("msg");
   if (msg) {
      document.body.removeChild(msg);
   }
}

function resetField(elem) {
   elem.parentNode.setAttribute("style","background-color: #ffffff");
   var valid = elem.getAttribute("aria-invalid");
   if (valid) elem.removeAttribute("aria-invalid");
}

function badField(elem) {
  elem.parentNode.setAttribute("style", "background-color: #ffeeee");
  elem.setAttribute("aria-invalid","true");
}
function generateAlert(txt) {

   // create new text and div elements and set
   // Aria and class values and id
   var txtNd = document.createTextNode(txt);
   msg = document.createElement("div");
   msg.setAttribute("role","alert");
   msg.setAttribute("id","msg");
   msg.setAttribute("class","alert");

   // append text to div, div to document
   msg.appendChild(txtNd);
   document.body.appendChild(msg);
}

function validateField() {

  // remove any existing alert regardless of value
  removeAlert();

  // check for number
  if (!isNaN(parseFloat(this.value))) {
     resetField(this);
  } else {
     badField(this);
     generateAlert("You entered an invalid value in Third Field.
Only numeric values such as 105 or 3.54 are allowed");
  }
}

function mandatoryField() {

   // remove any existing alert
   removeAlert();

   // check for value
   if (this.value.length > 0) {
     resetField(this);
   } else {
     badField(this);
     generateAlert("You must enter a value into First Field");
   }
}
function finalCheck() {

  removeAlert();

  var fields = document.querySelectorAll("[aria-invalid='true']");
  if (fields.length > 0) {
    generateAlert("You have incorrect fields entries that must be
fixed before you can submit this form");
    return false;
  }
}

</script>
</head>
<body>

<form id="testform">
   <div><label for="firstfield">*First Field:</label><br />
      <input id="firstfield" name="firstfield" type="text"
aria-required="true" /></div>
   <div><label for="secondfield">Second Field:</label><br />
      <input id="secondfield" name="secondfield" type="text" /></div>
   <div><label for="thirdfield">Third Field (numeric):</label><br />
      <input id="thirdfield" name="thirdfield" type="text" /></div>
   <div><label for="fourthfield">Fourth Field:</label><br />
      <input id="fourthfield" name="fourthfield" type="text" /></div>

<input type="submit" value="Send Data" />
</form>

</body>

If either of the validated fields is incorrect in the application, the parent element’s background color is set to pink and an error message is displayed. In addition, the aria-invalid attribute is set to true in the field, and an ARIA role is set to alert on the error message, as shown in Figure 14-1.

Highlighting an incorrect form field
Figure 14-1. Highlighting an incorrect form field

Notice in the code that the element wrapping the targeted form field is set to its “correct state” when the data entered is correct, so that when a field is corrected it doesn’t show up as inaccurate or missing on the next go-round. Regardless of what event happens, I remove the existing message alert, as it’s no longer valid once the event that triggered it has been handled.

When the form is submitted, the application uses a querySelectorAll method call to check for all instances of aria-invalid set to true, and rejects the submission until these are corrected, as shown in Figure 14-2:

var badFields = document.querySelectorAll("[aria-invalid='true']");

You can also disable or even hide the correctly entered form elements, as a way to accentuate those with incorrect or missing data. However, I don’t recommend this approach. Your users may find as they fill in the missing information that their answers in other fields were incorrect. If you make it difficult for them to correct the fields, they’re not going to be happy with the experience—or the company, person, or organization providing the form.

Another approach you can take is to only do validation when the form is submitted. Most built-in libraries, such as the jQuery Validation plug-in, operate this way. Rather than check each field for mandatory or correct values as your users tab through, you only apply the validation rules when the form is submitted. This way people who want to fill out the form in a different order may do so without getting hit with irritating validation messages as they tab through. This approach is a friendlier technique for those people using a keyboard, rather than a mouse, to fill in the form. Or you can use a mix of both: field-level validation for correct data type and format, form-level validation for required values.

Attempting to submit a form with inaccurate form field entries
Figure 14-2. Attempting to submit a form with inaccurate form field entries

Using JavaScript to highlight a form field with incorrect and missing data is only one part of the form submission process. You’ll also have to account for JavaScript being turned off, which means you have to provide the same level of feedback when processing the form information on the server, and providing the result on a separate page.

It’s also important to mark if a form field is required ahead of time. Use an asterisk in the form field label, with a note that all form fields with an asterisk are required. Use the aria-required attribute to ensure this information is communicated to those using assistive devices.

See Also

See Recipe 14.1 for more information about displaying and hiding page sections, such as messages, and Recipe 14.2 for information about displaying error messages and other alerts.

14.4. Adding Keyboard Accessibility to a Page Overlay

Problem

You’ve created a page overlay in order to display a larger image (or text, or other content), and you want it to be keyboard accessible.

Solution

Add keyboard listening to the page to complement the mouse events:

// mouse click on image within link
function imgClick() {
   var img = this.firstChild;
   expandPhoto(img.getAttribute("data-larger"));
   return false;
}

// key press on image within link
function imgKeyPress(evnt) {
   evnt = (evnt) ? evnt : ((window.event) ? window.event : "");
   var keycode = (evnt.which) ? evnt.which : evnt.keyCode;
   if (document.getElementById("overlay")) {
     if (keycode == 27) {
       restore();
       return false;
     }
   } else {
     if (keycode == 13) {
        var img = this.firstChild;
        var src = img.getAttribute("data-larger");
        expandPhoto(src);
        return false;
     }
   }
}

Discussion

The first step to adding keyboard accessibility into a web page is to either use elements that can receive keyboard focus (a, area, button, input, object, select, and textarea), or use the tabindex="0" setting on the element, which will make the element focusable.

The second step is to capture keyboard activity in addition to the mouse events. In Example 14-2, I’ve taken Example 13-3 from Recipe 13.6, and modified what was a mouse-event-only application to also accept keyboard events. The example creates a page overlay and displays a larger image when a person either clicks on a thumbnail image in the original page, or presses the Enter (Return) key when the image has the focus. Clicking the expanded image or pressing the Esc key removes the overlay, and returns the page to its original state.

Example 14-2. Making an overlay/photo display page keyboard accessible
<!DOCTYPE html>
<head>
<title>Overlay</title>
<style>
img
{
  padding: 5px;
  border-style: none;
}

.overlay
{
   background-color: #000;
   opacity: .7;
   filter: alpha(opacity=70);
   position: absolute; top: 0; left: 0;
   width: 100%; height: 100%;
   z-index: 10;
}
.overlayimg
{
   position: absolute;
   z-index: 11;
   left: 50px;
   top: 50px;
}
</style>
<script>

// expand photo when a/img is clicked
function imgClick() {
   var img = this.firstChild;
   expandPhoto(img.getAttribute("data-larger"));
   return false;
}

// if overlay is open, and ESC, close overlay
// account for keydown event in page
function imgKeyDown(evnt) {
   evnt = (evnt) ? evnt : ((window.event) ? window.event : "");
   var keycode = (evnt.which) ? evnt.which : evnt.keyCode;
   if (document.getElementById("overlay")) {
      if (keycode === 27) {
       restore();
       return false;
     }
   } else {
     if (keycode == 13) {
        var img = this.firstChild;
        var src = img.getAttribute("data-larger");
        expandPhoto(src);
        return false;
     }
   }
   return true;
}
// create overlay, expand photo
function expandPhoto(src) {

   // create overlay element
   var overlay = document.createElement("div");
   overlay.setAttribute("id","overlay");
   overlay.setAttribute("class", "overlay");

   // IE7
   // overlay.id="overlay";
   // overlay.className = "overlay";

   document.body.appendChild(overlay);

   // add image
   var img = document.createElement("img");
   img.src = src;
   img.setAttribute("id","img");

   // set tabindex, for focus
   img.setAttribute("tabindex","-1");

   // style image
   img.setAttribute("class","overlayimg");

   // IE7
   // img.className = "overlayimg";

   img.onclick=restore;
   img.onkeydown=imgKeyDown;

   document.body.appendChild(img);

   // focus on image in overlay
   img.focus();
}

// remove overlay and image
function restore() {

 document.body.removeChild(document.getElementById("overlay"));
 document.body.removeChild(document.getElementById("img"));
}

// add click and keyboard events
window.onload=function() {
   var aimgs = document.getElementsByTagName("a");
   aimgs[0].focus();
   for (var i = 0; i < aimgs.length; i++) {
     aimgs[i].onclick=imgClick;
   }
}

</script>

</head>
<body>
<p>Mouse click on image, or use keyboard to move to photo and hit
ENTER to expand the photo. To close expanded photo, hit ESC or
mouse click on image.</p>
<a href="dragonfly2.jpg"><img src="dragonfly2.thumbnail.jpg" data-larger="dragonfly2.jpg"
alt="image of common dragonfly on bright green and pink flowers" /></a>
<a href="dragonfly4.jpg"><img src="dragonfly4.thumbnail.jpg" data-larger="dragonfly4.jpg" 
alt="Dark orange dragonfly on water lily" /></a>
<a href="dragonfly6.jpg"><img src="dragonfly6.thumbnail.jpg" data-larger="dragonfly6.jpg"
alt="Dark orange dragonfly on purple water lily" /></a>
<a href="dragonfly8.jpg"><img src="dragonfly8.thumbnail.jpg" data-larger="dragonfly8.jpg"
alt="Dragonfly on bright pink water lily" /></a>
</body>

When I opened the new image, I assigned it tabindex of –1, to set keyboard focus. To ensure the application works with scripting disabled, the link references the larger image. When scripting is enabled, the image shows in the overlay; with scripting disabled, it opens in a separate page.

A click (rather than keypress) event is triggered when the Enter key is pressed and the focus is on one of the standard focusable elements: a, area, button, input, object, select, and textarea.

The tabbing did not work with Firefox on the Mac. It does work with Firefox on Windows, and hopefully, eventually, will work on the Mac, too. I also tested the application with Opera, Safari, and Chrome, and it worked fine. You do have to use Shift + the arrow keys to move among the images with Opera.

Safari overhauled its event system with Safari 3.1, and you no longer get a keypress event when clicking a noncharacter key. When the overlay is open, rather than capture the keypress event, you need to capture the keydown event if you’re using the Esc key to return the page to normal.

The application worked out of the box for IE8. To make the page work with IE7, the use of setAttribute and getAttribute with the class attribute should be changed to direct assignment (the downloadable example code contains a workaround).

As you can see, using tabbing within a web page is somewhat challenging. However, the future looks bright for this capability, with the new tabindex instructions in HTML5 that clarify tabbing and tabindex behavior. Instead of having to wrap images in links to make them accessible, we can just assign them a tabindex="0". However, for nonfocusable elements such as img, you do need to capture keypress and click events.

Providing keyboard access is absolutely essential for folks using AT devices, as well as people who have impaired movement. It’s also an important capability for those who are using devices that may have limited mouse capability, such as some phones. And it’s a nice enhancement for people who just prefer to use the keyboard whenever possible.

See Also

See Recipe 13.6 for a more in-depth explanation of the mouse events for the application. John Resig has an interesting post on the Safari 3.1 event-handling decision at http://ejohn.org/blog/keypress-in-safari-31/. Recipe 12.15 has more information on using setAttribute with CSS style properties.

14.5. Creating Collapsible Form Sections

Problem

You want to encapsulate form elements into collapsible sections, and expand when a label is clicked.

Solution

Use an accordion widget in combination with the aria-hidden and aria-expanded states, the tablist, tab, and tabpanel roles, and the aria-labeledby relationship indicator.

The entire set of accordion label/panel pairs is surrounded by one element, given a role of tablist and an attribute of aria-multiselect set to true to indicate that the element is a container for an accordion or multiselectable tablist. The text for the label is enclosed in a link to make it keyboard-accessible. Since these are groupings of form elements, the label/elements pair are surrounded by a fieldset, and the label is a legend element. If this application were a menu, these elements would most likely be div elements. The processing would, however, remain the same:

<form>
<div role="tablist" aria-multiselectable="true">
<fieldset>
<legend class="label" aria-controls="panel1" role="tab"
aria-expanded="true" id="label_1" >
<a href="">Checkboxes</a>
</legend>
<div  class="elements" id="panel_1" role="tabpanel"
aria-labeledby="label_1">
<input type="checkbox" name="box1" id="box1" value="one" />
<label for="box1">One</label><br />
<input type="checkbox" name="box2" id="box2" value="two" />
<label for="box2">Two</label><br />
<input type="checkbox" name="box3" id="box3" value="three" />
<label for="box3">Three</label><br />
<input type="checkbox" name="box4" id="box4" value="four" />
<label for="box4">Four</label><br />
<input type="checkbox" name="box5" id="box5" value="five" />
<label for="box5">Five</label><br />
</div>
</fieldset>
<fieldset>
<legend class="label" aria-controls="panel2" role="tab"
aria-expanded="true" id="label_2" >
<a href="">Buttons</a></legend>
<div class="elements" id="panel_2" role="tabpanel"
aria_labeledby="label_2">
<input type="radio" name="button1" id="b1" value="b one" />
<label>button one</label><br />
<input type="radio" name="button1" id="b2" value="b two" />
<label>button two</label><br />
<input type="radio" name="button1" id="b3" value="b three" />
<label>button three</label><br />
<input type="radio" name="button1" id="b4" value="b four" />
<label>button four</label><br />
<input type="radio" name="button1" id="b5" value="b five" />
<label>button five</label><br /><br />
<input type="submit" value="submit" />
</div>
</fieldset>
</div>
</form>

For each accordion label/panel pair, the label is displayed when the page is loaded, while the contents of the accordion panel are hidden. The label is given a role of tab, and the panel given a role of tabpanel. The aria-expanded attribute in the label is also set to false, to indicate to the AT devices that the panel is collapsed, and the aria-hidden is set to true on the panel, to indicate that the contents are not displayed:

// process tab panel elements
var elements = document.querySelectorAll(".elements");
for (var i = 0; i < elements.length; i++) {
    elements[i].style.display="none";
    elements[i].setAttribute("aria-hidden","true");
}

// process tab elements
var labels = document.querySelectorAll(".label");
for (var j = 0; j < labels.length; j++) {
    labels[j].onclick=switchDisplay;
    labels[j].style.display="block";
    labels[j].setAttribute("aria-expanded","false");
}

When the label is clicked, if the aria-expanded attribute is set to false, the panel is displayed, its aria-hidden attribute is set to false, and the label’s aria-expanded attribute is set to true. The opposite occurs if the aria-expanded attribute is set to true:

// when tab is clicked or enter key clicked
function switchDisplay() {

  var parent = this.parentNode;
  var targetid = "panel_" + this.id.split("_")[1];
  var target = document.getElementById(targetid);

  if (this.getAttribute("aria-expanded") == "true") {
    this.setAttribute("aria-expanded","false");
    target.style.display="none";
    target.setAttribute("aria-hidden","true");
  } else {
    this.setAttribute("aria-expanded","true");
    target.style.display="block";
    target.setAttribute("aria-hidden","false");
  }
  return false;
}
</script>

Discussion

The solution is a modification of the accordion solution in Recipe 13.5. The major structural differences are that one container element is used to wrap all of the accordion label/panel pairs, to indicate a grouping, each tab/panel pair is surrounded by a fieldset element, and the div element that acted as a label was replaced by a legend element.

One aspect of working with ARIA that I wasn’t expecting is that it led me to reexamine how I create my widget-like applications, such as an accordion. In Recipe 13.4, I didn’t group the individual accordion pairs into one cohesive whole because I didn’t need to for the solution I created, and I wanted to keep my element use to a minimum.

The ARIA attributes reminded me that there are semantics to the structure of an accordion—semantics I wasn’t capturing. For instance, I was working with a form but wasn’t using form-specific elements. In the new version, instead of using div elements for all aspects of the form element accordion, I used actual form elements: fieldset and legend.

Another structural change is that a link is used to wrap the text in the label so that the element can receive keyboard focus. Hitting the Enter key in the label triggers a click event, since a link is a focusable element. Since I’m not capturing the click event in the link, it bubbles up to the parent element—the label.

I also added an additional visual element—a background image with an arrow, to indicate the direction the panel will open, as shown in Figure 14-3.

An accessible accordion
Figure 14-3. An accessible accordion

The reason for the addition of the arrows is to ensure clarity of purpose for the grouping of elements. Those users unfamiliar with accordion applications, or who may have cognitive disabilities, can see there is an action associated with the label. I’m not sure about the type of arrow I used—a better approach might be a simple triangle.

The other change to the example was the addition of ARIA roles and states. At first, the accessibility additions may seem to be rather numerous, but each provides a specific piece of information necessary to understand what’s happening with the accordion widget. Consider how you would describe an accordion if you were describing it over the phone to a friend: “The accordion is a group of label elements, each of which has text. Each label also has an arrow, and I know from prior experience with similarly structured web page objects that clicking each label opens a panel underneath. When I do click each label, a section of previously hidden web page is displayed directly beneath the label, and I’m able to see its contents. When I click other labels, their associated panels also display. However, when I click the label for an already open label, the panel it is associated with is removed from view.”

In ARIA, these visual impressions are conveyed by the use of the tablist role on the outer container, and the role of tab on the label. In addition, the use of aria-multiselect also indicates that more than one panel can be expanded at the same time.

The fact that the panels are collapsed is indicated by the aria-expanded attribute, and that the panels aren’t displayed is indicated by the aria-hidden on each. When you open the application with a screen reader, such as NVDA in Firefox, and close your eyes, what I described is more or less what the device states.

The application works with all the book’s target browsers. However, the use of querySelectorAll doesn’t work with IE7. An alternative would be to access elements by tag name, and then check each element’s class to see how to handle it. You’ll also want to avoid using CSS attribute selector syntax with IE7.

See Also

See Recipe 13.4 for another implementation of the accordion. For an excellent demonstration of the use of the ARIA attributes with an accordion, see the accordion example at the Illinois Center for Information Technology and Web Accessibility.

14.6. Displaying a Flash of Color to Signal an Action

Problem

Based on some action, you want to display a visual cue to signify the success of the action.

Solution

Use a flash to signal the success or failure of an action. The use of a red flash is standard to signal either a successful deletion, or an error; the use of a yellow flash is typically used to signal a successful update, or action:

var fadingObject = {
   yellowColor : function (val) {
   var r="ff";   var g="ff";
   var b=val.toString(16);
   var newval = "#"+r+g+b;
   return newval;
},

  fade : function (id,start,finish) {
   this.count = this.start = start;
   this.finish = finish;
   this.id = id;
   this.countDown = function() {
      this.count+=30;
      if (this.count >= this.finish) {
         document.getElementById(this.id).style.background=
                                                    "transparent";
         this.countDown=null;
         return;
      }
      document.getElementById(this.id).style.backgroundColor=
         this.yellowColor(this.count);
      setTimeout(this.countDown.bind(this),100);
   }
  }
};
...
// fade page element identified as "one"
fadingObject.fade("one", 0, 300);
fadingObject.countDown();

Discussion

A flash, or fade as it is frequently called, is a quick flash of color. It’s created using a recurring timer, which changes the background color of the object being flashed. The color is varied by successively changing the values of the nondominant RGB colors, or colors from a variation of 0 to 255, while holding the dominant color or colors at FF. Figure 14-4 shows how this color variation works with the color green. If for some reason the green color can’t be perceived (such as this figure being in a paper copy of this book, or because of color blindness), the color shows as successions of gray. As you progress down the figure, the color gets progressively paler, as the nondominant red and blue values are increased, from initial hexadecimal values of 00, to FF(255).

The color yellow used in the solution kept the red and green values static, while changing the blue. A red flash would keep the red color static, while adjusting both the green and blue.

In the solution, I’m setting the beginning and ending colors for the flash when I call the fade method on the object, fadingObject. Thus, if I don’t want to start at pure yellow or end at white, I can begin with a paler color, or end with a paler color.

Demonstrating how a color flash effect changes
Figure 14-4. Demonstrating how a color flash effect changes

A color flash is used to highlight an action. When used with Ajax, a red flash can single the deletion of a table row just before the row is removed from the table. The flash is an additional visual cue, as the table row being deleted helps set the context for the flash. A yellow flash can do the same when a table row is updated.

A flash can also be used with an alert message. In Recipe 14.2, I created an alert that displayed a solid color until removed from the page. I could also have used a red flash to highlight the message, and left the background a pale pink at the end:

function generateAlert(txt) {

   // create new text and div elements and set
   // Aria and class values and id
   var txtNd = document.createTextNode(txt);
   msg = document.createElement("div");
   msg.setAttribute("role","alert");
   msg.setAttribute("id","msg");
   obj.fade("msg", 0, 127);
   obj.redFlash();
   msg.setAttribute("class","alert");

   // append text to div, div to document
   msg.appendChild(txtNd);
   document.body.appendChild(msg);
}

The only requirement for the solution would be to either make the color-fade effect more generic, for any color, or add a new, specialized redFlash method that does the same as the yellow.

Previously, if the color flash hasn’t been considered an accessibility aid, it’s also not considered an accessibility hindrance, either. As I mentioned earlier, it should be paired with some other event or response that provides information about what’s happening. In the code snippet, when the alert message is displayed, it’s done with a flash, but the ARIA alert role is also assigned so that those using a screen reader get notified.

How about other accessibility concerns, though, such as photosensitive epilepsy? Or those with cognitive impairments where page movement or light flickering can disrupt the ability to absorb the page content?

A simple color flash as demonstrated should not result in any negative reaction. It’s a single progressive movement, rather than a recurring flicker, and over quickly. In fact, the WebAIM website—which is focused on accessibility—makes use of a yellow flash in an exceedingly clever and accessible way.

If you access one of the WebAIM articles, such as the one on keyboard accessibility, and click one of the links that takes you to an in-page anchor, the page (once it has scrolled to the anchor location) gives material associated with the anchor a subtle and quick yellow flash to highlight its location.

In the site’s JavaScript, the author of the JavaScript writes:

// This technique is a combination of a technique I used for
// highlighting FAQ's using anchors
// and the ever popular yellow-fade technique used by
// 37 Signals in Basecamp.

// Including this script in a page will automatically do two
// things when the page loads...
// 1. Highlight a target item from the URL (browser address bar)
// if one is present.
// 2. Set up all anchor tags with targets pointing to the current
// page to cause a fade on the target element when clicked.

In other words, when the page is loaded, the author accesses all anchors in the page:

var anchors = document.getElementsByTagName("a");

The author then traverses this array, checking to see if the anchor has a fragment identifier (#):

if (anchors[i].href.indexOf('#')>-1)

If so, then clicking that link also causes the section to highlight:

anchors[i].onclick = function(){Highlight(this.href);return true};

I haven’t seen a flash used for this effect before, and it demonstrates how accessible a flash can be when used for good effect. If you’ve ever clicked an in-page link that goes to a page fragment located toward the bottom of the page, you know how frustrating it can be to find exactly which section is referenced by the link when there is not enough page left to scroll the item to the top.

Now, with the use of page-fragment link highlighting, you can immediately locate the linked section. And since the highlight fades, once the section is located, you don’t have to put up with the colored background, which can impact on the amount of contrast between color of text and color of background.

See Also

Gez Lemon has an excellent article on photosensitive epilepsy at http://juicystudio.com/article/photosensitive-epilepsy.php.

14.7. Adding ARIA Attributes to a Tabbed Page Application

Problem

You want to split the page contents into separate panels, and only display one at a time. You also want the application to be accessible.

Solution

Use a tabbed page application and include the ARIA roles tablist, tabpanel, and tab, as well as the aria-hidden state.

The tabbed page is a div element, as is the container, with the tabs as list items (li) within a single unordered list (ul) at the top. The container div is given a role of tablist, and each of the li elements given a role of tab. The tabbed panels are div elements containing whatever type of contents, and each is given a role of tabpanel. The relationship between panel and tab is made with the aria-labeledby attribute:

<div class="tabcontainer" role="tablist">
   <div class="tabnavigation" role="tab">
      <ul>
         <li id="tabnav_1" role="tab"><a href="">Page One</a></li>
         <li id="tabnav_2" role="tab"><a href="">Page Two</a></li>
         <li id="tabnav_3" role="tab"><a href="">Page Three</a></li>
      </ul>
   </div>

   <div class="tabpages">
      <div class="tabpage" role="tabpanel" aria-labeledby="tabnav_1"
aria-hidden="false" id="tabpage_1">
         <p>page 1</p>
      </div>
      <div class="tabpage" role="tabpanel" aria-labeledby="tabnav_2"
aria-hidden="true" id="tabpage_2">
         <p>page 2</p>
      </div>
      <div class="tabpage" role="tabpanel" aria-labeledby="tabnav_3"
aria-hidden="true" id="tabpage_3">
         <p>page 3</p>
      </div>
   </div>
</div>

When the page loads, the tabs are displayed and all but the first of the panels are hidden. The hidden panels are assigned an aria-hidden attribute value of true. The click event handler for all of the tab elements is assigned a function, displayPage, to control the tab page display. A custom data attribute, data-current, on the tabbed page container is used to store which tab is currently selected:

// set up display
// for each container display navigation
// hide all but first page, highlight first tab
window.onload=function() {

  // for each container
  var containers = document.querySelectorAll(".tabcontainer");
  for (var j = 0; j < containers.length; j++) {

    // display and hide elements
    var nav = containers[j].querySelector(".tabnavigation ul");
    nav.style.display="block";

    // set current tab
   var navitem = containers[j].querySelector(".tabnavigation ul li");
    var ident = navitem.id.split("_")[1];
    navitem.parentNode.setAttribute("data-current",ident);
    navitem.setAttribute("style","background-color: #ccf");

    // set displayed tab panel
    var pages = containers[j].querySelectorAll(".tabpage");
    for (var i = 1; i < pages.length; i++) {
      pages[i].style.display="none";
      pages[i].setAttribute("aria-hidden","true");
    }

    // for each tab, attach event handler function
   var tabs = containers[j].querySelectorAll(".tabnavigation ul li");
    for (var i = 0; i < tabs.length; i++) {
      tabs[i].onclick=displayPage;
    }
  }
}

When a tab is clicked, the old tabbed entry is cleared by setting the background of the tab to white and hiding the panel. The new entry’s tab is highlighted (background color is changed), and its associated panel is displayed. The hidden panel’s aria-hidden attribute is set to true, while the displayed panel’s aria-hidden attribute is set to false. The custom data attribute data-current is set to the new tab selection, and the clicked tab’s id is used to derive the related panel’s ID:

// click on tab
function displayPage() {

  // hide old selection
  var current = this.parentNode.getAttribute("data-current");
  var oldpanel = document.getElementById("tabpage_" + current);

  document.getElementById("tabnav_" + current).setAttribute("style",
  "background-color: #fff");
  oldpanel.style.display="none";
  oldpanel.setAttribute("aria-hidden","true");

  // display new selection
  var ident = this.id.split("_")[1];
  this.setAttribute("style","background-color: #ccf");
  var newpanel = document.getElementById("tabpage_" + ident);

  newpanel.style.display="block";
  newpanel.setAttribute("aria-hidden","false");
  this.parentNode.setAttribute("data-current",ident);

  return false;
}

Discussion

The code in the solution is very similar to that in Recipe 13.7; the only difference is the addition of the ARIA roles and attributes. The changes are minor—I highlighted the lines in the JavaScript that were added to enable ARIA support. As you can see, adding accessibility with ARIA is not an onerous task.

Another excellent demonstration of an ARIA-enabled tabbed page can be found at the Illinois Center for Information Technology and Web Accessibility. Though the code to manage the tabbed page behavior differs significantly from mine, the relative structure and use of ARIA roles and attributes is identical. The main difference between the two implementations is that my example uses a link to make my tabs clickable, while the external example uses tabindex. The external application also makes more extensive use of the keyboard.

As mentioned in Recipe 14.5, working with the ARIA attributes provides a new way of looking at widget-like applications like tabbed pages and accordions. The Illinois Center I just mentioned actually lists both the accordion and tabbed page example in one section specific to tabbed pages, because they have very similar behavior. The only difference is one is multiselectable, the other not; one requires modification to the label to signify which label is associated with which panel, while the other does not need this information. By looking at both types of widgets with a fresh viewpoint, I’ve learned new ways to use both: instead of creating vertical accordion panels, I’ve also started using horizontal panels; rather than tabs being located at the top of a tabbed application, I’ve started placing them on the side. It taught me to appreciate how they are both semantically linked, and how to ensure this semantic similarity is preserved in both structure and code.

See Also

Recipe 14.4 covers some of the implementation details with using tabindex.

14.8. Live Region

Problem

You have a section of a web page that is updated periodically, such as a page section that lists recent updates to a file, or one that reflects recent Twitter activity on a subject. You want to ensure that when the page updates, those using a screen reader are updated with the new information.

Solution

Use ARIA region attributes on the element being updated:

<ul id="update" role="log" aria-alive="polite" aria-atomic="true"
aria-relevant="additions">
</ul>

Discussion

A section of the web page that can be updated after the page is loaded, and without direct user intervention, calls for the use of ARIA Live Regions. These are probably the simplest ARIA functionality to implement, and they provide immediate, positive results. And there’s no code involved, other than the JavaScript you need to create the page updates.

I took the example application from Recipe 18.9, which updates the web page based on the contents of a text file on the server that the application retrieves using Ajax, and provided two minor updates.

First, I modified the code that polls for the updates to check how many items have been added to the unordered list after the update. If the number is over 10, the oldest is removed from the page:

// process return
function processResponse() {
   if(xmlhttp.readyState == 4 && xmlhttp.status == 200) {
     var li = document.createElement("li");
     var txt = document.createTextNode(xmlhttp.responseText);
     li.appendChild(txt);
     var ul = document.getElementById("update");
     ul.appendChild(li);

     // prune top of list
     if (ul.childNodes.length > 10) {
       ul.removeChild(ul.firstChild);
     }

   } else if (xmlhttp.readyState == 4 && xmlhttp.status != 200) {
     alert(xmlhttp.responseText);
   }
}

With this change, the list doesn’t grow overly long.

I made one more change, adding the ARIA roles and states to the unordered list that serves as the updatable live region:

<ul id="update" role="log" aria-live="polite" aria-atomic="true"
aria-relevant="additions s">

From left to right: the role is set to log, because I’m polling for log updates from a file, and only displaying the last 10 or so items. Other options include status, for a status update, and a more general region value, for an undetermined purpose.

The aria-live region attribute is set to polite, because the update isn’t a critical update. The polite setting tells the screen reader to voice the update, but not interrupt a current task to do so. If I had used a value of assertive, the screen reader would interrupt whatever it is doing and voice the content. Always use polite, unless the information is critical.

The aria-atomic is set to true, so that the screen reader only voices new additions. It could get very annoying to have the screen reader voice the entire set with each new addition, as would happen if this value is set to false.

Lastly, the aria-relevant is set to additions, as we don’t care about the entries being removed from the top. This is actually the default setting for this attribute, so in this case it wasn’t needed. In addition, AT devices don’t have to support this attribute. Still, I’d rather list it than not. Other values are removals, text, and all (for all events). You can specify more than one, separated by a space.

This ARIA-enabled functionality was probably the one that impressed me the most. One of my first uses for Ajax, years ago, was to update a web page with information. It was frustrating to test the page with a screen reader (JAWS, at the time) and hear nothing but silence every time the page was updated. I can’t even imagine how frustrating it was for those who needed the functionality.

Now we have it, and it’s so easy to use. It’s a win-win.

See Also

See Recipe 18.9 for more of the code for the live update.

Get JavaScript Cookbook now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.