Unobtrusive, Accessible Collapsible Content

19 Sep

I’m stretching my Javascript legs a bit at the moment and am beginning to really enjoy the freedom that decent DOM support across the major browsers offers.

I recently had occasion to create an ‘events’ page which will be inserted into an upcoming project. As there will be a lot of events being added to this page I wanted to create it as a collapsible unordered list. But as a Javascript ‘workout’ for myself I resolved that I would only use Javascript that was unobtrusive, degraded gracefully and was accessible as possible. that meant no onclick attributes, no ‘#’ in href attributes and code that was valid and clean.

Here’s the markup:

<h1>Events Listing</h1>	
	
<p>Browse the listing for upcoming events</p>
			
<ul class="open">
	
<li>
		
<h2>4th Annual &ndash; Summit</h2>
			
<ul id="ul_item1" class="opensub">
<li id="item1_1">
<h3>22nd September 2005, Main Campus</h3>
<p>blah blah blah....</p>
</li>
</ul>
		
</li>
					
<li>
		
<h2>Managing the Market Or Some Such Bollocks</h2>
			
<ul id="ul_item2" class="opensub">
<li id="item2_1">
<h3>24th November 2005. The Conference Centre</h3>		
<p>etc etc etc...</p>
</li>
</ul>					                             
		
</li>
	
</ul>

Fairly self-explanatory markup. The sublists hold the content but users with Javascript and CSS enabled will get a clickable link (not shown in this markup – patience my precious…) whilst those without will just get this.

I then wrote three fairly simple javascript codeblocks (two functions and one class). Function number one I called ‘toggle’ and thats what it does. When a specific link is clicked, it either shows or hides an element. Like so:

function toggle(id){
var ul = "ul_" + id;
var img = "img_" + id;
var ulElement = document.getElementById(ul);
var imgElement = document.getElementById(img);
if (ulElement){
 if (ulElement.className == "closed"){
  ulElement.className = "opensub";
  imgElement.src = "opened.gif";
  }else{
  ulElement.className = "closed";
  imgElement.src = "closed.gif";
  }
 }
}

So, the function is passed a value referenced as ‘id’ (more on that later). It then appends that passed value onto the end of two elements and applies a class of ‘closed’ or ‘opensub’ as appropriate.

What I wanted to do next was ensure that if the user didn’t have Javascript then the ‘opensub’ CSS class was applied (as we need to present the whole page to them) and if they do have Javascript then we need to apply the ‘closed’ class to the element.

I also wanted to ensure that if the user did have Javascript then he existing markup above was annotated with markup that will make each heading a clickable link and provide a little feedback in the form of an image. Hence:

function initState () {
var lists = document.getElementsByTagName("ul");
for (var j=0; j<lists.length; j++) {
 if(lists[j].className == "opensub") {
  lists[j].className = "closed";
 }
}	
var subHeads = document.getElementsByTagName("h2");
for (var k=0; k<subHeads.length; k++) {
 var arrImg = document.createElement("img");
 arrImg.src="closed.gif";
 arrImg.id="img_item" + [k+1];
 subHeads[k].appendChild(arrImg);
 var holdText = subHeads[k].firstChild.data;
 subHeads[k].firstChild.data = "";
 var placeAnchor = document.createElement("a");
 placeAnchor.href = "test.htm";
 placeAnchor.id = "item" + [k+1];
 placeAnchor.className = "ddown";
 placeAnchor.appendChild(document.createTextNode(holdText));
 subHeads[k].appendChild(placeAnchor);
 }
}

