Yes, this is a blog post about browser clicks.

On a recent project, our design dictated the use of a fixed footer. There are probably hundreds of examples of this kind of design in use, although some sites use them as toolbar trays that are collapsed by default. Basically, it’s one of those footers that’s always at the bottom of the viewport, no matter how long or short the page is, or where you’ve scrolled within the page.

A fixed footer; the browser focus is on a link which is visible.

A fixed footer; the link “1.3.1.1. Spring Dependencies and Depending on Spring” has the focus and is outlined accordingly.

An inherent problem will the typical implementation of the fixed footer is evident when a user navigates the page with the tab key instead of the mouse. If the document is longer than the browser window, and some elements are positioned below the viewport (e.g., a long form), try navigating the page with the tab key. At first, everything goes fine. Then you get to a field at the bottom of the form and the cursor is… somewhere. You can type into it — but you can’t see it, because it’s hiding behind your fixed footer.

A fixed footer; the browser focus is on a link which is not visible to the user.

The link after “1.3.1.1. Spring Dependencies and Depending on Spring” has the focus… but it’s hiding (mostly) behind the fixed footer.

When navigating a page with the keyboard, browsers are smart enough to move the page down so the focused element is in the window. Fixed footers break this behavior. To the browser, which measures the coordinates of the element relative to the page, it has moved the element into the visible viewport — it just happens to have a chunk of opaque content positioned over it. However, with some jQuery (or plain JavaScript, or any other framework), we can ensure that the element is brought into the viewport above the footer. I am never fond of using JavaScript to handle something like this but could not find a pure CSS approach that would resolve the issue, so we went with it.

The basic idea: any time a focusable element receives the focus, figure out where it is. If it’s hiding behind the footer – which you know because you know how tall the footer is – scroll the window down. This is a great place to use jQuery’s live event handler method:

$('a, input, select, textarea').live('focus', function(event) {
   var $this = $(this); // create a jQuery object from the element getting the focus
   var $window = $(window); // jQuery object of the window
   var windowTop = $window.scrollTop(); // current window scroll-y position
   var windowLeft = $window.scrollLeft(); // current window scroll-x position
   var elementBottom = $this.offset().top + $this.height(); // position of the bottom of the element
   var $footer = $('#fixedFooter');

   /* Determine the lowest acceptable visible point on the screen */
   var windowBottomPosition = $window.scrollTop() + $window.height() - $footer.outerHeight();

   /* Let's see if the element that received the focus is acceptably visible */
   var elementPositionRelativeToBottom = elementBottom - windowBottomPosition;
   if (elementPositionRelativeToBottom > 0) {
      /* It's not visible, but that doesn't mean we can't make it visible */
      window.scrollTo(windowLeft, windowTop + elementPositionRelativeToBottom);
   }
}

So that’s nice. Now, as you tab along, your elements are visible. Everything is going fine again until an overlay appears.

Because we’ve attached the focus event to all focusable elements, those same elements in your overlay all fire your focus event, which probably isn’t a big deal most of the time. It will, however, become a big deal when that element is a link positioned over your fixed footer and you click on the link in Firefox, IE, or Opera (Chrome and Safari use WebKit, which seems exempt here). Suddenly, your onclick events don’t get fired, and/or the links no longer perform navigation.

Why does that happen? The answer is found in the 2000 W3C specification on DOM events — specifically:

click
The click event occurs when the pointing device button is clicked over an element. A click is defined as a mousedown and mouseup over the same screen location. The sequence of these events is:

mousedown
mouseup
click

mousedown and mouseup get their own definitions:

mousedown
The mousedown event occurs when the pointing device button is pressed over an element. This event is valid for most elements.

  • Bubbles: Yes
  • Cancelable: Yes
  • Context Info: screenX, screenY, clientX, clientY, altKey, ctrlKey, shiftKey, metaKey, button, detail

mouseup
The mouseup event occurs when the pointing device button is released over an element. This event is valid for most elements.

  • Bubbles: Yes
  • Cancelable: Yes
  • Context Info: screenX, screenY, clientX, clientY, altKey, ctrlKey, shiftKey, metaKey, button, detail

What I’d always thought of as a “click” is really two distinct events, which may trigger up to three additional events:

* mousedown, which trigger (except in WebKit)
* focus, then
* mouseup, which triggers
* click, which triggers
* the default behavior (navigate to the href attribute), depending on what happens in the screen.

If the user drags the link around the screen, the browser fires an ondrag event repeatedly (unless it’s IE6 or 7, which don’t support ondrag) until you release the mouse button, at which point an onmouseup event fires. If the focus event fires and changes the scroll of the screen to the point where the mouse is no longer over the link, the browser treats this as a drag event (not a W3C defined event, but the world of modern browsers all seem to support it). Drag events break up the whole mouseup/mousedown sequence the browser requires to consider the event a true click – even if you were to drag the link (or the cursor, at least) back over the link, neither the click event nor the default behavior will happen.

This is all really interesting, but it doesn’t provide any solution to the outstanding issue. For us, it ended up being really simple. Our content is wrapped in a div (with an id of wrapper), but our overlays are always appended to the body of the document, so they’re always outside the wrapper. We just changed the selector for our live binding to this:

$('#wrapper a, #wrapper input, #wrapper select, #wrapper textarea')

Now, the live event simply doesn’t fire on our overlays.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

*