Making the web accessible is important. We have ethical and, in some cases, legal obligations to ensuring access to all of users.
Luckily for us, it's easy to make an accessible Ember Component.
To understand the accessibility story around Ember Components, we have to start by talking about Web Components. Ember Components are designed to be interoperable with the final Web Components API.
Web Components | Ember Components |
---|---|
Templates | Templates |
Custom elements | Ember.Component |
Imports | Resolver |
Shadow DOM | ??? |
An Ember.Component might look like this:
App.NameTagComponent = Ember.Component.extend({
tagName: 'name-tag',
actions: {
hello: function(name) {
alert(name);
}
}
});
This is a simple button. It has an action that pops open an alert that displays the string it's passed as the arg name
. Its template might look like this:
<div {{action 'hello' person.name}}>
Hi, my name is {{person.name}}
</div>
It might be instantiated like this:
{{name-tag person=model}}
Here's a demo: JSBin
We don't need to worry about how the Resolver works - that's an implementation detail solved by the framework. But keep the Shadow DOM in mind - I'll mention it under Future Proofing
.
The ability to use an application. When a user has diminished control over an input device (keyboard, mouse), assitive technologies fill the gap. Screen readers are an extremely common example.
If you're using OS X you've got a screen reader built into your operating system. It can be opened by hitting ⌘-F5
.
The W3C maintains a number of accessibility specs. The most important of these, for the Ember developer, is WAI-ARIA - or Web Accessibility Initiative - Accessible Rich Internet Applications.
At a high level, the ARIA API is a way of annotating HTML to define the behaviour of an arbitary element. This diagram from the accessibility gives us a good idea of how ARIA fits into what we're building.
So it turns out - and this might surprise some experienced developers - that Javascript ain't no thing for screen readers - as long as that Javascript is building DOM. Which is exactly what Ember does (this will be truer still with HTMLBars).
ARIA gives us two primitives to mark up our HTML with: Roles and States/Properties. In using both in combination we can produce DOM that is semantically meaningful to a screen reader.
The role attribute, borrowed from the, Role Attribute [ROLE], allows the author to annotate host languages with machine-extractable semantic information about the purpose of an element. It is targeted for accessibility, device adaptation, server-side processing, and complex data description.
Put another way, a role is the purpose of an element in the DOM.
Simplest possible example:
<div role="button">
Click me
</div>
ARIA gives us lots of roles to work with. The most basic roles can "act as standalone user interface widgets or as part of larger, composite widgets":
alert alertdialog button checkbox dialog gridcell link log marquee menuitem menuitemcheckbox
menuitemradio option progressbar radio scrollbar slider spinbutton status tab tabpanel
textbox timer tooltip treeitem
We also have access to complex roles, with child-role relationships:
combobox grid listbox menu menubar radiogroup tablist tree treegrid
An example of a component with parent and child roles is a menu:
<awesome-nav role="menu">
<div role="menuitem">Item one</div>
<div role="menuitem">Item two</div>
</awesome-nav>
What meaningful properties does this object have at this time?
We also need a way to be able to describe various properties and states a component might have.
<awesome-nav role="menu" aria-expanded="true">
<div role="menuitem">Item one</div>
<div role="menuitem">Item two</div>
</awesome-nav>
When a user tabs to the awesome-nav
element, we need to be able to communicate the state of the menu to the screen reader. Otherwise, there would be no way to determine if the next tab action should highlight
It should be obvious that the element's role will dictate which states/properties are relevant. These states/properties can be required, supported, or inherited. For our purposes this distinction isn't important. All we need to know right now is that a given role has a given list of states/properties which can be meaningful.
The menu
role, for example, allows use to specifiy any of the following states/properties:
aria-activedescendant aria-atomic aria-busy aria-controls aria-describedby aria-disabled aria-dropeffect aria-expanded aria-flowto aria-grabbed aria-haspopup aria-hidden aria-invalid aria-label aria-labelledby aria-live aria-owns aria-relevant
Roles communicate the purpose of the component to the assistive software.
States communicate the state of the current state of the component.
Ember.Component - by way of Ember.View - actually gives us built-in support for a property called ariaRole. We can pass this property a string, and it will automatically fill in the role
attribute in the generated DOM. Otherwise, we need to do a little bit of work ourselves.
App.TacoButtonComponent = Ember.Component.extend({
tagName: 'taco-button',
nameBinding: 'taco.name',
attributeBindings: ['label:aria-label', 'tabindex'],
answer: false,
label: function() {
return "Are " + this.get('name') + " tacos tasty?";
}.property('name'),
tabindex: -1,
ariaRole: 'button',
click: function(event) {
alert('Yes');
},
keyDown: function(event) {
if (event.keyCode == 13 || event.keyCode == 32) {
this.click(event);
}
}
});
Parts worth mentioning: tabindex, ariaRole, aria-label, keyDown
tabindex: makes the content accessible via tabbing
ariaRole: outputs a role attribute to the DOM
aria-label: can be semantically important (for our demo this won't matter)
keyDown: captures enter/space keyboard presses which emaulate the behaviour of a click
{{label}}
{{taco-button taco=model}}
Note: I've omitted the metamorphs and ember-view ID attributes
<taco-button aria-label="Are spicy tacos tasty?" tabindex="1" role="button">
Are spicy tacos tasty?
</taco-button>
Pop open VoiceOver and check out the difference for yourself.
Recalling the difference between Web Components and Ember Components:
Web Components | Ember Components |
---|---|
Templates | Templates |
Custom elements | Ember.Component |
Imports | Resolver |
Shadow DOM | ??? |
[T]here is a fundamental problem that makes widgets built out of HTML and JavaScript hard to use: The DOM tree inside a widget isn’t encapsulated from the rest of the page. This lack of encapsulation means your document stylesheet might accidentally apply to parts inside the widget; your JavaScript might accidentally modify parts inside the widget; your IDs might overlap with IDs inside the widget; and so on. Shadow DOM addresses the DOM tree encapsulation problem.
"Yes"
Yes.
If all this sounds great, please send a PR adding accessibility to the Ember UI project. I've added support for a few in my free time. It would be great to see other people at this meetup contribute.
Come see an expanded version of this talk at Toronto Javascript on June 23
Nice article @jdjkelly , Thanks