Skip to content

Instantly share code, notes, and snippets.

@jdjkelly
Last active December 25, 2021 14:30
Show Gist options
  • Save jdjkelly/0bddf2e834b6d6bc2174 to your computer and use it in GitHub Desktop.
Save jdjkelly/0bddf2e834b6d6bc2174 to your computer and use it in GitHub Desktop.
Making Accessible Ember Components

Making Accessible Ember Components

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.

What are components?

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.

What is accessibility?

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.

ARIA Architecture

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.

Accessible elements

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.

Roles

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>

States and Properties

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

or move on to a sibling element.

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

Summary

Roles communicate the purpose of the component to the assistive software.

States communicate the state of the current state of the component.

Accessible Ember Component Examples

Built-in support?

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.

Button Example

taco-button.js

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

components/taco-button.hbs

{{label}}

index.hbs

{{taco-button taco=model}}

Rendered DOM

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>

JS Demo: Accessible Example

JS Demo: Inaccessible Example

Pop open VoiceOver and check out the difference for yourself.

Menu Example

ic-menu

A note about future proofing

Recalling the difference between Web Components and Ember Components:

Web Components Ember Components
Templates Templates
Custom elements Ember.Component
Imports Resolver
Shadow DOM ???

What's the 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.

HTML5 Rocks

Is the Shadow DOM keyboard navigable?

"Yes"

-Marcy Sutton

So when Ember Components implement Shadow DOM will they still be accessible?

Yes.

Call for help

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.

Thanks!

Come see an expanded version of this talk at Toronto Javascript on June 23

@zersiax
Copy link

zersiax commented May 22, 2017

I am an emberJS newcomer, so please keep that in mind :) But, why are you making this so much harder for yourself than it needs to be? The first rule of ARIA is, do not use ARIA. My second rule is, if you think you should break the first rule, consult me first :P
I have heard that by default, a component in Ember returns a div, however this behavior can apparently be overwritten.
Do you want a button that is navigable using the keyboard? Clickable using the keyboard? Has a label of its own without h aving to use aria-label? We have such a thing, its called the html5 native button element. Want a checkbox that just works with keyboard, reports its state without ARIA? Yup, we have native HTML for that as well.
I am an accessibility expert as both a developer and a user, I am fully blind, and this trend of stepping away from structurally sound HTML worries me.
As I said though, I am an emberJS newcomer, sincerely hoping that this dependence on ARIA is not something baked into the framework.
ARIA has, in my opinion, two major roles. One, by far the biggest especially in already existing, big codebases, is to patch the screw-ups that have already happened. The

Awesome, click me
kind of screwups. The screwups where people who don't know about accessibility forget about the rich vocabulary HTML offers and just make everything meaningless divs and spans so they don't have any browser-enforced default styling to worry about and as a result break usability and accessibility for everyone.
The second role of ARIA is to complement (not replace!) HTML, a good example of this is aria-expanded, which has no equivalent in native HTML.
I don't mean to bash, I am eagerly learning emberJS myself because I like the philosophy behind the framework. I really hope there's a less hacky way to incorporate accessibility into emberJs-built applications though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment