CSS Strategies for resizable text

The WCAG guidelines on resizable text state that you should be able to scale text by 200%.

How to test:
In Firefox, got to View -> Zoom -> Zoom Text Only. Then when you are back in your page, zoom up to 200% by pressing Ctrl + 6 times.

Possible problems that happen when only text gets bigger:

  • Text that wraps underneath itself has too little space between the letters. You’ve set your line-heights in px.
  • using height which causes text to get cut when it overflows that height
  • Items lie on top of one another, because you’re using absolute positioning
  • text stops sitting on a particular background colour and becomes hard to see (or just looks untidy)

SCENARIO: I have an element that must be at least x pixels tall, but could grow larger if I put more text in there
SOLUTION: use min-height rather than height, remember that older IE will treat height as min-height so use your .ie6 styles or whatever you are using to set it seperately

SCENARIO: I have an element that must be no more or less than a specific height
SOLUTION: set the height in ems rather than px and the box will grow with the text resizing

SCENARIO: text is sitting on top of other text or items and is illegible
SOLUTION: rework the layout using floats or relative positioning, negative margins, or if you have fixed dimensions for your absolutely positioned item (e.g. a close icon on a lightbox), style your surrounding boxes so that their margins sit on the area where the absolutely positioned item is. At worst you may have to use a bit of JS to refine the position (e.g. you need to align something to the bottom of another element) but that is mixing styling rules with behaviour rules.

SCENARIO: my text no longer sits on a coloured background, I’ve run out of background image
SOLUTION: rebuild so that the background surrounds the content section of your box, which has a background color. The background adjusts as the size of the content grows and shrinks because it sits around it rather than under it
LEAST DESIRABLE SOLUTION: wrap your text in an element that gets given a background color

SCENARIO: there’s no longer enough space between the lines of text
SOLUTION: don’t set line-height in pixels, set it in ems, and make less use of them because as you use relative units, you’ll find that they are a pain to maintain when they start getting inherited.

Make JavaScript updates (including AJAX) accessible

Towards the end of Laura Kalbag‘s excellent talk on accessibility at last night’s FrontEndLondon meetup, a question was asked about how to deal with when JavaScript updated the page.  There wasn’t really time to go through it, and really its something best shown with a demo.

There are a couple of aria attributes that are well-supported by screenreaders that can help you out with this.

Here’s a pared-down example of an implementation.

....
<ul aria-live="assertive" aria-relevant="all" id="list">
    <li>item one</li>
    <li>item two</li>
</ul>
<button type="button" id="addnew" aria-controls="list">Add new item</button>
<button type="button" id="remove" aria-controls="list">Remove last item</button>
....
//assuming jquery
//

window.onload = function () {
    mymodule();
}
//NB: this is not how you'd build a module in production, its pared down for clarity of what's important.
var mymodule = (function ($) {
  return function () {
    var addBtn = $("#addnew");
    var removeBtn = $("#remove")
    var list = $("#list");

    var addItem = function () {
      var xhr = $.ajax({url:"/"});
      list.attr("aria-busy", "true");//apply the busy state
      xhr.done(function (data) {
        var newLi = $('<li>New item</li>');
        list.append(newLi);
      });
      xhr.fail(function () {
        alert("something went wrong");
      });
      xhr.always(function () {
        list.attr("aria-busy", "false"); // don't just do this on success, do it always. your server may return an error.
      });
    };
    var removeItem = function (e) {
      var c = list.children();
      c.eq(c.length - 1).remove();
    };

    addBtn.on("click", addItem);
    removeBtn.on("click", removeItem);
  }
}(jQuery));

If you run this example, you will notice that the announcement just consists of the new text. For example, you’d hear “Remove last item.” then upon activating it you’d hear “item two”. There’s no further information such as whether the content has been added or removed. If your feature doesn’t make sense without that extra bit of information, then you’d want to do something slightly different, where you update the text of a separate element with aria-live set on it with the fuller message.

Note that ‘add’ makes an ajax call, but ‘remove’ just acts on the DOM. Both get announced.

There are 2 attributes you will definitely need:

  • aria-live
  • aria-busy

There are 3 attributes you will possibly need:

  • aria-atomic
  • aria-relevant
  • aria-controls

These aria attributes sit on block-level elements that contain the content that is going to change.

Aria-live

Indicates that this is a part of the page that will get updated even after the page has loaded.

When the element with this attribute has a content change, the assistive technology automatically picks up on it and deals with it without you having to do anything more.

Its values are: off (ignore changes, default), polite (announce new content when you’ve run out of content to read), assertive (announce new content at the first opportunity).

