Getting Started with Angular and Accessibility

Getting Started with Angular and Accessibility

blindfold

I recently did some preliminary work adding accessibility support to an existing Angular application. At the start of this work I knew very little about website accessibility, and I suspect the evolution of my thinking during the process would be common amongst other developers who have been in the same situation. Specifically:

  1. Initial annoyance at having to do it
  2. Slow progress reworking sections of markup
  3. Growing satisfaction that the app was becoming accessible to a broader audience
  4. The realisation that the codebase itself was actually better off for the process

In this post I’ll talk about the 3 things that I’ve done so far during this journey to an accessible Angular app: accessible icons, keyboard navigation and finally, ARIA support.

Accessible Icons

Irrespective of whether you are using Angular or not, probably the easiest thing you can do to improve the accessibility of any site is ensure that images that contribute to the functionality of the site can be picked up by screen reading software.

Doing this for regular img tags is relatively straightforward, but to be honest we don’t really use that many non-decorative JPGs or PNGs anymore on the sites we build. Most of the heavy lifting is done by icon fonts, which are harder to make accessible because icon characters don’t mean much to screen readers. In fact, often there’s an explicit speak: none; style defined for them because if the screen reader did read them, it’d just be a character code that wouldn’t make much sense.

So what we need to do is put some text on an icon that they can be picked-up by a screen reader, but won’t be visible on the screen. Unfortunately, this means we’re going to have to make some changes to the markup.

Take, for example, a simple clickable icon. Whereas once the markup might have looked like this:

<a href="#doStuff" class="icon-doStuff"></a>

we now have to include both a text and icon portion:

<a href="#doStuff">
  <span class="icon-doStuff"></span>
  <span class="invisible">Do stuff</span>
</a>

Next we have to make the text invisible on the screen but at the same time accessible to screen readers. The best solution I’ve seen so far comes from Jonothan Snook:

.invisible {
    border: 0;
    clip: rect(0 0 0 0);
    height: 1px;
    margin: -1px;
    overflow: hidden;
    padding: 0;
    position: absolute;
    width: 1px;
}

It’s not pretty but hey, this is CSS, so what did you expect? That said, Angular can make things easier for us on the markup front by letting us encapsulate the extended icon markup in a simple directive:

angular.module("myModule").directive("accessibleIcon", function() {
  return {
    restrict: 'E',
    scope: {
      name: '@',
      text: '@'
    },
    template: '<span class="icon-{{name}}"></span>' + 
              '<span class="invisible">{{text}}</span>'
  };
});

Using this directive, our original example then becomes:

<a href="#doStuff">
  <accessible-icon name="doStuff" text="Do stuff">
  </accessible-icon>
</a>

Keyboard Navigability

The next big thing you can do to improve the accessibility of your app is to make it keyboard-navigable. As well as being good for users who can’t use a mouse, an unexpected benefit for me as a developer was that it made it much quicker to test changes to the app without my hands having to leave the keyboard. Another upside of accessibility!

The basics

If you haven’t already, I highly recommend that you upgrade to Angular 1.3 and start using the ngAria module – even before you start adding ARIA support to your app (which I will talk more about later).

This is because ngAria does something that strictly speaking isn’t ARIA-related: it adds tabindex=“0” attributes to elements that have ng-click on them. This in-turn means that elements that mightn’t already be navigable by default – for example, divs – become navigable. If you’re using frameworks like ui-bootstrap, this automatically makes components like tabs navigable as well.

Another thing that ngAria does automatically (after version 1.3.6) is add a keypress handler to elements that have an ng-click so that they can be activated via the keyboard. This saves you having to work in hacks yourself to achieve the same thing.

Tightening up your templates

Enabling the ngAria module can expose some issues with the code that you already have. For example, we had a list of clickable links, where we also wanted to track when those links had been clicked. The markup looked something like this:




<li ng-click="doSomeTracking()">
  <a ng-click="doSomeStuff()">Do stuff</a>
</li>



The anchor tag was styled to fill the whole li and was already keyboard-navigable. So the tracking worked as expected and so did keyboard-navigability – even if the code was a little brittle (if somebody restyled the anchor to not fill the link, then a tracking call could occur without the anchor actually being triggered).

