Moving Focus for Accessibility 1.1.3

C. Blouch

June 14, 2006

Introduction

Jaws and other screen readers were created in the Web 1.0 world where a page generally loaded up once and then the user clicked a link which either moved to another part of the page or loaded another page. As we all know this isn't the way things work in Web 2.0. In many cases the page is only loaded once while pieces are dynamically updated and links are little more than hooks for JavaScript functions to connect to the DOM. Not only are the pieces of content updating, but even the DOM structures to hold the updated content are being generated on the fly via DOM node manipulation in Javascript. All of this has thrown screen readers like Jaws for a loop as they have no idea where to move focus or what to read. Often times clicking a link, which appears to go to "#" really is creating a new DOM structure with new content.

There have been hacks to try and 'trick' the screen readers into noticing updated content static containers such as documented in my Ajax and Accessibility presentation. This class of solutions still breaks down when dynamic node manipulation is thrown into the mix. The solution documented here lets you steer the focus of the screen readers by moving focus to an arbitrary DOM node at will via Javascript.

Files

readme.htmlWhat you are reading right now
focus.htmlA working example/test environment
focus.jsThe commented JS library which makes it go
focus.cssSome basic css to make the boxes, columns and pretty colors

Solution

To solve this we need to bend some W3C rules. In particular the tabIndex attribute lets the browser know what is to be focused and in what order. Supposedly the tabIndex attribute is limited to certain tags such as form elements and anchors. In addition, it is supposed to be set to a 0 or higher value. We will ignore both of these constraints and add tabindex to any DOM object we want to focus on and set tabindex="-1" to enable focusing on that object. Problem is how to do this correctly via JavaScript and not bloat our markup with tabindex attributes all over the place. We also want to avoid mucking with the tabindex attribute on DOM nodes that are already focusable such as A and INPUT nodes. With the attribute properly set on a DOM node we can put focus there anytime.

Test Environment

I created focus.html as a test page with several useful features to prove what was happening and when.
  1. Clicking on "Add 2nd DIV" inserts a dynamically created DIV between the header and footer DIV. The content of this DIV is controlled by the HTML in the green edit box. You can modify the HTML at will and any additional DIVs you create will contain what is currently in the green box.
  2. Clicking on "Move Focus" will look for a DOM node with a class named in the white box. In the pre-populated example HTML I used "lookatme" as the class on the first LI. So clicking "Move Focus" will move the browser's focus to the DOM object with the class shown in the white box. In practice you could be getting a handle to the DOM object by any number of means besides the class.
  3. "Delete 2nd DIV" does just that. You can Add as many DIVs as you want and Delete removes the 2nd instance up until there is nothing left but the header and footer.
  4. Several 'honeypot' links are provided right below the main action links so if focus doesn't move as expected chances are good you will land on "This is not the link you are looking for".
  5. All internal actions are logged in realtime in the little darker-blue log box. This makes it easier to understand how the code works by gaining visibility into what happened and when. Because web applications are doing Ajax, JiLL or Datapipe operations, using alerts will often times mask bugs by forcing asynchronous operations in to a synchronous sequence. Realtime logging eliminates this side-effect when debugging.

How it Works