There used to be aria-live=”rude” but it was so badly abused and disrupted users so much that it got dropped.  Be considerate when using assertive.  Confirming a save action probably justifies assertive.  An updated news feed is better being polite.

Aria-busy

Indicates that an element’s content is in the process of changing.  Its values are true or false.

Unlike aria-live, this one is something that you’d dynamically update from JavaScript.

A good rule of thumb is that if you are showing a spinner over something to indicate activity, you probably also want to set aria-busy to true on the container element for the area is getting updated, and to false when it hides.

Aria-atomic

Should the whole area be announced when just one part of if has changed? Or just the bit that changed?

Its values are true (announce the whole region when something changes) or false (default).

TIP: if your area’s text is not being read out when it updates, sometimes adding aria-atomic=”true” fixes it.

Aria-relevant

What type of changes should be announced?

Values are one or more of additions (meaning elements being added), text, removals (again, of elements) and all, separated by a space.  Default is aria-relevant=”additions text”

Aria-controls

Indicates a relationship between an interactive element (like a link or button) and the element it changes.

Value is ID of the controlled element.

Software

Mac: System Preferences > Universal Access

From there you can check the box to Show Universal Access status in the menu bar for easier access.

You will get best results from working with Safari or Chrome rather than Firefox on Mac.

Windows: Visit Freedom Scientific and download the demo version of JAWS.  After 20 minutes of use, it stops and you have to reboot to get it going again, so plan your test time accordingly.

NVDA is another free, popular screenreader but has fewer functions than the longer-established, paid-for JAWS.

Discover the reading level of your website. (Maybe).

