3dd42cea68
Remove miscellaneous usages and references to usages of whitelist and blacklist throughout the repository.
454 lines
16 KiB
Plaintext
454 lines
16 KiB
Plaintext
@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>
|
||
<div> 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)
|