When we enabled ngAria in our app, the brittleness of this code was exposed because the li links became tab-navigable by virtue of having ng-clicks. From the user’s perspective, this was not good, because they now had to cycle through both elements when really all they cared about was the anchor.

At first it may seem that the solution is just to shift the ng-click from the li into the anchor tag:




<li>
  <a ng-click="doSomeTracking()" ng-click="doSomeStuff()">
    Do stuff
  </a>
</li>



This resolves the tabbing issue, but clicking the link now won’t work as expected because Angular will only pay attention to the first ng-click. To work around this, we can eliminate doSomeTracking() and push the tracking code into doSomeStuff():




<li>
  <a ng-click="doSomeStuff()">
    Do stuff
  </a>
</li>



With doSomeStuff() looking something like:

...
$scope.doSomeStuff() = function() {
  // Do some tracking
  ...
  // Now do the actual stuff
};
...

This also makes it easier for us to test our logic, because code in controllers is much easier to test than code in templates.

So you can see how thinking about accessibility forced us to evolve towards a design that was cleaner and more testable.

ARIA

Now for the big one: ARIA. I mentioned ngAria briefly in the previous section, but you may be asking the question: exactly what is ARIA?

The Problem

As web UIs become more sophisticated, we’re increasingly being asked to build widgets that behave a like regular components, but with a highly customized look. For example, it’s been a long time since I worked on a project that used stock browser checkboxes because every client wants them to have a customized look. Even worse, we’re also regularly being asked to build components that don’t even have any native analogue in the browser; for example, tabs or modal dialogs.

To implement all of these widgets, we resort to increasingly sophisticated techniques/hacks, usually involving masses of HTML elements (often divs), CSS and Javascript. Unfortunately, the only part of this that a screen reader will be able to see is a blizzard of mutating divs.

Introducing ARIA

The ARIA (Accessible Rich Internet Application) standard describes a set of attributes that we can add to some of our HTML elements so that a screen-reader can ascribe meaning to them.

For example, consider some simple Angular template code that describes a custom checkbox:

<div class="checkbox">
  <div class="checkbox-image" 
       ng-click="isChecked = !isChecked" 
       ng-class="'icon-' + (isChecked ? 'checked' : 'unchecked')">
  </div>
  <div class="checkbox-text">
    {{checkboxText}}
  </div>
</div>

The classes give some hints as to the meaning of each part, but that doesn’t mean much to a screen reader. However, we can annotate the top-level div with an ARIA role to give a hint as to what it actually represents:




<div class="checkbox" role="checkbox">
  ...
</div>



That’s better but it immediately begs the question: how does a screen reader know if the checkbox is activated or not? Well we can add an attribute for that too, and dynamically set its value:




<div class="checkbox" role="checkbox" aria-checked="{{isChecked}}">
  …
</div>



Now a screen reader will know that it’s a checkbox and when it is checked!

This is just the beginning – there are many other different ARIA roles, and many, many aria- attributes. That said, the ARIA specification can be a little intimidating at first. If you just want to get stuff done, I recommend jumping directly to the definitions of the different role attributes, and then using those to understand the different attributes that are available for each role.

Note that ARIA is not a replacement for writing ‘semantic’ HTML – i.e. leveraging all of the tags that HTML gives you to best describe the semantics of your HTML document. However, when your HTML has gone beyond being just a ‘document’ and stretched into ‘application’ territory, ARIA is what you use to make that app accessible. And like semantic HTML, having to think about ARIA attributes often results in better, cleaner markup, irrespective of the accessibility outcome. We’ll come back to that in a second.

Angular and ARIA

The Angular team at Google have wholeheartedly embraced ARIA. Angular 1.3 includes the ngAria module and the Angular Material Design team have baked ARIA support into their implementation of Google’s Material Design specification with the help of ngAria.

ngAria can’t automatically add ARIA roles to your app, as it has no better chance of understanding your mass of divs than a screen reader does. However, it still provides a good basis for building your own ARIA support.

