UPDATE August 2nd 2014: The Australia Post mobile site has been deemed AA compliant with respect to the WCAG accessibility guidelines!

This page is dedicated to documenting some of the lessons learnt while implementing accessibility on iOS devices for the Australia Post mobile site.

Note: the term “everyone” in this article means “sighted users and users of VoiceOver technology on iOS devices”. It is accepted practice - at least in Australia - to focus accessibility testing on iOS devices due to the superior (as of 2014) screen reading technology on those devices, which is known as VoiceOver. Vision Australia endorses this approach because their user-statistics indicate that people that have a need for a mobile screen reader almost exclusively (98%+) use iOS devices, due to the superior experience. On the Android side, there are many screen-reader options with varying degrees of ability and utility.

1. Element Visibility

  • Use aria-hidden to hide things from screen readers
  • Create a visually-hidden (or sr-only in Bootstrap lingo) CSS class to hide things from display but make them ‘visible’ to screen readers
  • Use ng-show / ng-hide / style="display:none to hide things from everyone

2. Setting focus

Moving the focus around is crucial for VoiceOver users to understand what they are “looking” at.

  • Use tabindex="0" on non-focussable elements is best. tabindex="-1" doesn’t always work with VoiceOver - it sometimes hits-then-skips to the next/nearest visible element instead of remaining on the focussed element
  • Set focus to the page heading when the page changes. This is especially necessary for single-page applications.
  • May need to add descriptive text (e.g. fieldset with a legend) and aria-describedby
  • Ideally, try to find a visible-to-everyone element that you can set the focus to. Setting focus on elements that are only visible to VoiceOver seems to be unreliable/fragile. (This was the approach I ended up taking when showing search results (see below)).

3. Showing search results