We won't go over all the DOM manipulation used to add/remove/update the page content. The JavaScript code is fully commented on those points and reviewing those techniques are not the purpose of this document. The core function to this whole thing is the move_focus() function which looks like this:
var move_focus_obj;
function move_focus(dom_object,tabvalue){
	var focus_exceptions={A:1,AREA:1,BUTTON:1,INPUT:1,OBJECT:1,SELECT:1,TEXTAREA:1};
	if(	tabvalue ||
		(!focus_exceptions[dom_object.nodeName] && 
		(dom_object.getAttribute("tabIndex")==null || !dom_object.getAttributeNode("tabIndex").specified))
	)
	{
		if(!tabvalue)tabvalue=-1;
		dom_object.setAttribute("tabIndex",tabvalue);
	}
	if(document.all)dom_object.focus();
	move_focus_obj=dom_object;
	setTimeout("move_focus_obj.focus()",0);
}
Lets walk though what's happening.
  1. The move_focus_obj is a global var used to temporarily hold a node pointer for focusing later as part of a setTimeout hack (described later).
  2. The first parameter passed in is a DOM object. We don't care how you got the object. You could have used document.getElementByID("SomeID") or document.getElementsByTagname(object,"div")[3] or whatever. All this function cares about is that the node exists in the DOM and you want focus moved to it.
  3. The second parameter is an optional value to set tabindex to. If the optional tabvalue has been passed in we ignore all the other checks and assume the user wants to set a specific tabindex value before we set focus on that node. If this value is not passed in then we assume it should be set to -1 and do the other checks for node validity.
  4. If we're not using a passed-in tabvalue then we check to make sure the node is the type that needs the tabindex attribute. We do this by checking the .nodeName of the object and using that as the index into our exception array. If it is found in the array we fall through and just leave the node alone. Otherwise we continue with our 'safety' checks.
  5. Before we go adding tabIndex to this node we need to make sure that it doesn't already have a tabIndex attribute. Of course IE does some winky things so we have to do some special stuff.

    Firefox
    .getAttribute("tabIndex") returns a null when there is no tabIndex, so if we get a null then we can go ahead and add tabIndex to the node.
    If .getAttribute("tabIndex") doesn't return a null then there is a an existing tabIndex so we leave things alone.

    IE
    Unfortunately in IE .getAttribute("tabIndex") returns a 0 even when the tabIndex attribute does not exist. Therefore we don't know if there was no tabIndex attribute or if it was set legitimately to 0 elsewhere. To get around this we check .getAttributeNode("tabIndex").specified to see whether the tabIndex value was specified or is just a browser default value. If the node has no tabIndex attribute the first part of if should resolve to false in IE as 0!=null but the second part should be true because tabIndex is being set to 0 as a browser default, not by the HTML or a script. Therefore we go ahead and add a tabIndex.

    Note:
    Doing a getAttributeNode().specified in Firefox when there is no tabIndex breaks because we would be attempting to invoke the .specified method on null, but this should never happen because of the way the if is constructed.
    If the node's tabIndex is null the first part of the if should evaluate to true, causing execution to immediately flow into the body of the if, avoiding the attempt at invoking methods on null in the latter half of the if.
    If the node's tabIndex is not null then the both the .getAttribute("tabIndex") and !getAttributeNode("tabIndex").specified should evaluate to false leaving the existing tabIndex alone. In the case of FireFox, because the node's tabIndex is not null the getAttributeNode("tabIndex").specified works fine.

    Also note:
    tabIndex parameter passed into the getAttribute and getAttributeNode methods must have the capital "I" to work in IE, even though the html is specified as tabindex. Firefox appears to be happy either way.
  6. Now that we have figured out if there was a tabIndex for this node we can set one if needed. We used a -1 if no particular value was passed in.
  7. After all that, with a tabIndex in place, you would think we could just invoke .focus() on the node. As it turns out there is a bug in IE where sometimes it can't seem to focus on newly created DOM nodes for a brief period of time. We get around this by calling the .focus() method twice when in IE. We protect other browsers from this total hack by checking for document.all before doing the second focus.
  8. The regular .focus() also has issues in that some browsers seemed to be unable to find the node to focus on and would instead scroll the page around. To hack around this we store the object in the global var and then do a 0ms setTimeout on invoking the .focus() method on this global var. It has to be global because when setTimeout wakes up the function it calls will be in the global scope, not in the move_focus() function scope, so we had to make sure the pointer to the DOM node was in the global scope. Note that this bug only appeared on more complex pages so you might be able to get away without it, but you've been warned.

  9. My Theory:
    The aol.com folks found that, on their complex page, using setTimeout to delay the .focus() invocation (a common workaround) would sometimes fail even when waiting up to 1500ms. I suspect this issue is being triggered by garbage collection going on after the innerHTML update, which interferes with the completion of the DOM updates. This would cause a brief delay between the return of control to the JS thread and the new DOM objects actually being available. As the page becomes more complex and garbage collection takes longer the timing gap between completion of the innerHTML update and DOM objects actually being available for manipulation grows. Maybe these .focus() hacks triggers the garbage collection causing them to block until the collection is complete, allowing them to execute normally after that.

  10. We currently do not remove the tabindex="-1" attribute. Having it does not hurt anything visually or with tab order and doing less DOM node attribute create/destroy operations means less potential for hitting a memory leak or other subtle bugs. I'm also concerned that removing the attribute would cause focus to be removed from the element in some combination of browser and screen reader. If a bad side effect was discovered it would be rather easy to reset the attribute to null for FF or 0 for IE after moving focus.

Big Huge Caveat

As of this writing we have discovered a bug in Freedom Scientific (FS) Jaws screen reader where if you are in IE and are not in forms mode it will fail to follow the focus change. We have verified that the MSAA has been updated correctly which, in theory, is what Jaws uses as visibility into what's on the page. Don Evans and Tom Woldkowski have been in contact with the FS folks who verified that it is a bug on their end. FS is saying the bug is an easy fix and should have a new build out in a week or two. In other words, you should use this technique now and let Jaws catch up.

MSAA Inspector

If you would like visibility into where the MSAA thinks focus is you can use the Microsoft Active Accessibility Object Inspector found here. On the web page scroll down and download inspect32.exe. After uncompressing this run inspect32 which should pop up a small panel of MSAA probing tools. In the first toolbar the 8th item from the left is a gold box button. Click that to turn on the gold box which will show where the MSAA thinks focus is in your browser. This works for both IE and Firefox.

Test Cases