In April, Google added a feature where you can find (and filter by) reading difficulty level (https://support.google.com/websearch/answer/1095407?hl=en)

While their instructions work on the assumption that you’re looking for a particular search term by reading level, it is also possible to combine it with the ‘site’ operator.  Does this get a thumb-in-the-air approximation for the reading difficulty level of a website? Probably, but Google’s not saying. Its certainly worth giving it a go.

Why might this be useful? You probably have in mind who you would like your audience to be – who you write for.  The ‘pass/fail’ result you get will depend on the abilities of your intended audience.  If the least able members of that audience struggle to comprehend what you’ve written, then you’ve failed.  On the other hand, if your target audience is highly educated and specialised then the advanced reading level is a way to filter out the entry-level content. Ability can be affected by a range of things – your audience may be still at school or may have a low reading age, may have problems concentrating (that could be a cognitive disability, or it could be environmental like a noisy office or screaming kids in the garden), may be new to your subject or may have a different first language with only an intermediate level of fluency in your chosen language.

How-to

Instead of a regular search term, type site:mydomain.com. Once on your search results page, look for the ‘Search tools’ button at the top, click it to show the extra tools and then choose ‘Reading level’ from the list opened by clicking ‘All results’

Example results

For my own site, I’d consider the most basic level of my intended audience to be new-ish to the topics I cover, so I’m fine with this split.

Reading difficulty results for this website shows 14% basic, 86% intermediate and 0% advanced reading difficulty level

News / journalism

BBC News.  Here’s the conundrum for them – while ‘dumbing down’ is broadly undesirable, license fee payers cover the whole spectrum of intelligence.  How do you avoid someone being under-served by a service for which they contribute financially on the grounds that they’re not the sharpest knife in the drawer?

BBC news reading difficulty is on average higher than the BBC as a whole

Compare with the BBC as a whole:

Reading difficulty results for bbc.co.uk show 38% basic, 60% intermediate and 1% advanced

No surprises for the Sun

Sun newspaper has very little advanced reading difficulty content

If you are not British you won’t understand why the image below is amusing. Sorry about that.

The Daily mail is almost entirely basic level reading

The Guardian is fairly consistent in its style.

The Guardian is predominantly intermediate reading difficulty, but has little advanced level content

Learning and education

The world wide web consortium.  Subjects that fall under advanced are SMIL, Voice Browser Working Group, Internationalization, QA and a few other bits and pieces. Would they benefit from a rewrite to something with a lower cognitive barrier to entry?

W3.org content has a comparitively large amount of advanced level reading

html5 doctor

Key Stage 1 – kids aged between 5 and 7

KS1 has easy reading levels

GCSE revision help – exams sat typically at 16 years old in England and Wales

GCSE has a mixed range of reading levels

Nature.com content is mostly academic and highly specific post-graduate level material. Today’s homepage includes links to articles such as “Helicity dependent directional surface plasmon polariton excitation using a metasurface with interfacial phase discontinuity” and “Proof mooted for quantum uncertainty”.

Difficulty reading levels for nature.com, with less than 1% basic, 3% intermediate and 97% advanced

Government

Hansard is the publisher of the UK Parliamentary proceedings – i.e. all the debates and written questions get transcribed and put onto the internet

Paliamentary debates in the uk commons are almost entirely intermediate level

Kildare street is the Irish equivalent

kildare street has a similar profile to hansard

Legislation.gov.uk is where all the UK legislation is published.  Unsurprisingly, very little of it is easy reading though there is less ‘advanced’ level content than I’d expected.

uk legislation has 18% advanced level

Gov.uk is the site where the government puts all its guidance and services, ranging from how to renew your car tax to detailed guidance on depleted uranium policy.

gov.uk has a wide range of reading levels

Blogging

The new hotness

Medium.com content is mostly basic and intermediate, with a few advanced articles

The old hotness

blogger reading level is similar to medium

WordPress seems to have attracted more academic bloggers. E.g. the first result under advanced is “Indigenous Peoples and Reducing Emissions from Deforestation & Degradation”

wordpress blogs have more advanced level content at 14%

Nature blogs have a significantly different profile to nature.com

Screen Shot 2013-06-26 at 15.58.04

You’re not going to get much complexity in 140 characters

Twitter mostly basic reading level

However, I’m wondering why this is falls under ‘advanced reading’. Is the correct usage of apostrophe but cavalier disregard for question marks the cause?

Supposedly advanaced level reading tweet reads 'Why does Kia feel the need to snapchat me on the toilet can't she leave me alone for 5 seconds'

 

Hacking the <a> tag – implications for search engines

Bilawal Hameed recently posted up Hacking the <a> tag in 100 characters, where he uses JavaScript to capture a click event on a link and then redirects it to another page entirely.

Knowing that search engine crawlers have limited JavaScript support, I went to see if Google thinks his page links to paypal or not.

If you search in google for link:www.paypal.co.uk site:bilaw.al, you will see that his post shows up.

To check I wasn’t getting a false positive, I tried a slightly different URL.

If you search for link:www.paypal.com site:bilaw.al instead, you get no results.

Is it a problem? Can it be exploited for profit? Not as far as I’m aware, beyond damaging the quality of Google’s search engine data.

Accessible select box (dropdown) change events that work cross-browser, including IE, Chrome and touch devices

There’s a well-know issue with select elements where if you listen for the change event, it will trigger differently between browsers, and on IE it will trigger in a way that makes it inaccessible.

Take the following example: user tabs onto a select element and then uses the arrow key to move through the list. In IE (tested versions 7-9), this triggers the change event whereas other browsers will wait until you’ve moved off the element.

Try this demo on jsfiddle, it will alert the value of the option you’ve selected when ‘change’ event gets fired.

If you are using your select element for navigating to another page, the keyboard user will never be able to get past the first and second item in your list. If you are using the select element for loading in more content via AJAX (e.g. updating other field items based on a select box selection is quite common), then you are going to be hitting your server far more than necessary.

The solution is to bind to blur, click and keydown for IE and change for everything else (should include touch devices), and then a) filter out keypresses that aren’t spacebar or enter and b) check to see if the selected option has actually changed. The below code uses jQuery, but it is all possible via native JS or other libraries. Sadly it relies on checking the browser’s user agent as there isn’t a feature you can test for (if I’ve overlooked something I’d love to hear about it).

Have a look at the accessibile version on jsfiddle


var somemodule = (function () {
  var obj = {
    //some code
    bindDOM: function () {
      var evts = "";
      //accessibility workaround: onchange is good for non-ie; keydown, click and blur is good for ie (but not for chrome)
      //unfortunately we cannot detect this with feature support so must rely on browser support
      if ($jQ.browser.msie) {
        evts = "keydown click blur";
      }
      else {
        evts = "change";
      }
      this.elem.on(evts, $jQ.proxy(this.onFooChange, this));
    },
    onFooChange: function (e) {
      var id;
      if (e.type !== "keydown" || (e.which === 13 || e.which === 32)) {
        id = $jQ(e.target).val();
        if (id !== this.currentFoo) {
          this.currentFoo = id;
          this.getBar(id);
        }
      }
    },
  };
  
  //more code
  return obj;
}());

Webaim screenreader user survey

The annual screenreader user survey is out, at http://webaim.org/projects/screenreadersurvey4/

If you’ve not seen it before, it is definitely worth the five minutes it will take you to scan through. Its results contradict a lot of received wisdom about screenreader behaviour, and highlight which features are actually really helpful (and which aren’t).

