Tricks for ARIA on the iPad/iOS

I've been doing some work using ARIA on the iPad (iOS 4.2) and wanted to share some of the things I've found for others so they don't have to grope in the dark like I've been :)

Introduction

ARIA on the iPad is implemented using VoiceOver, which when turned on will 'read' everything for a user. When in Mobile Safari there is also something called the Web Rotor, which is a cool virtual 'dial' that appears on the screen when you do a two finger touch and turn it. This dial allows users to jump through your pages in different ways, whether by character, words, links, landmarks, etc.

Navigating in VoiceOver is straightforward. Swipe left and right to simulate 'tabbing' through the elements of the page. Swipe up and down to jump through whatever the Web Rotor setting is; for example, if Web Rotor is set to Lines, it will jump by lines. Using three fingers to scroll the page up and down. Do a single touch to select an element; once selected double-tap your finger on the element to simulate a single mouse click. The element that has the current focus will have a black square around it and VoiceOver will announce the appropriate text with audio.

Note that for the work I'm doing I only have to have the ARIA I'm creating work with VoiceOver on the iPad and not with other screen readers, so take the material below with a grain of salt if you have to do cross-screen reader work.

Synthetic Links

I wanted to be able to have users jump through the page between all the links; some of the things I was working with were 'virtual' links that JavaScript actually activated, such as HTML5 <figures>, etc. It turns out you can have these show up as fake links to VoiceOver and Web Rotor by adding role="link" to the element:

<figure role="link">

tabindex="-1"

ARIA provides the ability to use the tabindex on any element now to control how tab ordering and focus works. It is possible to use tabindex="-1" in order to keep an element from being focusable by the user. However, I was unable to get this working in VoiceOver; either I'm misunderstanding tabindex="-1" or it simply doesn't work in VoiceOver.

For example, let's say I have the following markup:

<h1>This is an H1</h1>
<h2 tabindex="-1">This is an H2</h2>

If I give the H1 focus, and then swipe to the right, my understanding is VoiceOver should not then jump to the H2 and give it focus, but VoiceOver incorrectly gives the H2 focus. This seems like a bug to me.

The workaround I've found, which I'm not happy about, is to use aria-hidden:

<h1>This is an H1</h1>
<h2 aria-hidden="true">This is an H2</h2>

This will then hide the element from the tabindex ordering in VoiceOver. This is not a happy long-term situation however, so if someone has a better way to do this (or perhaps an explanation of how I'm using tabindex="-1" wrong) that would be great.

aria-labelledby

Supposedly you can attach aria-labelledby to any element, giving a list of IDs that provide a descriptive label:

<h1 aria-labelledby="part1 part2 part3">Foobar</h1>

Where part1, part2, and part3 are the IDs of three other HTML elements. When the H1 is given focus, VoiceOver should read the text values inside elements part1, part2, and part3 (if my understanding of aria-labelledby is correct). Unfortunately VoiceOver only seems to do this for BUTTON and INPUT elements that use aria-labelledby, not other elements.

To fake this, combined with the previous tabindex="-1" hack, I have to do the following, using role="heading":

<div role="heading">

<h1 id="testH1" aria-hidden="true">
This is an H1
</h1>

<h2 id="testH2" aria-hidden="true">
This is an H2
</h2>

<div id="testDIV1" aria-hidden="true">
This is a div
</div>
</div>

If ARIA worked correctly in Mobile Safari/iOS 4.2, then the correct markup to achieve this would be:

<div aria-labelledby="testH1 testH2 testDIV1">
<h1 id="testH1" tabindex="-1">
This is an H1
</h1>

<h2 id="testH2" tabindex="-1">
This is an H2
</h2>

<div id="testDIV1" tabindex="-1">
This is a div
</div>
</div>

Update: I was also able to get the above chunk working using role="group", but not when the parent element was a DIV; it only worked on LIs for me:

<ul>
<li role="group">

<h1 id="testH1" aria-hidden="true">
This is an H1
</h1>

<h2 id="testH2" aria-hidden="true">
This is an H2
</h2>

<div id="testDIV1" aria-hidden="true">
This is a div
</div>
</li>
</ul>

Sigh; we really shouldn't have to do this to our markup or our ARIA. Fix these bugs Apple :)

Update: Voice Pause

Technically, you should be able to use a comma to cause VoiceOver to 'pause' between items:

Hello, World!

Will cause VoiceOver to pause at the comma.

What happens if you are using the aria-labelledby trick/hack above in order to have VoiceOver 'talk' out loud multiple elements at once?

<h1>Foobar</h1>
<h2>Zoobar</h2>

VoiceOver will munge Foobar and Zoobar together, speaking them outloud too quickly.

In a sane world you could solve this by adding a comma with CSS, or by using CSS Aural Stylesheets which aren't supported by basically anyone:

<style media="speech">
h1:after, h2:after {
content: ",";
}
</style>

However, Speech Media Query stylesheets aren't supported on iOS 4.2; even if I apply the content style rule with the comma as a general style sheet, VoiceOver doesn't pick up the pause.

It turns out I have to put the comma in my markup itself to have the pause take effect:

<h1>Foobar,</h1>
<h2>Zoobar</h2>

Now VoiceOver will pause between the two elements.

While you're here, why not check out Inkling, the company I work for? We are creating interactive digital textbooks for the iPad. Oh, and we're hiring too :)