This function has two main parts. The first part (knowing that the user does have javascript installed – we'll see how later) turns all instances of 'opensub' (which displays to the page by default) to 'closed' – meaning a user with Javascript gets the 'rolled up' items. The second part hunts for all h2 elements and creates the markup for both the image and the clickable text in the heading element. These two lines:

var holdText = subHeads[k].firstChild.data;
subHeads[k].firstChild.data = "";

firstly, get the value of the text in the

element, stores it for later use and then gets rid of it – if it didn’t we’d have both the existing text plus the clickable text. Which would be crap.

Once all this is coded we need a way to fire this whole process. I did this in the class below:

window.onload = function() {
  if (!document.getElementsByTagName) return false;
  initState();
  var lnks = document.getElementsByTagName("a");
  for (var i=0; i<lnks.length; i++) {
    if(lnks[i].className == "ddown"){
      lnks[i].onclick = function() {
        toggle(this.getAttribute("id"));
        return false;
      }
    }
  }
}

The crucial line is line 2:

if (!document.getElementsByTagName) return false;

This says that if the browser doesn't support the prerequisite method then the whole things off, the extra markup doesn't get created and everything appears on the page all at once.

This function also passes the 'id' value to the 'toggle' function by getting the id attribute value of the link thats been clicked.

To complete this whole thing we lastly need to create the CSS that will do the actual showing and hiding:

h2 a {
display: inline;
}

.open {
display: block;
}

.closed {
display: none;
}

ul li,
ul {
list-style-type: none;
padding: 0;
margin: 0;
}

.open li img {
vertical-align: middle;
}

And thats that – job done. See it in action or download the whole thing here.

Amended

I thought it might be nice to see if we could close every other entry when one entry is clicked – this means only one entry is ever displayed which might be preferable for some. All this needs is some minor tweaks to the Javascript.

For the function initState() I added the line:

arrImg.className="ddImg";

Immediately below this one:

arrImg.id="img_item" + [k+1];

So I have a class to hook myself into in the next function – toggle(id).

I’ve added two new if statements here that firstly close up all the ul elements with the class name ‘opensub’ and then closed all the img elements with the (see above) class name ‘ddImg’. What’ll happen later in this function is that the selected id will be passed in exactly the same manner as it was before thus opening the correct list and image. Here’s the annotated code for toggle(id)

var allLists = document.getElementsByTagName("ul");
for (var x=0; x<allLists.length; x++) {
if(allLists[x].className == "opensub") {
 allLists[x].className = "closed";
 }
}
var allListImg = document.getElementsByTagName("img");
 for (var y=0; y<allListImg.length; y++) {
 if(allListImg[y].className == "ddImg") {
  allListImg[y].src = "closed.gif";
 }
}

I put this code right at the very start of the function.

And thats that. Whenever you click on an entry, all other open entries will shut automatically.

The example’s here and the download is here.

Amendement No II

And its only now of course that I remember that screenreader users invariably _do_ have Javascript turned on and that display: none hides things from the screenreader.

One frantic search later, I find the answer in the Off-Left technique. So the code for the closed class now reads:

.closed {
 position: absolute;
 left: -999px;
 width: 990px;
}

12 Responses to “Unobtrusive, Accessible Collapsible Content”

  1. Joe Clark September 19, 2005 at 21:07 #

    How does this approach compare with Cameron Adams’s and Russ Weakley’s, down in Oz? (I can never find the URLs, actually.)

  2. Kev September 19, 2005 at 21:15 #

    I’ve no idea. I’ll try and mail Russ to ask him. To be honest I didn’t even know he’d done something similar. Its almost certainly better than this.

  3. Tom September 19, 2005 at 23:21 #

    hmm on my machine the image changes just after the content has popped out, big of a lag. Seems to work fine, perhaps try lots and lots of events see how well it scales.

    Oh and you should also read this.

  4. Kev September 20, 2005 at 08:57 #

    Good point re: flags Tom. I’ll amend at the first opportunity.

  5. Stuart Maynard-Keene September 20, 2005 at 21:13 #

    Hi Kev, -Paul Koch did something similar – http://www.quirksmode.org/js/display.html

  6. Kev September 20, 2005 at 22:10 #

    I see what you mean Stuart. However Paul’s doesn’t seem to operate without Javascript – and his Javascript is better than mine ;o)

  7. pixeldiva September 26, 2005 at 12:42 #

    I’ve also had something quite similar in use on my photo pages for a wihle now, after pestering a friend of mine (alangraham.co.uk) to write the javascript for me cos I’m no javascript coder.

    Example page: http://www.pixeldiva.co.uk/photo/flying-high.html

  8. Kev September 26, 2005 at 13:54 #

    Nice one pixeldiva :o)

    The only thing I don’t like is the document.write in the markup but Alan’s Javascript is much cleaner than mine. I’ll have to learn how to streamline my stuff a lot more. I’m sure a very experienced JS person would see lots of ways of cutting out redundancies. My JS skills are very much at the ‘intermediate’ level.

  9. pixeldiva September 26, 2005 at 18:43 #

    Thanks.

    The doc.write is in there because it’s about the only javascript I know how to do and I wanted to make sure that the show/hide bit didn’t show up if javascript is off (don’t see the point in letting users know there’s a widget there if they can’t use it).

    I’m still quite narked at myself that I haven’t been able to get rid of those bits if CSS is off and JS is on, but it’s something I keep going back to, and in the grand scheme of things, isn’t exactly a disaster, but the perfectionist in me would prefer it to be gone given that it’s gone when js is off.

    Who knows, I might even get it sorted by the time I’m ready to reboot.

    Mostly what prompted thinking about doing it at all (and bugging alan about it) was finding a way to make my photos accessible (to my mind) by providing an alternative, but not ruining the experience for the users who just want to look at the pretty picture. This seemed like the best compromise that wasn’t a crap compromise – if that makes sense.

  10. Mark November 4, 2005 at 22:44 #

    OK, please don’t kick me. I tried changing the H2 tag to H3 so that the links weren’t so big. I made sure to change it as well in the Style list:
    H2 a{
    changed to
    H3 a{
    but now, no links. Any ideas?

  11. Kev November 5, 2005 at 08:01 #

    HI Mark – did you change this line too?

    var subHeads = document.getElementsByTagName("h2");

  12. Jalpuna November 8, 2005 at 20:28 #

    Wow! You’ve written exactly what I’m looking for! Kudos on a job well done, and thanks for sharing!

    (a google search brought me here)

    CHEERS!

Comments are closed.