DOM Trees and Focus Order

When creating complex user interfaces there comes the time when we have to think hard about focus order and where the focus should be put at after a certain action. Think of a modal dialog that is shown to the user. Where should the focus go to after the dialog closes?

This question is sometimes trivial to answer. In our dialog case, if there is only one button that opens the dialog, the focus should return to this button on dialog close.

But in other cases you might have no control over the final markup, and your code must react to whatever it finds on the page. A navigation in the form of a mega menu is opened by several actions: mouse hover, tab focus, external buttons. And if you happen to code it as component to be used by others, you might not even know, where exactly on a page your code might end up.

If such a mega menu is closed, while some content in there has focus, we need to decide where to move the focus now. If we let the browser do it, it will place the focus back on the main <body>. Keyboard users will get frustrated, though, when they are forced again to tab around the page from the very beginning, just because they wanted to get some minor info.

So, let’s make a decision. If the mega menu closes, we want to put the focus on the last focusable element prior to the mega menu, in DOM tree order. This might not be optimal in all situations, but at least it’s way better than doing nothing at all. (You could also choose the first focusable element after the mega menu. The following solution can be adapted to that problem, too.)

Step 1 is therefore getting a list of all focusable elements on the page. This seems like a daunting task, but thanks to Chris Fernandi and Heydon Pickering we are equipped with a one-line solution for this (minus some formatting):

var allFocusableElements = document.querySelectorAll(`
button,
[href],
input,
select,
textarea,
[tabindex]:not([tabindex="-1"])
`
);

Now, how do we get the one element from this very list, that is the closest to our mega menu, but before it? Sounds like a horror of nested for loops.

Thankfully there is a little-known DOM method that allows us to find this element extremely easy. It’s called compareDocumentPosition().

Unfortunately, for people not familiar with bit masks the method is on first glance a bit unwieldy to handle. But after some tries and reading up on StackOverflow you’ll get the hang of it in no time.

The code looks like this then:

/* assume megamenuRootElement holds a reference to
* our menu's upper-most wrapper element, e.g. a <nav> */


var targetElement = null;

allFocusableElements.forEach(element => {
const pos = element.compareDocumentPosition(megamenuRootElement);
if (pos & Node.DOCUMENT_POSITION_FOLLOWING) {
/* note the single "&" above! This is not a typo! */
targetElement = element;
}
});

/* now targetElement contains a reference to our searched node. Let's
* focus it: */

targetElement.focus();

And we’re done. Some formatting notwithstanding in ~7 lines of code.

Of course it’s well possible that you need to tweak and fine-tune this solution a bit, before you can finally use it. For example, we also find elements that are not truly focussable, because they are inside a hidden parent or disabled <fieldset>. We can filter them with a test for

element.closest('[hidden], fieldset[disabled]') !== null

Or you might be uneasy with looping through all those nodes, when you’ve got a long document. In that case, you can rewrite the .forEach() to a classic for loop that you can break out of as soon as you found your element.

There might be more reasons why we want to exclude certain candidates. But now the business logic of your code can take much more room compared to the boilerplate of finding some elements based on their DOM position.