angular/angular.js

View on GitHub
docs/content/guide/accessibility.ngdoc

Summary

Maintainability
Test Coverage
@ngdoc overview
@name  Accessibility
@sortOrder 530
@description


# Accessibility with ngAria

The goal of ngAria is to improve AngularJS's default accessibility by enabling common
[ARIA](http://www.w3.org/TR/wai-aria/) attributes that convey state or semantic information for
assistive technologies used by persons with disabilities.

## Including ngAria

Using {@link ngAria ngAria} is as simple as requiring the ngAria module in your application. ngAria hooks into
standard AngularJS directives and quietly injects accessibility support into your application
at runtime.

```js
angular.module('myApp', ['ngAria'])...
```

### Using ngAria
Most of what ngAria does is only visible "under the hood". To see the module in action, once you've
added it as a dependency, you can test a few things:
 * Using your favorite element inspector, look for attributes added by ngAria in your own code.
 * Test using your keyboard to ensure `tabindex` is used correctly.
 * Fire up a screen reader such as VoiceOver or NVDA to check for ARIA support.
[Helpful screen reader tips.](http://webaim.org/articles/screenreader_testing/)

## Supported directives
Currently, ngAria interfaces with the following directives:

 * {@link guide/accessibility#ngmodel ngModel}
 * {@link guide/accessibility#ngdisabled ngDisabled}
 * {@link guide/accessibility#ngrequired ngRequired}
 * {@link guide/accessibility#ngreadonly ngReadonly}
 * {@link guide/accessibility#ngvaluechecked ngChecked}
 * {@link guide/accessibility#ngvaluechecked ngValue}
 * {@link guide/accessibility#ngshow ngShow}
 * {@link guide/accessibility#nghide ngHide}
 * {@link guide/accessibility#ngclick ngClick}
 * {@link guide/accessibility#ngdblclick ngDblClick}
 * {@link guide/accessibility#ngmessages ngMessages}

<h2 id="ngmodel">ngModel</h2>

Much of ngAria's heavy lifting happens in the {@link ng.ngModel ngModel}
directive. For elements using ngModel, special attention is paid by ngAria if that element also
has a role or type of `checkbox`, `radio`, `range` or `textbox`.

For those elements using ngModel, ngAria will dynamically bind and update the following ARIA
attributes (if they have not been explicitly specified by the developer):

 * aria-checked
 * aria-valuemin
 * aria-valuemax
 * aria-valuenow
 * aria-invalid
 * aria-required
 * aria-readonly
 * aria-disabled

### Example

<example module="ngAria_ngModelExample" deps="angular-aria.js" name="accessibility-ng-model">
  <file name="index.html">
    <form>
      <custom-checkbox role="checkbox" ng-model="checked" required
          aria-label="Custom checkbox" show-attrs>
        Custom checkbox
      </custom-checkbox>
    </form>
    <hr />
    <b>Is checked:</b> {{ !!checked }}
  </file>
  <file name="script.js">
    angular.
      module('ngAria_ngModelExample', ['ngAria']).
      directive('customCheckbox', customCheckboxDirective).
      directive('showAttrs', showAttrsDirective);

    function customCheckboxDirective() {
      return {
        restrict: 'E',
        require: 'ngModel',
        transclude: true,
        template:
            '<span class="icon" aria-hidden="true"></span> ' +
            '<ng-transclude></ng-transclude>',
        link: function(scope, elem, attrs, ctrl) {
          // Overwrite necessary `NgModelController` methods
          ctrl.$isEmpty = isEmpty;
          ctrl.$render = render;

          // Bind to events
          elem.on('click', function(event) {
            event.preventDefault();
            scope.$apply(toggleCheckbox);
          });
          elem.on('keypress', function(event) {
            event.preventDefault();
            if (event.keyCode === 32 || event.keyCode === 13) {
              scope.$apply(toggleCheckbox);
            }
          });

          // Helpers
          function isEmpty(value) {
            return !value;
          }

          function render() {
            elem[ctrl.$viewValue ? 'addClass' : 'removeClass']('checked');
          }

          function toggleCheckbox() {
            ctrl.$setViewValue(!ctrl.$viewValue);
            ctrl.$render();
          }
        }
      };
    }

    function showAttrsDirective($timeout) {
      return function(scope, elem, attrs) {
        var pre = document.createElement('pre');
        elem.after(pre);

        scope.$watchCollection(function() {
          return Array.prototype.slice.call(elem[0].attributes).reduce(function(aggr, attr) {
            if (attr.name !== attrs.$attr.showAttrs) aggr[attr.name] = attr.value;
            return aggr;
          }, {});
        }, function(newValues) {
          $timeout(function() {
            pre.textContent = angular.toJson(newValues, 2);
          });
        });
      };
    }
  </file>
  <file name="style.css">
    custom-checkbox {
      cursor: pointer;
      display: inline-block;
    }

    custom-checkbox .icon:before {
      content: '\2610';
      display: inline-block;
      font-size: 2em;
      line-height: 1;
      speak: none;
      vertical-align: middle;
    }

    custom-checkbox.checked .icon:before {
      content: '\2611';
    }
  </file>
  <file name="protractor.js" type="protractor">
    var checkbox = element(by.css('custom-checkbox'));
    var checkedCheckbox = element(by.css('custom-checkbox.checked'));

    it('should have the `checked` class only when checked', function() {
      expect(checkbox.isPresent()).toBe(true);
      expect(checkedCheckbox.isPresent()).toBe(false);

      checkbox.click();
      expect(checkedCheckbox.isPresent()).toBe(true);

      checkbox.click();
      expect(checkedCheckbox.isPresent()).toBe(false);
    });

    it('should have the `aria-checked` attribute set to the appropriate value', function() {
      expect(checkedCheckbox.isPresent()).toBe(false);
      expect(checkbox.getAttribute('aria-checked')).toBe('false');

      checkbox.click();
      expect(checkedCheckbox.isPresent()).toBe(true);
      expect(checkbox.getAttribute('aria-checked')).toBe('true');

      checkbox.click();
      expect(checkedCheckbox.isPresent()).toBe(false);
      expect(checkbox.getAttribute('aria-checked')).toBe('false');
    });
  </file>
</example>

ngAria will also add `tabIndex`, ensuring custom elements with these roles will be reachable from
the keyboard. It is still up to **you** as a developer to **ensure custom controls will be
accessible**. As a rule, any time you create a widget involving user interaction, be sure to test
it with your keyboard and at least one mobile and desktop screen reader.

<h2 id="ngvaluechecked">ngValue and ngChecked</h2>

To ease the transition between native inputs and custom controls, ngAria now supports
{@link ng.ngValue ngValue} and {@link ng.ngChecked ngChecked}.
The original directives were created for native inputs only, so ngAria extends
support to custom elements by managing `aria-checked` for accessibility.

### Example

```html
<custom-checkbox ng-checked="val"></custom-checkbox>
<custom-radio-button ng-value="val"></custom-radio-button>
```

Becomes:

```html
<custom-checkbox ng-checked="val" aria-checked="true"></custom-checkbox>
<custom-radio-button ng-value="val" aria-checked="true"></custom-radio-button>
```

<h2 id="ngdisabled">ngDisabled</h2>

The `disabled` attribute is only valid for certain elements such as `button`, `input` and
`textarea`. To properly disable custom element directives such as `<md-checkbox>` or `<taco-tab>`,
using ngAria with {@link ng.ngDisabled ngDisabled} will also
add `aria-disabled`. This tells assistive technologies when a non-native input is disabled, helping
custom controls to be more accessible.

### Example

```html
<md-checkbox ng-disabled="disabled"></md-checkbox>
```

Becomes:

```html
<md-checkbox disabled aria-disabled="true"></md-checkbox>
```

<div class="alert alert-info">
You can check whether a control is legitimately disabled for a screen reader by visiting
[chrome://accessibility](chrome://accessibility) and inspecting [the accessibility tree](http://www.paciellogroup.com/blog/2015/01/the-browser-accessibility-tree/).
</div>

<h2 id="ngrequired">ngRequired</h2>

The boolean `required` attribute is only valid for native form controls such as `input` and
`textarea`. To properly indicate custom element directives such as `<md-checkbox>` or `<custom-input>`
as required, using ngAria with {@link ng.ngRequired ngRequired} will also add
`aria-required`. This tells accessibility APIs when a custom control is required.

### Example

```html
<md-checkbox ng-required="val"></md-checkbox>
```

Becomes:

```html
<md-checkbox ng-required="val" aria-required="true"></md-checkbox>
```

<h2 id="ngreadonly">ngReadonly</h2>

The boolean `readonly` attribute is only valid for native form controls such as `input` and
`textarea`. To properly indicate custom element directives such as `<md-checkbox>` or `<custom-input>`
as required, using ngAria with {@link ng.ngReadonly ngReadonly} will also add
`aria-readonly`. This tells accessibility APIs when a custom control is read-only.

### Example

```html
<md-checkbox ng-readonly="val"></md-checkbox>
```

Becomes:

```html
<md-checkbox ng-readonly="val" aria-readonly="true"></md-checkbox>
```

<h2 id="ngshow">ngShow</h2>

The {@link ng.ngShow ngShow} directive shows or hides the
given HTML element based on the expression provided to the `ngShow` attribute. The element is
shown or hidden by removing or adding the `.ng-hide` CSS class onto the element.

In its default setup, ngAria for `ngShow` is actually redundant. It toggles `aria-hidden` on the
directive when it is hidden or shown. However, the default CSS of `display: none !important`,
already hides child elements from a screen reader. It becomes more useful when the default
CSS is overridden with properties that don’t affect assistive technologies, such as `opacity`
or `transform`. By toggling `aria-hidden` dynamically with ngAria, we can ensure content visually
hidden with this technique will not be read aloud in a screen reader.

One caveat with this combination of CSS and `aria-hidden`: you must also remove links and other
interactive child elements from the tab order using `tabIndex=“-1”` on each control. This ensures
screen reader users won't accidentally focus on "mystery elements". Managing tab index on every
child control can be complex and affect performance, so it’s best to just stick with the default
`display: none` CSS. See the [fourth rule of ARIA use](http://www.w3.org/TR/aria-in-html/#fourth-rule-of-aria-use).

### Example
```css
.ng-hide {
  display: block;
  opacity: 0;
}
```
```html
<div ng-show="false" class="ng-hide" aria-hidden="true"></div>
```

Becomes:

```html
<div ng-show="true" aria-hidden="false"></div>
```
*Note: Child links, buttons or other interactive controls must also be removed from the tab order.*

<h2 id="nghide">ngHide</h2>

The {@link ng.ngHide ngHide} directive shows or hides the
given HTML element based on the expression provided to the `ngHide` attribute. The element is
shown or hidden by removing or adding the `.ng-hide` CSS class onto the element.

The default CSS for `ngHide`, the inverse method to `ngShow`, makes ngAria redundant. It toggles
`aria-hidden` on the directive when it is hidden or shown, but the content is already hidden with
`display: none`. See explanation for {@link guide/accessibility#ngshow ngShow} when overriding the default CSS.

<h2><span id="ngclick">ngClick</span> and <span id="ngdblclick">ngDblclick</span></h2>
If `ng-click` or `ng-dblclick` is encountered, ngAria will add `tabindex="0"` to any element not in
the list of built in aria nodes:

 * Button
 * Anchor
 * Input
 * Textarea
 * Select
 * Details/Summary

To fix widespread accessibility problems with `ng-click` on `div` elements, ngAria will
dynamically bind a keypress event by default as long as the element isn't in a node from the list of
built in aria nodes.
You can turn this functionality on or off with the `bindKeypress` configuration option.

ngAria will also add the `button` role to communicate to users of assistive technologies. This can
be disabled with the `bindRoleForClick` configuration option.

For `ng-dblclick`, you must still manually add `ng-keypress` and a role to non-interactive elements
such as `div` or `taco-button` to enable keyboard access.

<h3>Example</h3>
```html
<div ng-click="toggleMenu()"></div>
```

Becomes:
```html
<div ng-click="toggleMenu()" tabindex="0"></div>
```

<h2 id="ngmessages">ngMessages</h2>

The ngMessages module makes it easy to display form validation or other messages with priority
sequencing and animation. To expose these visual messages to screen readers,
ngAria injects `aria-live="assertive"`, causing them to be read aloud any time a message is shown,
regardless of the user's focus location.
### Example

```html
<div ng-messages="myForm.myName.$error">
  <div ng-message="required">You did not enter a field</div>
  <div ng-message="maxlength">Your field is too long</div>
</div>
```

Becomes:

```html
<div ng-messages="myForm.myName.$error" aria-live="assertive">
  <div ng-message="required">You did not enter a field</div>
  <div ng-message="maxlength">Your field is too long</div>
</div>
```

## Disabling attributes
The attribute magic of ngAria may not work for every scenario. To disable individual attributes,
you can use the {@link ngAria.$ariaProvider#config config} method. Just keep in mind this will
tell ngAria to ignore the attribute globally.

<example module="ngAria_ngClickExample" deps="angular-aria.js" name="accessibility-ng-click">
 <file name="index.html">
  <div ng-click="someFunction" show-attrs>
    &lt;div&gt; with ng-click and bindRoleForClick, tabindex set to false
  </div>
 <script>
  angular.module('ngAria_ngClickExample', ['ngAria'], function config($ariaProvider) {
    $ariaProvider.config({
      bindRoleForClick: false,
      tabindex: false
    });
  })
  .directive('showAttrs', function() {
    return function(scope, el, attrs) {
      var pre = document.createElement('pre');
      el.after(pre);
      scope.$watch(function() {
        var attrs = {};
        Array.prototype.slice.call(el[0].attributes, 0).forEach(function(item) {
          if (item.name !== 'show-attrs') {
            attrs[item.name] = item.value;
          }
        });
        return attrs;
      }, function(newAttrs, oldAttrs) {
        pre.textContent = JSON.stringify(newAttrs, null, 2);
      }, true);
    }
  });
 </script>
 </file>
</example>

## Common Accessibility Patterns

Accessibility best practices that apply to web apps in general also apply to AngularJS.

 * **Text alternatives**: Add alternate text content to make visual information accessible using
 [these W3C guidelines](http://www.w3.org/TR/html-alt-techniques/). The appropriate technique
 depends on the specific markup but can be accomplished using offscreen spans, `aria-label` or
 label elements, image `alt` attributes, `figure`/`figcaption` elements and more.
 * **HTML Semantics**: If you're creating custom element directives, Web Components or HTML in
 general, use native elements wherever possible to utilize built-in events and properties.
 Alternatively, use ARIA to communicate semantic meaning. See [notes on ARIA use](http://www.w3.org/TR/aria-in-html/#notes-on-aria-use-in-html).
 * **Focus management**: Guide the user around the app as views are appended/removed.
 Focus should *never* be lost, as this causes unexpected behavior and much confusion (referred to
 as "freak-out mode").
 * **Announcing changes**: When filtering or other UI messaging happens away from the user's focus,
 notify with [ARIA Live Regions](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions).
 * **Color contrast and scale**: Make sure content is legible and interactive controls are usable
 at all screen sizes. Consider configurable UI themes for people with color blindness, low vision
 or other visual impairments.
 * **Progressive enhancement**: Some users do not browse with JavaScript enabled or do not have
 the latest browser. An accessible message about site requirements can inform users and improve
 the experience.

## Additional Resources

 * [Using ARIA in HTML](http://www.w3.org/TR/aria-in-html/)
 * [AngularJS Accessibility at ngEurope](https://www.youtube.com/watch?v=dmYDggEgU-s&list=UUEGUP3TJJfMsEM_1y8iviSQ)
 * [Testing with Screen Readers](http://webaim.org/articles/screenreader_testing/)
 * [Chrome Accessibility Developer Tools](https://chrome.google.com/webstore/detail/accessibility-developer-t/fpkknkljclfencbdbgkenhalefipecmb?hl=en)
 * [W3C Accessibility Testing](http://www.w3.org/wiki/Accessibility_testing)
 * [WebAIM](http://webaim.org)
 * [A11y Project](http://a11yproject.com)