It took a few attempts to get a search-results presentation scheme that worked for everyone. Key points:

  • set focus to the first result
  • add “result x of y,” message to the beginning of each result item, to provide more context (and avoids the need for a separate “Y results found” label). Note that this string can be hidden from sighted-users.
  • Using <span> elements inside an anchor element, in combination with aria-labelledby (and aria-describedby, for secondary text) allows VoiceOver to read the anchor text smoothly, rather than reading the text-blocks that are inside the anchor one-at-a-time. Using <div> elements caused the “read-one-text-block-at-a-time” behaviour.
  • Adding a role attribute to a visually-hidden button element changed the VoiceOver behaviour so that it no longer focussed on the button (when without the role attribute, it DID focus it. Roles tried: status, alert, log, heading. I tried this approach initially with a “Y results found” label
  • adding aria-controls="searchRegion" to the search textbox created a link to the “searchRegion” element, which would have worked well if it was visible

4. Accessible radio buttons and checkboxes that look nice

  • Need certain structure to overlay the input-element on-top-of the label/button to work with Safari
  • Use the same name for the collection of inputs, but provide unique ids for each element to allow the label-click to work
  • group controls with fieldset

5. Abbreviations

  • We wrote an <abbr> directive to make abbreviations accessible by converting the <abbr title="...">...</abbr> text into a visually-hidden child node of the <abbr> element, then using aria-hidden on the abbreviation itself. This doesn’t work when the abbreviation is inside an block that you are using via aria-labelledby or aria-describedby, which just reads the text (ignoring the aria-hidden attributes in most cases) or reverts to reading in block mode instead of reading the text like a sentence.

6. Tabs

Not too hard to implement, though there are still some unexpected (to me at least) behaviours when opening/closing tab-accordians.

Standard tabs

Basically, you have a parent element with role="tablist" to contain the tab, each tab has role="tab", tab panels have role="tabpanel". You can use aria-labelledby to label the tabs some content in the corresponding tab panel.

Tab accordians

Same as standard tabs: parent element has role="tablist", each tab has role="tab", tab panels have role="tabpanel". Additionally, add an aria-expanded="true/false" to each tab, backed up with some visually hidden text to match (e.g. “expanded”, “collapsed”)

Unexpected behaviour

When the tab state changes (and hence, the text inside the tab element (or the aria-selected value) changes), Voiceover reads the OLD state THEN the new state. Before the tab panel is opened, Voiceover says “collapsed, , tab, of ". When the tab is activated, it says, "collapsed, , tab, of , selected. expanded, , tab, of <total-tabs + 1>" - which means that it treats the **tab-panel** (if it is a child of the element with `role="tablist"`) as a **tab** element.

7. Checkboxes

  • If using ngModel with ng-checked property, you need to be careful to ensure that ng-checked includes the ng-model expression in it’s own expression. I had a case where the ng-model expression was not part of the ng-checked expression, and when the checkbox changed from checked to unchecked (with the ngModel value still true), visually the checkbox looked checked, but clicking on it did not change the ngModel value (it was still true). It required a second click to change the checked state. E.g. ng-model="a" ng-checked="a || b || c" // Include "a" in the expression

OK, so I finally understand why it’s bad to use sudo npm install.

Here’s how I got a sudo-free install of Node+NPM on OSX:

  1. Download OSX Node package
  2. Install the package using the defaults
  3. Open terminal, and type npm config set prefix /path/to/your/npm/global/folder. The path that you use should be a path that you have full permissions to (e.g. you created/own the folder)
  4. Edit your ~/.bash_profile to include /path/to/your/npm/global/folder/bin in your $PATH (note the /bin)

In my case, I used npm config set prefix /mydev/tools/npm. This sets the location where global NPM packages are dropped.

You need to put the /path/to/your/npm/global/folder/bin location on your path so that the globally-installed packages (e.g. npm install -g grunt) can be executed from the command line using command instead of /path/to/your/npm/global/folder/bin/command.

On Monday 23rd June, I was privileged to attend the first AngularJS Melbourne Meetup as a guest speaker. Thanks to Alex for organising the event. It was a great night!

You can view the slides below (no animation), or download the animated presentation here: Presentation (MS PowerPoint, 1.9MB)

The accompanying demo files:

The Problem

You want to keep your code DRY and/or you want to keep data separate form your code. So you pull the data out of your code into a separate data file (in an XML or JSON format). Sweet! Now you have appropriate syntax highlighting for your code and data, and you have separated your concerns. But how do you use the data from your code?

Well you can either:

  • Load the data at runtime
  • OR, recombine the code and data in a compilation step

There are pros and cons to both approaches, but lets say that the main priority is you want to avoid an extra HTTP request at runtime. That means that combining the files during compilation is the best option.

There are number of GruntJS plugins available which purport to offer the required functionality. Let’s take a look at the main features of 4 plugins:

  1. grunt-bake
  2. grunt-include-replace-cwd
  3. grunt-includes
  4. grunt-include-replace

Key requirements

Include any kind of file

Obviously, the plugin must allow you to include one file from another file:

// index.html:
//@@include('indexTemplate.html')

// indexTemplate.html:
<html><body>
Hello world
</body></html>

This should produce the following index.html file:

<html><body>
Hello world
</body></html>

Some plugins are adept and doing file-includes for only certain types of files, usually HTML. This is fine if you only need HTML file-includes. But an ideal solution shouldn’t care what kind of files you want to include. For example, you should be able to do this:

// routeLoader.js:
...
function getRoutes() {
	var routeTable = [];
	//@@include('routeData.jsonp')
	return routeTable;
}
...

// routeData.jsonp:
routeTable = ['really', 'big', 'list', 'of', 'things', ...];

This should produce the following routeLoader.js file:

...
function getRoutes() {
	var routeTable = [];
	routeTable = ['really', 'big', 'list', 'of', 'things', ...];
	return routeTable;
}
...

Templates and attribute passing

This is a really important feature which extends-on from the idea of being able to insert one file into another: the ability to insert one-or-more values into one-or-more places in an included file. For example:

// indexDev.html:
//@@include('indexTemplate.html', {environment: "Dev"})

// indexProd.html:
//@@include('indexTemplate.html', {envirionment: "Prod"})

// indexTemplate.html:
<html><body>
Hello world, you're in //@@environment now!
</body></html>

This produces indexDev.html…

<html><body>
Hello world, you're in Dev now!
</body></html>

…and indexProd.html

<html><body>
Hello world, you're in Prod now!
</body></html>

This capability provides a compile-time template engine! Very powerful.

Template and file-include configurable syntax

The ideal file-include plugin would allow you to set the syntax to use when defining the file-include command and when defining what a template-value looks like. Naïve implementations often use a syntax that conflicts with run-time template engines, or server-side template engines.

For example, if you use AngularJS, it uses {{}} notation for binding to data from within HTML fragments:

<html><body>
Hello world, you're in {{environment}} now!
</body></html>

If the file-include plugin uses the same syntax, you’re in trouble because the plugin will replace all the {{}} templates it finds before Angular gets to run! Most of the newer plugins allow you to configure the syntax for both templates and file-include commands:

includereplace: {
  options: {
    prefix: '//@@', // This works for HTML and JS replacements
    suffix: ''
  }
}

Support for Grunt 0.4+ file globbing conventions

Depending on when the plugin was written, the plugin will support different syntaxes for file specifications. You don’t want to have to specify every single file that has an include in it (unless you have a really-small project… size DOES matter).

For example, the following sub-tasks should be supported by your plugin:

include-plugin: {
  // Specify a list of JS files as inputs, and output them into the output/js directory
  js: {
    files: [
      { expand: true, cwd: '<%= env.environment.outputDir %>/js', src: ['*.js'], dest: '<%= env.environment.outputDir %>/js/' },
    ]
  },
  
  // Find all JS files in the test directory, at any depth, and output them to the output/test directory
  test: {
    files: [
      { expand: true, cwd: '<%= env.environment.outputDir %>/test', src: ['**/*.js'], dest: '<%= env.environment.outputDir %>/test/' }
    ]
  },
  
  // Find all html files in the output directory, except for ones ending in '...Template.html' and output them to the output directory
  // Note that we are over-writing the original files, and that's ok!
  web: {
    files: [
      { expand: true, cwd: '<%= env.environment.outputDir %>', src: ['*.html', '!*Template.html'], dest: '<%= env.environment.outputDir %>/' }
    ]
  }
}

Note that in all cases, none of the matching source files are expected to be concatenated into a single file by the plugin.

Other features

Some plugins support conditional logic in their templating, which can be useful if you are heavily using the templating features. In my case, I don’t need this functionality.

Plugins

grunt-bake

When I was first looking for a file-include plugin, grunt-bake was the first plugin I could get working. After using it a bit more and finding that it didn’t support specifying a directory as a destination, I created a local patch to do it. This patch had to be installed before any other Grunt tasks were run, so it became a glaring sign that something wasn’t right. In the end, the need for the patch file drove me to explore other file-include plugins further.

Features:

  • File include capability
  • Inline attributes for included files
  • Attributes via Gruntfile.js
  • Configurable template replacement pattern
  • Conditional logic (if, for)

Missing Features:

  • Specifying a directory as a destination for processed files
  • Configurable file-include syntax pattern

grunt-include-replace-cwd

I didn’t investigate grunt-include-replace-cwd in much depth due to it’s age, lack of examples and the sense that it was not being maintained.

Features:

  • File include capability
  • Configurable template replacement pattern
  • Configurable file-include syntax pattern
  • Specifying a directory as a destination for processed files
  • Inline attributes for included files
  • Attributes via Gruntfile.js

Missing Features:

  • Conditional logic (if, for)

grunt-includes

grunt-includes supports basic file-include features, but does not support attribute-passing or templates.

Features:

  • File include capability
  • Configurable template replacement pattern
  • Configurable file-include syntax pattern
  • Specifying a directory as a destination for processed files

Missing Features:

  • Inline attributes for included files
  • Attributes via Gruntfile.js
  • Conditional logic (if, for)

grunt-include-replace

include-replace is a great plugin which met my main requirements.

Features:

  • File include capability
  • Inline attributes for included files
  • Attributes via Gruntfile.js
  • Configurable template replacement pattern
  • Specifying a directory as a destination for processed files
  • Configurable file-include syntax pattern

Missing Features:

  • Conditional logic (if, for)

Conclusion

After evaluating these plugins, I decided on include-replace for the following reasons:

  • It supports normal Grunt 0.4+ file source/destination conventions. You don’t want to have to specify a destination file pattern for each source file-pattern, which is what some plugins forced you to do. Alternatively, they interpreted a directory specification as “concatenate all source files into the specified directory”!
  • It supports configurable file and template syntax which means that the plugin won’t interfere with any other template syntax that is in use (such as AngularJS, Handlebars, JSPs). It also allows me to choose a syntax that is ignored by lint-checkers for JS and HTML files.
  • It supports passing attributes to included-files globally (via Gruntfile.js) and inline.

After using it for a month, it performs reliably and does what I need it to.

Using this article as a guide and this one:

Setup

Basically what we will do is commit the source code into a source branch and have the compiled code on the master branch:

# rename current master to something else
git branch -m master master-old
# Create source branch from the old master and check it out
git checkout master-old && git checkout -b source

Publishing Steps

  1. Commit your new content to the source branch!!!
  2. Build your site from the source branch into /web (your output folder). I’m using grunt to do this step.
  3. Checkout the master branch
  4. Remove the existing content files from master
  5. Copy the new site content into the root project directory
  6. Add the new site content to git & push to origin

Here’s a script to do Step 3 onwards, which I call from grunt after doing step 1:

git status

# Checkout master branch
git checkout master

# Rename the /web directory to .web
mv web .web
mv node_modules .node_modules
 
# Remove all non . files
rm -rf *

# Copy everything from .web to current dir
rsync -a .web/ ./
 
# Remove .web 
rm -rf .web

# Add everything
git add .

# Commit using the same commit message as from the 'source' branch
git commit -C source
git push origin master

# Remove everything again
rm -rf *

# Re-add node_modules
mv .node_modules node_modules

# checkout source again
git checkout source

# undo any pending changes to any files
git checkout -- .
git status