Saturday, August 11, 2018

It's A Trap!

If you read my previous post, you know I'm a big fan of accessibility, and reading this post after the previous may be a little confusing because although one of the Success Criterion (2.1.2) is "No Keyboard Trap", I'm going to tell you how to build one.

The reason I'm going to tell you how to build a 'keyboard trap' is that there is a exception to the 2.1.2 - when a modal window is open. These virtual windows have been called 'lightbox', 'overlay', or even 'dialog' - no matter what name we call them, they restrict control to their own little container...and the "keyboard trap" is only a keyboard trap because the input mechanism the user has chosen is a keyboard. What we're really going to build is a 'focus trap' - something that traps focus to a specific container.

I will preface what I am about to describe by clarifying that the small amount of code here will be vanilla JavaScript, but you can translate the generalized approach here to Angular, ReactJs, or any framework you choose. The idea; however, is not to provide much code, but rather a framework of requirements that will allow you to create the focus trap and to explain the because's associated with the multiple why's that come with how the focus trap is constructed.

First, let's talk about why a 'focus trap' rather than a 'keyboard trap'. If we were building a keyboard trap, we would bind a handler to the keydown event and check whether the key hit was a navigational key, e.g., a Tab or Arrow key. This method works - but only if the method of navigation is via a navigation key and not through a navigation method that does not include keys - like any one of several methods available within an assistive application. As a side note, event listeners for common events - like a keydown, keyup, or keypress - are very expensive, so a focus or blur listener offers performance benefits as well. Additionally, using a focus or blur listener makes more sense conceptually, because what we're really trying to control is focus, not which keys are hit or which things are clicked.

Of course, the immediate problem is that unlike a keydownkeyup, or keypress event, a focus or blur event does not bubble. This lack of bubbling means the event handler that traps focus will have to be listening to every blur or focus event for every focusable element within the container. Here's another point where your performance can be impacted - assigning the listener by putting the function declaration in the listener attachment will drastically reduce performance. Create a focus handler and then add it to each focusable element within the container. Note - a code sample of how to add this event listener to each focusable element is not provided, because each framework provides a different method for traversing a Node tree.

Within your event handler, you're not going to be concerned about where focus is coming from, but where it is going to, because we can already assume focus is coming from somewhere in our container, where we're trying to trap it. Luckily, we have this information in a blur event - it's called the relatedTarget.

Note: The use of relatedTarget inside a blur event is not standard across all browsers. For example, some browsers set relatedTarget for focusOut events but not blur events. Although there is no sure way to make this behavior absolutely consistent, some browsers will set the activeElement prior to firing the blur. It is recommended that you check the document.activeElement if the relatedTarget is null.


One last decision you'll have to make is where to place focus if the user tries to go outside the container. You might consider leaving focus where it is; however, that would trap focus to the last focusable element when the default behavior of moving focus off the last focusable element is going back to the beginning. You just need to decide where the beginning is for your needs. If your modal is a form, you're likely to have a different 'beginning' than if it's a pure dialog.

So our event handler will look something like the code in Example 1...



Example 1 (JavaScript)

const onBlur = (e) => {   /* destructure with a default to handle a null relatedTarget */   const {     relatedTarget = document.activeElement,     } = e;   if (!container.contains(relatedTarget)) {     beginning.focus();   } };


Because this uses the native blur event, it won't just block blurs based on navigation keys, but will block all attempts to blur. There will be a slight performance impact because the handler is bound to all focusable elements within a container, but that cannot be avoided - just be sure to remove the listeners when the modal is not displayed.

So, there you go - that's all there is to it. As long as this is used exclusively in a modal, there aren't additional accessibility issues associated with the trap - it's use within a modal doesn't violate the Keyboard Accessible guideline via Success Criterion 2.1.2 - in fact, it makes your modal more accessible than if it lacked the focus trap.

Happy coding.

No comments:

Post a Comment