JavaScript: Accessible Search Toggle

This post is now out-of-date. Please refer to jQuery accessible search toggle instead.

To use a script in a way that ensures users without JavaScript get the same functionality as users with it enabled means it is accessible. It doesn’t necessarily mean the same user experience, but both must be able to view the same information. An example of this is using a script to open a new window to show some extra information: with script disabled the user should be able to view the same information, but within the same window.

Toggle via Radio Buttons

I want to allow the user to search within all sections of a website or to search within one or more chosen groups. The list should only be visible when the user selects that they want to choose groups. To do this I am going to use 2 radio buttons to toggle the unordered list of sections on and off.

To start an accessible piece of JavaScript and content, I suggest laying out the HTML as you want it to display when JavaScript is disabled:

<p>
  <input type="radio" name="group" checked="checked" /> 
    Search all groups 
  <input type="radio" name="group" /> 
    Search chosen groups 
  <input type="submit" name="submit" value="Search" />
</p>
<div id="groupOptions">
  <ul>
    <li><input type="checkbox" />Group 1</li>
    <li><input type="checkbox" />Group 2</li>
    <li><input type="checkbox" />Group 3</li>
    <li><input type="checkbox" />Group 4</li>
    <li><input type="checkbox" />Group 5</li>
    <li><input type="checkbox" />Group 6</li>
  </ul>
</div>

Label tags should be added to improve usability and to allow screen readers to associate the correct label with it’s input.

<input type="checkbox" value="1" id="group1"/>
<label for="group1">Group 1</label>

Hunt the List

The list will be hidden using JavaScript; written in English it works as follows:

  1. Check the browser understands what we’re about to tell it
  2. Find the div with an id of “groupOptions”
  3. Change the style properties of that div to hide it
function hideOptions() {
  if( !document.getElementById ) return false;
  var opts = document.getElementById( "groupOptions" );
  opts.style.display="none";
}

Found the List

When the user clicks “Search chosen groups” the list should be made visible. The radio buttons need a ‘hook’ for the JavaScript to know when to fire the method when we click a particular radio button, this is added using a class name.

<input type="radio" id="togOn" name="group" class="toggleOn" 
    checked="checked" />
<label for="togOn">Search chosen groups</label>

To do this in English:

  1. Check the browser understands what we’re about to tell it
  2. Find all the input tags
  3. Loop through them to see which has a class name of “toggleOn”
  4. If it’s found, when it gets clicked then change the list’s style to be visible
function toggleOptions() {
  if(!document.getElementById || 
     !document.getElementsByTagName ) return false;
  var groups = document.getElementsByTagName("input");
  for(var i=0; i < groups.length; i++) {
    var clsName = groups[i].getAttribute("class");
    if(clsName == "toggleOn") {
      var opts = document.getElementById("groupOptions");
      groups[i].onclick = function() { 
        opts.style.display="block"; 
      }
    }
  }
}

The final part is to hide the list if the user clicks “Search all groups”. Add the ‘hook’ class name to the input and add another if condition to toggleOptions() function:

...
    if(clsName == "toggleOn") {
      var opts = document.getElementById("groupOptions");
      groups[i].onclick = function() { 
        opts.style.display="block"; 
      }
    }
    else if(clsName == "toggleOff") 
      groups[i].onclick = hideOptions;
...

Putting it all together

After putting this together you can see that a couple of var‘s are used in multiple places, so these can be moved out of the functions and created globally and then referenced as follows:

function setVars() {
  if(!document.getElementById || 
     !document.getElementsByTagName ) return false;
  groups = document.getElementsByTagName("input");
  opts = document.getElementById("groupOptions");
}

Referenced in functions as: window.groups or window.opts

The functions have to be called after the page has finished loading, because methods like getElementById and getElementsByTagName only work once the DOM has been created. This is done by calling the methods to create the global variables, hide the list and to poll for showing the list all after window.onload.

window.onload = function() {
  var groups, opts;
  setVars();
  hideOptions();
  toggleOptions();
}

You can see the finished script in action.

IE Spoils the Party

Unfortunately, IE does not understand the method getAttribute(). So where getAttribute("class") has been used, substitute it with className which will work across the various browsers.

Naughty Flasher

If the page is very large or if the page never fully loads (i.e. if a single image fails to load) the user will be shown the list from anywhere between a millisecond to forever.

There are 3 solutions to this problem:

  1. Do nothing: if the users/clients want accessible JavaScript and content then they will have to live with it
  2. Make it inaccessible: use display:none in the stylesheet to hide the div from the beginning. The question here is whether a user without JavaScript is discriminated against by not having the added functionality. They can still find any piece of information, but just can’t have the option filter by groups. Is it an added bonus feature or a necessary function?
  3. Use invalid HTML: adds to solution 2. Use a display:block in the noscript tag to show the div when JavaScript is disabled – works well, but means the HTML is invalid (style can only be a child of the head tag)

The flash of hidden content is distracting and I find it difficult to make my code inaccessible or invalid just to solve the problem. If there isn’t another way to solve this, then I’d probably go with option 3 – but I don’t know the ramifications of putting a style inside a noscript tag.

If you’ve got a small weight page, then stick with option 1 and do nothing.