
Yesterday I read an article by Aaron Gustafson on A List Apart regarding hiding elements with JavaScript and the accessibility implications depending on the technique used. As mentioned by Aaron the common way this is done in libraries, including jQuery, is by manipulating the display property.
In the article Aaron discusses five ways to hide content and what the accessibility effect is of each of these. Out of the five there is only one that passes the accessibility checklist and that is:
position:absolute; left:-99999em;
With this I decided to have a look at the jQuery source and see whether it would be possible to change, or enhance, the hide function to use this positioning as apposed to the current use of the display property. Turns out, it is.
The jQuery hide() Fucntion
hide: function(speed, easing, callback) {
if (speed || speed === 0) {
return this.animate(genFx("hide", 3), speed, easing, callback);
} else {
for (var i = 0, j = this.length; i < j; i++) {
if (this[i].style) {
var display = jQuery.css(this[i], "display");
if (display !== "none" && !jQuery._data(this[i], "olddisplay")) {
jQuery._data(this[i], "olddisplay", display);
}
}
}
// Set the display of the elements in a second loop
// to avoid the constant reflow
for (i = 0; i < j; i++) {
if (this[i].style) {
this[i].style.display = "none";
}
}
return this;
}
}
Looking at the hide function of jQuery above, there are two points in the code that is relevant to what we want to accomplish. The first part is this:
var display = jQuery.css(this[i], "display");
if (display !== "none" && !jQuery._data(this[i], "olddisplay")) {
jQuery._data(this[i], "olddisplay", display);
}
First the current value of display is read from the element and stored in the display variable. Next, there is a check to see whether the element is already hidden and that there is no key called ‘olddisplay’ in the data object for this element. If both passes, a new entry is added storing the element’s current display state.
After this it enters another for loop setting the display of the element:
if (this[i].style) {
this[i].style.display = "none";
}
This is then where the changes need to be made:
var position = jQuery.css( this[i], "position" ),
left = jQuery.css( this[i], "left" );
if ( left !== "-99999em" && !jQuery._data( this[i], "oldposition" ) ) {
jQuery._data( this[i], {
"oldposition" : position,
"oldleft" : left
});
}
For the changes we now need to track two properties, the current position as well as the left offset. The checks inside the if also needs to change so that we check for the left offset not being -99999em and that the ‘oldposition‘ key does not exist in the data object for the current element.
Again, if the above passes, we store these on the data object for the current element. Next, we move on to hide the element:
if ( this[i].style ) {
this[i].style.position = "absolute";
this[i].style.left = "-99999em";
}
So as mentioned before, and in the article, we are now using positioning to hide the element and not display.
The show() Function
However, it is not possible to change hide() in this way without affecting the show() function so next, we need to make some changes to show().
show: function(speed, easing, callback) {
var elem, display;
if (speed || speed === 0) {
return this.animate(genFx("show", 3), speed, easing, callback);
} else {
for (var i = 0, j = this.length; i < j; i++) {
elem = this[i];
if (elem.style) {
display = elem.style.display;
// Reset the inline display of this element to learn if it is
// being hidden by cascaded rules or not
if (!jQuery._data(elem, "olddisplay") && display === "none") {
display = elem.style.display = "";
}
// Set elements which have been overridden with display: none
// in a stylesheet to whatever the default browser style is
// for such an element
if (display === "" && jQuery.css(elem, "display") === "none") {
jQuery._data(elem, "olddisplay", defaultDisplay(elem.nodeName));
}
}
}
// Set the display of most of the elements in a second loop
// to avoid the constant reflow
for (i = 0; i < j; i++) {
elem = this[i];
if (elem.style) {
display = elem.style.display;
if (display === "" || display === "none") {
elem.style.display = jQuery._data(elem, "olddisplay") || "";
}
}
}
return this;
}
}
The part to focus on here is the second loop:
if (elem.style) {
display = elem.style.display;
if (display === "" || display === "none") {
elem.style.display = jQuery._data(elem, "olddisplay") || "";
}
}
At this point we are not interested in the value of the display property anymore and instead want to have a look at the left offset. As above, if the checks pass we will set the properties of the element back to the previous values that we stored in the data object.
if ( elem.style ) {
var left = elem.style.left;
if ( left === "-99999em") {
elem.style.position = jQuery._data(elem, "oldposition") || "";
elem.style.left = jQuery._data(elem, "oldleft") || "";
}
}
With that, we are done or, so I thought. Turns out, everything is fine, unless you pass a speed parameter, such as ‘slow’, to the show() function. I had to do a little debugging to find the root of the problem but, the reason is that the animate function that is called when passing in the speed parameter is dependent on the state of the element being hidden.
With this not being the case anymore, simply nothing happens when animate is called. So here we are at a bit of a cross road. Change the animate function to check for the new properties and not display anymore or, abandon the idea. But wait, there is a third option. What if we set the element to be hidden just before we call the animate function? This will not hurt the accessibility of the element as it will have this state for a very small amount of time, literally less then a second.
To implement this look at the following lines in the show function:
if (speed || speed === 0) {
return this.animate(genFx("show", 3), speed, easing, callback);
}
We need to add just one line to fix the problem:
if ( speed || speed === 0 ) {
this[0].style.display = "none";
return this.animate( genFx("show", 3), speed, easing, callback);
}
Problem is, now that we have set the element to display:none the element will keep this state and will not be set to visible at the end of the animation because we are not setting it nor are we storing it’s initial state in the data object. Let’s change that.
Revisiting the code that sets the properties of the element, we need to add another line here:
if ( left === "-99999em") {
elem.style.display = jQuery._data(elem, "olddisplay") || "";
elem.style.position = jQuery._data(elem, "oldposition") || "";
elem.style.left = jQuery._data(elem, "oldleft") || "";
}
The above will not work however until we fix the hide function as well. In hide() we have to change two aspects. First we need to store the initial value in the data object again:
if ( this[i].style ) {
var display = jQuery.css( this[i], "display" ),
position = jQuery.css( this[i], "position" ),
left = jQuery.css( this[i], "left" );
if ( left !== "-99999em" && !jQuery._data( this[i], "oldposition" ) ) {
jQuery._data( this[i], {
"olddisplay" : display,
"oldposition" : position,
"oldleft" : left
});
}
}
One last thing to do. With the above, the display property will be set during the show function but will not be changed during hide(). So, to complete the change, change the ‘if’ statement of hide that sets the properties to the following:
if ( this[i].style ) {
this[i].style.display = "";
this[i].style.position = "absolute";
this[i].style.left = "-99999em";
}
And with that, it is done. We now have a hide function that uses positioning as apposed to manipulating the display property. I have a simple test page over here showing that all of this still works, the JSLint tests all pass when building jQuery with these changes in there but, more testing will need to be done.
My fork of jQuery with this change is available on Github and I would love to get feedback and hear your comments on this change.
UPDATE: I removed my fork of jQuery with the change as I will be releasing a ’plugin’ in the next couple of days.
Image courtesy: nick wright planning