This is because it detects ARIA roles you have placed on elements, and then automatically handles some of the more standard ARIA-related attributes – for example, indicating whether a component is shown or hidden, whether an input is enabled or disabled, or whether the value in an input is valid or invalid. It is especially useful when those attributes easily map to existing ngModel properties.

Speaking from experience I highly recommend that if you are planning on adding ARIA support to your app, you start using Angular 1.3 and the ngAria module sooner rather than later. There’s a pretty good chance that, without ngAria, your ARIA support will certainly be different-to (and probably not as a good as) that which ngAria would guide you towards.

Consider, for example, the checkbox example above. If the ngAria module is enabled, it adds tabindex="0" to both the top-level tag (because it has an ARIA role on it) and to the checkbox-image element (because it has an ng-click on it). However, this means that when we tab through document, both the overall checkbox element and the checkbox-image element will receive focus in-turn. Even worse, the checkbox element isn’t selectable, even though it can get focus.

This is bad. What’s going on? Well, it turns out that our checkbox markup is flawed. Indeed, if you look at the behaviour of a stock browser checkbox, the checkbox text should be selectable as well as the checkbox itself. To resolve both this and the focus issue, we need to move the ng-click and ng-keypress up from the checkbox-image element to the checkbox element:




<div class="checkbox" 
     role="checkbox" 
     aria-checked="{{isChecked}}" 
     ng-click="isChecked = !isChecked">
  <div class="checkbox-image" 
       ng-class="'icon-' + (isChecked ? 'checked' : 'unchecked')">
  </div>
  <div class="checkbox-text">
    {{checkboxText}}
  </div>
</div>



Now the checkbox will only receive focus once, and the whole thing will be checkable. Thinking about ARIA roles and using ngAria just forced us to think more carefully about our implementation, and resulted in a component that behaved more like it is supposed to – for everybody.

Conclusion

The work I have done making our Angular app accessible has only just begun. We have many more ARIA roles and attributes to set, across a range of custom components. Also, we need to look at additional things like making sure validation error messages are handled correctly, and ensuring that the app always sets focus correctly as it transitions between states.

However, I am encouraged by the commitment that the Angular team have made to accessibility, and impressed by the fact that thinking about accessibility has made our app better overall. My parting advice would be that, if accessibility is a requirement for your app, you consider it from the beginning rather than trying to tack it on at the end. A few simple guidelines and a little ARIA knowledge can save you significant rework later on.

ben.teese@shinesolutions.com

I'm a Senior Consultant at Shine Solutions.

5 Comments
  • Marcy Sutton (@marcysutton)
    Posted at 08:28h, 05 December Reply

    Just a heads up that the keypress issue was fixed in a recent ngAria pull request: https://github.com/angular/angular.js/pull/10288

    • Ben Teese
      Posted at 11:04h, 05 December Reply

      Thanks Marcy, look forward to picking this up in the next release!

      • Ben Teese
        Posted at 12:14h, 22 December

        I just upgraded to Angular 1.3.6 and it indeed now automatically adds keyboard handlers to elements with an ng-click. I’ve updated this blog post to reflect this improved behaviour.

  • Pingback:Meligy’s AngularJS & Web Dev Goodies — Issue7: Goodbye 2014! « GuruStop.NET By @Meligy
    Posted at 16:00h, 31 December Reply

    […] Getting Started with Angular and Accessibility A good introduction to accessibility in AngularjS and the ngAria official AngularJS module. […]

  • Jose
    Posted at 10:28h, 10 October Reply

    I have a directive that does very much the same you explained here but I have what seems to be an asynchronous problem. If you you use the keyboard to check/uncheck the checkbox, the screen reader reads it before the aria-checked value changes.

    See plunker here http://plnkr.co/edit/EPREzoocWqAzFjGfPKL2?p=preview

    If you turn voice over on, then set the focus on the toggle button and change the values by using the CTRL-ALT-SPACE, you see that it reads the value before it gets reset. (I’ve tried without the timeout as well)

    Any ideas?

    Thanks

Leave a Reply

%d bloggers like this: