Accessible routing with vue.js [updated]

Routing is an integral part of a Single Page Application, and therefore for the accessible-app.com project. Since it's so central for SPA inclusiveness it is the first of the committed features for version one I decided to tackle. Also, from all of the big JavaScript frameworks I want to cover in this project, I am most familiar with Vue. So I tried to use the official Router, vue-router, in an accessible way.

As I mentioned before, React is ahead of Vue when it comes to plug-and-play solutions for and documenting of accessible solutions in their framework. One of the (aiming to be) ready-made solutions is Reach UI, specifically Reach Router by Ryan Florence. On Reach Router's product page he summarizes why developers must be aware of the accessibility problems that come with not refreshing the page as a whole:

Whenever the content of a page changes in response to a user interaction, the focus should be moved to that content; otherwise, users on assistive devices have to search around the page to find what changed – yuck! Without the help of a router, managing focus on route transitions requires a lot effort and knowledge on your part.

When you use Reach Router in your React app it takes care of the managing focus part by manually setting the focus to the container of the newly loaded contents. This is great, but for my Vue routing approach, I wanted to make it configurable where focus is being sent to. You should be able to send the focus for example to a headline within the loaded content (as Google's Rob Dodson summarizes it concisely, or simplyaccessible.com explains this in detail using Angular).

Accessing the focus target

We can mark the node where we will send the focus onto after route transition with a reference. Meaning: putting the ref attribute on it and then accessing it (learn more about accessing the DOM with $refs here). An example:

<h2 ref="focusTarget">Focus me</h2>
// Get the element in Vue with this.$refs.focusTarget

Now that we got the reference to the focus target we must find out when a route transition happened, and hook into that event. You can use a watcher for this. But you have to make sure that you wait for the DOM to have actually changed. This is what Vue.nextTick is for:

new Vue({
    router,
    watch: {
        $route: function() {
            this.$nextTick(function () {
            // $nextTick = DOM updated

            });
        }
    }
}).$mount('#app');

Wait for it...

One other thing is to add a delay before running the actual focus code. This apparently stems from Voice Over failing to set focus on changed DOM nodes in iOS 7 and earlier. Although this appears to be fixed in Version 8 - since I can find new information on the topic, I'll add a delay.

Now for the central focus part. At first, we're looking for the focusTarget ref. If your route watcher can't find it, our focus target will be the container where content will be loaded into after route transition. Vue Router calls it <router-view>. To make this fallback easier to grab, we will add a reference to the router view like this:

<!-- Here be <router-links />'s -->
<router-view ref="routerView"></router-view>

But back to JavaScript:

// Get component's "routeFocusTarget" ref.
// If not existent, use router view container itself
let focusTarget =
    (this.$refs.routerView.$refs.componentFocusTarget !== undefined)
        ? this.$refs.routerView.$refs.componentFocusTarget
        : this.$refs.routerView.$el;

Before we finally can set focus on the focus target we actually have to make sure that we can set focus programmatically to it (because usually, just interactive elements like buttons, links, or form inputs are focusable).

focusTarget.setAttribute('tabindex', '-1');

GDS, the team behind gov.uk has discovered that a "stray" tabindex on a wrapping container in their case, the <main> element, which was a hack around a browser bug anyway, could cause some issues. Therefore, we're removing the tabindex just after eventually setting focus:

// Focus element
focusTarget.focus();

// Remove tabindex from focustarget.
focusTarget.removeAttribute('tabindex');

Putting it all together

I've prepared a CodePen demonstrating this where I put all of the parts mentioned above together. In this example, the "route target" components are very simple - two of them have their componentFocusTarget explicitly set to their first headline, one of them to their general container DOM node, and one of them has no such ref at all. But in any case - focus is being dealt with after a route change. For debug and display purposes I made the focus visible with a red border.

Demo

See the Pen Vue Router with focus management, advanced by Marcus Herrmann (@marcus) on CodePen.

This way we prevent the situation described by Ryan Florence above - that a user of assistive technologies interacts with a route link, focus stays on said link, although parts of the DOM has changed, and they need to actively search for the changes.

In the end, I am very curious what you think of this solution. Even to step back a bit - the focus management pattern for SPA routes presented here is a best practice, but all assumptions surrounding it should be tested (and it would be great if Deque Systems, Marcy Sutton, and the accessibility community could conduct such a test). Until then, please don't hesitate to tell me what you think of this routing and focus management approach - and where it can be approved.

[Update November 19th, 2018]

I filed a feature request on the focus subject in the official vue-router repo. Vue core member Eduardo San Martin Morote replied:

Thanks for bringing up the discussion. I've been looking at reach router for some time regarding this and it's something we should be able to provide

Not having to add strategies like the one mentioned above, and instead find this already built-in into vue-router would indeed be awesome!