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 Preferences→System→Universal 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
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.
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
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:
Create the new
textNode
.Create the parent element.
Append the new
textNode
to the parent element.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
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.
<!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.
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.
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.
<!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.
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
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.
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.