Main takeaways:

  • Screenereader users love headings for navigating around a page
  • 1.4% don’t have JS on, so your biggest user base without JS is going to be search bots.
  • 82.7% update their software
  • JAWS is still dominant, but decreasing due to uptake in NVDA (free) and VoiceOver (Mac, iOS)
  • CAPTCHA is still a massive pain. Only Flash is worse for difficulty
  • Quite a lot are using mobiles with screenreaders

jQuery UI sortable incorrectly positioned on drag when page is scrolled

While this issue is a bit odd, there’s not much in the way of information online and I suspect it will become more common as people start adding support for smaller screens and mobiles to their sites.

Here was our problem:
When dragging a sortable element, positioning was fine unless you were scrolled down the page a little.

After some trial and error, we found it was created by a combination of body{offset-x: hidden;} and position:relative on one of the parent elements of the sortable widget.

Now we don’t want to go removing things from other people’s pages to fix our work, because it will almost certainly break something else (these are templates, so the risk is high and regression testing a big task). The solution was to put offset-x: auto on the same element that gets scrollable applied to it.

This is due to get fixed in jQuery UI 2 as they rewrite the sortable plugin.

Android apps with PhoneGap – the missing basic steps

The getting started guide for Android apps on Phonegap misses out a few key pieces of information.

  1. Don’t install your Android SDK in Program Files, or any other folder with a space in it. Eclipse will fail on the build, and uninstalling and reinstalling the SDK somewhere else is just a pain.
  2. If you want to deploy to your device in developer mode and you’re on windows, you’ll need the USB driver. This is buried in the Android SDK installation guide
  3. Another way to deploy to your device is plain old file transfer. Unlike iOS, you don’t need a specific developer or enterprise license to do this. This involves exporting a signed app, which is a bit more involved.

Cleaning up Excel-generated HTML with sed on Mac Terminal

Not so long ago, a client gave us an Excel spreadsheet to work from as a temporary measure while the database got set up.

Excel exports to HTML but fills it with rubbish. Using regular expression-based find and replace in my IDE is normally fine, but with a file this large it froze. Command line is the only solution in such a case.

On the mac Terminal, the sed command is a little bit different from what you might use on *nix (and totally different from what you would do with a PC).

After manually cleaning up the head section, the following 3 commands cleaned up the table cells in my export:

sed -i '' 's/class=xl[0-9]*//g' Table.htm
sed -i '' 's/width=[0-9]*//g' Table.htm
sed -i '' 's/height=[0-9]*//g' Table.htm

sed is a stream editor, the -i switch makes it edit in place, the ” is where Mac differs. It requires a backup file name even if that is blank (therefore overwriting the original file) whereas for *nix it is optional.

The next bit is the regular expression and these particular ones are tailored to remove the class, width and height attributes that Excel adds to all the <td> elements. Finally, the name of the file to edit goes in.

Saving form input when you’re offline using localStorage

Leading on from the work I did with the application cache, I became interested in using local storage to allow me to input data into a form and save it offline, ready for synching later on when I was online.

I’ve made use of JSON.js for handling JSON and jQuery in the code below for some convenience, you should be able to swap out for your library of choice.

Some caveats:

  • potential for data collisions if you edit on another device or browser between saving offline and synching.
    • you could feasibly solve this by dynamically building duplicate form fields that sit next to the server version and contain the local data.  A button to copy across from one field to another is quite a simple trick.
  • you get 5MB of localstorage per domain, meaning it isn’t suitable for documents over 5000 characters (including input names and syntax) or for saving a vast amount of forms without synchronising. There’s a warning provided but it happens at a stage where the storage is already refusing to save, so the user has to copy their content to another file
  • local storage only handles text in key-value strings, so you can’t save your data as a JSON object, you have to stringify it.
  • Try…Catch is not my favourite way of handling detection or errors, but in the 2 cases below it is the only practical option

Inspiration:


/*
OnOffApp makes use of local storage to save form input locally when a user is offline and allow them to synchronise it when they are back online again.
Dependencies:	json.js http://www.json.org/
				jQuery http://www.jquery.com/
Config:			when calling init, there are 5 items that can be optionally configured
				id
					unique identifier for the form to be used in local storage as the key
				field_id
					jQuery object of the field that supplies the id, used to find the parent form, otherwise form defaults to all forms on page
				synchMsg
					HTML to display to inform the user they have items to be synchronised to the server
				synchBtn
					HTML for synchronising button
				warnMsg
					HTML for the message to show the user when local storage is full and data cannot be saved
*/