Comments

Unknown said…
A few comments

tabindex=-1 is meant to make an element ignore keyboard focus, ie, you cannot tab to that element. it is not meant to make an AT (like VoiceOver) ignore that element.

That is what aria-hidden is for. role="presentation" will also accomplish that. the difference being that aria-hidden will hide the whole subtree below that element as well.

----

In your aria-labelledby example, you have a regular div element that is being labeled. A div has no inherent role and does not show up in the Accessibility hierarchy. You'd need to put a role on it that an AT would look for as an accessible element. Example, "heading", "link". Using role="group" may also not achieve what you want since groups are usually ignored by ATs, since they don't usually provide any useful information.

----

if you want to NOT show the comma, but have it be present, you can use aria-label to override

ie. <h1 aria-label="blah,">blah</h1>
Anonymous said…
The confusion about tabindex may make more sense if you understand the desktop focus model, which is different from mobile devices.

On desktop systems there is a focus cursor for standard keyboard access (usually via Tab and Shift+Tab), and there is a separate focus for screen readers. HTML's tabindex attribute affects standard keyboard access, but does not disallow focusability via the screen reader cursor. Since the touch interface you mentioned does not have the same concept of a "keyboard focus model" as desktop systems (except for certain form elements that accept user input other than clicks), the tabindex extensions don't apply in the same way.

Hope that helps. There are additional open issues (deferred to ARIA 2.0 because ARIA 1.0 is so close to being finished) that will better address the device and user interface independence discrepancies that you noticed.
Brad Neuberg said…
@chris:

Thanks for the comments. The fact that tabindex is basically ignored by a screenreader is pretty confusing. The fact that role="group" is also not picked up by ATs is also frustrating, since if you are using a DIV to group together several elements and you want to control how they are dealt with you have to 'fake' it with a bad role, such as role="link" or role="heading" even if that is not their role.

On VoiceOver, aria-label and role="presentation" do not work. aria-labelledby also do not work even if the heading is role or link.
Jeff said…
It's not that screen readers ignore tabindex altogether, it's that tabindex=-1 is only meaningful when applied to a "focusable" element.

h1,h2, etc aren't focusable elements in the first place (e.g. tabindex=-1 is the default already). As James noted, screen readers typically have a virtual cursor that's separate from the normal system focus. Most screen readers also allow for quick navigation between heading elements (even though they're not focusable). When you navigate to a heading, you're not actually moving the focus, you're just telling the screen reader that you want to start reading from this point.