We assume you're running MSAA inspector with the Gold box turned on to see where focus is moving.

Case 1
Click on Add. You should see a new Yellow box appear. Now click Move Focus. You should see focus move to "This is some" in the unordered list in the Yellow box. Notice in the HTML there was no tabindex set and that the LI does not contain a link or any other normally focusable html tags. Also notice in the log that tabindex said it was 0 in IE or null in FF. The code correctly identified that there is no tabindex attribute so it added one, set it to -1 and then moved focus to that node. Click Delete to remove the Yellow DIV.
Case 2
In the Green html box modify the first LI to add a tabindex="0" attribute. Click on Add. You should see a new Yellow DIV with updated content. Click on Move. Notice that both FF and IE identify the tabindex attribute as being set to 0 (and not just missing) so it is left unmolested and with focus moved to it. Click Delete to remove the Yellow DIV.
Case 3
Modify the first LI to set tabindex="3". Click on Add. You should see a new Yellow DIV with updated content. Click on Move. Notice that both FF and IE identify the tabindex attribute as being set to 3 so it is left alone with focus moved to it. Click Delete to remove the Yellow DIV.
Case 4
Modify the green box to make a form input as the focusable item. You could use html like this:
<form><input type="text" id="lookatme" /></form>
Click on Add. You should see a new Yellow DIV with a form field in it. Click on Move. You should see focus moved to the form field with the cursor now in the text box. You should also see in the log that none of the tabindex attribute updates were executed. Click Delete to remove the Yellow DIV

Other special coding techniques

There are several nice JS coding techniques to highlight in this demo.

Browser Support

This technique was tested on Firefox 1.5 and IE 6.0.2900.2180 on WindowsXP with Jaws 6.1 and 7.0 and appears to work fine except for the previously noted caveat. We also noticed Safari was not honoring our .focus() call but did not investigate why.
Version History
1.1.3 - 6/14/2005
[New Feature] - move_focus now accepts an optional tabvalue parameter in addition to an object. If the tabvalue is passed in, tabindex will be set to this value without checking the object type or previous values. This lets move_focus become a more generalized method for setting tabindex values on all objects in addition to setting focus on stubborn ones.
[Bugfix] - The aol.com folks discovered that some lower versions of non-IE browsers didn't like the double-focus hack (they would scroll the page around) so we now only do the second focus when in IE by checking for document.all.
[Bugfix] - Some older browsers (FF 1.0.4, NS 7.2 and below) seemed to get confused if we focused on an item right after creating it and instead scrolled the page around, apparently unable to find the node to focus on. We now execute the focus inside a 0ms setTimeout delay. This hack appears to work around the problem and does no harm.

1.1.2 - 5/24/2006
[bugfix] - My getElementsByClassName method didn't check for word boundaries so searching for objects with class "box" and "bob" would both get returned when looking for just "bo". Fixed.
[Bugifx] - Mistyped OBJECT in the exception list of types to not add tabindex to.

1.1.1 - 5/24/2006
[Bugfix] - Turns out that calling .focus() method twice has same effect as doing the setTimeout(). Michael Richman from aol.com noted that they were able to reproducce failed focus moves with setTimeout waiting up to 1500ms. With the double focus it now seems to be reliable. Removed all the setTimeout and global var stuff and updated readme.

1.1 - 5/24/2006
[Bugfix] - If object was of type that didn't need the tabindex hack (A, INPUT etc) we now leave them alone. Adding tabindex="-1" to these objects would remove them the default tab order. Dropped the "What not to do" section which had admonished developers not to use this function on these types of nodes.
[Bugfix] - IE apparently has random issues with not moving focus to a newly created element. This bug did not appear in my test page but was discovered when the solution was implemented on the much more complex www.aol.com site. After much research it appears that the common workaround is to simply delay the .focus() method invocation by using setTimeout. Some places say 0 or 1ms are sufficient while others use 100m or more. I'll stick with 100ms for now. Unfortunately I was unable to reproduce the bug on my XP/IE box either on aol.com or my test page so this may not guarantee a resolution. At worst case our universal focus is no more broken than a plain old object.focus. Theory: I suspect this is being triggered by garbage collection going on after the innerHTML update interfering with the completion of the DOM update. This would cause a brief delay between the return of control to the JS thread and the new DOM objects actually being available. As the page becomes more complex and garbage collection takes longer the timing gap between completion of the innerHTML update and DOM objects actually being available for manipulation grows.
[Bugfix] - Because we may be adding multiple DIVs with identical HTML inserted we could easily have had the same ID multiple times in the DOM, which is not legal. Changed example code to instead assign a class to the node to be focused and updated moveit() to focus on the first instance of a DOM node with the class specified in the "Class of DOM object to focus on" form field. Wrote a new document method getElementsByClassName(string) which returns an array of DOM nodes which have a class of 'string'.