OnOffApp = (function(){
	
	//variables available throughout the function
	var UID;
	var postForm = $("form");
	var holder = $("<div id=\"synchMsg\"></div>");
	var config = {};
	
	//initialiser function
	//@argument usrconfig: an object of config options
	var init = function(usrconfig){
		setup_config(usrconfig);
		var submit_button = postForm.delegate("input[type=submit]", "click", submit_click);
		if(retrieveLocalData() === true){
			synchMessage();
		}
		$(window).bind("online", synchMessage);
		$(window).bind("offline", synchMessage_remove);
	};
	
	//set-up defaults for user configurable messages and overwrite where specified
	//@arguments usrconfig: object of user-set options
	//@returns the config object
	var setup_config = function(usrconfig){
		var id = location.href;
		config.synchMsg = "<p class=\"synch\">Locally saved data can be synchronised now</p>";
		config.synchBtn = "<input type=\"submit\" value=\"Synch\" class=\"synch\" />";
		config.warnMsg = "<p class=\"warn\">Local storage is full.</p>";
		 for(var i in usrconfig){
			if(i === "id"){
				id = usrconfig[i].val();
				continue;
			}
			else if(i === "field_id"){
				postForm = usrconfig[i].closest("form");
				continue;
			}
			config[i] = usrconfig[i];
		 }
		 UID = "uID-" + id;
		 return config;
	};
	
	//submit button clicked
	//detect if we are online.  If so, grab any saved data as well as existing data and save via AJAX, if not save to local storage
	var submit_click = function(e){
		e.preventDefault();
		var data = getFormInput();
		var savedData = $.parseJSON(getLocalData());
		if(navigator.onLine){
			saveToServer($.extend({},savedData,data));
		}
		else{
			setLocalData(JSON.stringify(data));
		}
	};
	
	//get the form inputs and put into object notation
	//@returns an object
	var getFormInput = function(){
		var formArray = postForm.serializeArray(),
			o = {};
			$.each(formArray, function(){
				o[this.name] = this.value;
			});
		return o;
	};
	
	//get localStorage data
	//@returns string of locally stored data or an empty string
	var getLocalData = function(){
		return localStorage.getItem(UID) || "";
	};
	
	//set localStorage data, if out of space add a warning instead
	var setLocalData = function(data){
		try {
			localStorage.setItem(UID, data);
		}
		catch (e) {
			if (e == QUOTA_EXCEEDED_ERR) {
				postForm.prepend(config.warnMsg);
			}
		}
	};
	
	//get the data from localStorage and add saved values into the relevant fields
	//@returns true or false depending on whether there was locally stored data
	var retrieveLocalData = function(){
		var oldData = getLocalData();
		if(oldData !== ""){
			var fields = $.parseJSON(oldData),
				field;
			$.each(fields, function(key, val){
				field = $("[name=" + key + "]");
				if(field.length > 0){
					if(field.is(":checkbox") || field.is(":radio")){
						field.attr("checked", "checked");
					}
					else{
						field.val(val);
					}
				}
			});
			return true;
		}
		return false;
	};
	
	//if you have localStorage data and you are online, prompt the user to synchronise their data (i.e. save it to the server)
	var synchMessage = function(){
		if(navigator.onLine && getLocalData() !== ""){
			if($("#synchMsg").length === 0){
				holder.append(config.synchMsg).append(config.synchBtn);
				postForm.prepend(holder);
			}
			else{
				holder.show();
			}
		}
	};
	
	//remove the synch message
	var synchMessage_remove = function(){
		holder.hide();
	};
	
	//save form data to the server via AJAX
	var saveToServer = function(data){
		var url = postForm.attr("action");
		var params = $.param(data);
		var me = this;
		$.ajax({
			method: "post",
			url: url,
			data: params,
			success: savedSuccess(data)
		});
	};
	
	//after data saved to server, remove the saved data from localStorage and remove the synchronise message
	var savedSuccess = function(){
		localStorage.removeItem(UID);
		synchMessage_remove();
	};
	
	//interfacing methods and properties
	return {
		init: init
	};
}());


//For the purposes of the demo, there is a hidden input with id "post_id" with the unique identifier for the post (like WordPress)
$(document).ready(function(){
	try {
		if('localStorage' in window && window['localStorage'] !== null){
			OnOffApp.init({
				"id_field" : $("#post_id"),
				"id" : $("#post_id").val(),
				"warnMsg" : "<p class=\"warn\">Sorry, your local storage is full up.  Please copy and save your latest work to a text file and go online to synchronise your work as soon as you can.</p>",
				"synchMsg" : "<p class=\"synch\">You have locally saved data that you can now synchronise with the live server</p>"
			});
		}
	}
	catch(e) {
		return false;
	}
});