Keyboard accessibility in web applications: part 3
Table of contents
Introduction
In the first part of the article we added keyboard shortcuts to a painting application. In the second part we implemented a way to move the pointer inside the drawing canvas using keyboard shortcuts. Now we will look into the cross-browser compatibility layer implementation for handling all the differences between browsers.
To make it easier to follow along with the code walkthrough presented below, download the full code example and follow along with it as you read the article.
Overview
There are important differences between browsers with regards to keyboard events. Differences apply to the properties and the values they provide in the DOM Event objects. There are also differences that affect the firing of the events, and default action prevention. Last but not least, the same browser can behave differently on a different operating system.
Keyboard events have several important properties:
keyCode
gives the code of the key for a specific event. For example, the key code for Enter is 13, or 17 for Control. Some keys, mostly the alpha-numeric ones, have the same key code as the character Unicode decimal value. For example, the key P has the key code 80, which is the same as the Unicode decimal for the "P" capital letter.charCode
is only provided by Firefox (Gecko), Safari (Webkit) and Google Chrome (Webkit) when the key generates a character. It holds the Unicode decimal for that character.which
is arbitrarily available in most browsers, except Internet Explorer.keyIdentifier
is the new property defined by the DOM 3 Events specification. This property is only available for thekeydown
andkeyup
events, in Webkit and Konqueror 4. The values it holds are either key names like Enter, PageDown etc., or the the Unicode character generated by the key event, using the literal notation "U+XXXX". If you press the P key, then thekeyIdentifier
value is "U+0050".
So this is the big picture - you are already getting an idea of how difficult is to identify a key from a keyboard event. In short, the browser differences you need to be aware of are as follows:
- The
keyCode
property holds the code of the key only during thekeydown
andkeyup
events in most browsers. In thekeypress
event, this property actually holds the character Unicode decimal value. For example, if you press the key P without holding the Shift key down, thenkeyCode
is 80 in thekeydown
and thekeyup
events. In thekeypress
event the property value is 112 - the Unicode decimal for the "p" character. - Since not all browsers give the
charCode
property, in thekeypress
event you cannot easily determine the difference between a key code and a character code. Therefore, you need to remember the key code from thekeydown
event. - Webkit does not always provide the correct value for the
keyIdentifier
property. For some keys, the value is "U+0000" instead of "Unidentified" as the specification says it should be when the key is not recognized. - Konqueror 4 does not use the "U+XXXX" notation in the
keyIdentifier
property. Instead, it gives you the character directly, as-is.
As I said before, there are differences between browsers with regards to event firing as well:
- Opera (Presto) and Firefox (Gecko) fire the
keypress
event for all keys, except for modifiers. Konqueror (KHTML) also fires this event for modifiers. - On Windows, Internet Explorer fires the
keypress
event only for characters (alpha-numeric and symbols). Additionally, the event is fired for three keys: Escape, Space and Enter. Safari and Google Chrome (both Webkit) do the same, with the exception that they do not fire the event for Escape.
Another important difference between browsers is how they repeat the
keyboard events, while the user holds down the same key for longer periods
of time. For example, Firefox on Linux only repeats the
keypress
event. However, Firefox on Windows repeats both
events, the keydown
and the keypress
events.
This is only an overview of browser differences, and it's not highly detailed. For more details, you might want to check out the code comments in the lib.js file from the app4 folder in the code download.
The implementation
The minimal JavaScript library code tries to determine the accurate
keyCode
and charCode
. The keypress
event
is dispatched synthetically for all keys, except modifiers. The
keydown
event is never repeated; only the keypress
event is repeated.
Here is the basic structure of the KeyboardEventListener
class:
lib.dom.KeyboardEventListener = function (elem_, handlers_) {
var keyCode_ = null;
var key_ = null;
var charCode_ = null;
var char_ = null;
var repeat_ = false;
this.attach = function () { ... };
this.detach = function () { ... };
function dispatch (type, ev) { ... };
function keydown (ev) { ... };
function keypress (ev) { ... };
function keyup (ev) { ... };
function isModifierKey (key) { ... };
function firesKeyPress (ev) { ... };
function findKeyCode (ev) { ... };
function findCharCode (ev) { ... };
this.attach();
};
When you create a new instance of the KeyboardEventListener
class you need to provide the DOM Element you want to listen for keyboard events
on, the elem_
argument. You also have to provide the event
handler(s) you want for each keyboard event: keydown
,
keypress
and keyup
. The handlers_
argument must be an object holding the event names as properties, with the
values are the functions, the handlers you want for each event. Example
code:
var klogger
= function (ev
) {
console.log(ev
.type +
' keyCode_ ' + ev
.keyCode_ +
' key_ ' + ev
.key_ +
' charCode_ ' + ev
.charCode_ +
' char_ ' + ev
.char_ +
' repeat_ ' + ev
.repeat_);
};
var kbListener
= new lib.dom.KeyboardEventListener(window,
{keydown: klogger
,
keypress: klogger
,
keyup: klogger
});
// later when you are done...
kbListener
.detach();
The event listeners are automatically attached to the element you give
when the KeyboardEventListener
class is instanced. Here is the
code used for attaching and detaching the event listeners:
/**
* Attach the keyboard event listeners to the current DOM element.
*/
this.attach = function () {
keyCode_ = null;
key_ = null;
charCode_ = null;
char_ = null;
repeat_ = false;
elem_.addEventListener('keydown', keydown, false);
elem_.addEventListener('keypress', keypress, false);
elem_.addEventListener('keyup', keyup, false);
};
/**
* Detach the keyboard event listeners from the current DOM element.
*/
this.detach = function () {
elem_.removeEventListener('keydown', keydown, false);
elem_.removeEventListener('keypress', keypress, false);
elem_.removeEventListener('keyup', keyup, false);
keyCode_ = null;
key_ = null;
charCode_ = null;
char_ = null;
repeat_ = false;
};
As you can see, the event handlers are not used here. The event handlers
are the private functions, keydown()
, keypress()
and keyup()
. Each of these functions will deal individually
with browser differences, and they will call the event handlers you
provided.
The event handlers
Here we have the code for the keydown()
event handler:
function keydown (ev) {
var prevKey = key_;
charCode_ = null;
char_ = null;
findKeyCode(ev);
ev.keyCode_ = keyCode_;
ev.key_ = key_;
ev.repeat_ = key_ && prevKey == key_ ? true : false;
repeat_ = ev.repeat_;
// Do not repeat keydown events.
if (!repeat_) {
dispatch('keydown', ev);
}
// MSIE and Webkit only fire the keypress event for characters
// (alpha-numeric and symbols).
if (!isModifierKey(key_) && !firesKeyPress(ev)) {
ev.type_ = 'keydown';
keypress(ev);
}
};
This function determines the keyCode_
and the key_
variables for the current event, using the findKeyCode()
function. The two variables are added as new properties to the DOM Event
object, such that the event handler you provided will be able to use the
values. If the keydown
event is repeated, then ev.repeat_
will be set to true. The event is dispatched to the keydown
handler you provided using the dispatch()
function only if the
event is not repeating.
Another issue being tackled by the keydown()
function is the
firing of the keypress
event. As described above, Webkit and IE
do not always fire the event. Therefore, if the key is not a modifier, and
if the firesKeyPress()
function returns false, then we manually
invoke the keypress()
event handler.
The charCode_
and char_
variables are reset to
null, because the character is never determined during the
keydown
event.
Before we see the code for the functions used by the keydown
event handler, let's take a look at the code of the other two event
handlers. Here is the keypress()
function:
function keypress (ev) {
if (!keyCode_) {
findKeyCode(ev);
repeat_ = false;
}
ev.keyCode_ = keyCode_;
ev.key_ = key_;
findCharCode(ev);
ev.charCode_ = charCode_;
ev.char_ = char_;
// Any subsequent keypress event is considered a repeated keypress (the user
// is holding down the key).
ev.repeat_ = repeat_;
if (!repeat_) {
repeat_ = true;
}
if (!isModifierKey(key_)) {
dispatch('keypress', ev);
}
};
This code expects the keyCode_
and key_
variables
to be set from the keydown
event; we can only determine the
charCode_
and the char_
variables using the
findCharCode()
function. As described above, in general cases
the keydown
event gives the key code, and the
keypress
event gives the character code. Still, if for some
reason the key was not determined during the keydown
event,
the code tries again.
The keypress
event is only dispatched to the event handler
you provided if it is not a modifier key. Here is the keyup
event handler:
function keyup (ev) {
findKeyCode(ev);
ev.keyCode_ = keyCode_;
ev.key_ = key_;
// Provide the character info from the keypress event in keyup as well.
ev.charCode_ = charCode_;
ev.char_ = char_;
dispatch('keyup', ev);
keyCode_ = null;
key_ = null;
charCode_ = null;
char_ = null;
repeat_ = false;
};
Here we are trying to determine the keyCode_
for the
keyup
event again, even if we might already have it from the
previous keydown
event. This is needed because the user might
press some key which only generates the keydown
and the
keypress
events, after which a sudden keyup
event
is fired for a completely different key. For example, in Opera press
F2 then Escape. It will first generate two events
- keydown
and keypress
- for the F2
key. When you press Escape to close the dialog box, the script
receives the keyup
event for the Escape key.
After the keyup
event is dispatched to the event handler you
provided, any information stored during the event flow is lost, to prevent
any mix-up.
Helper functions
Here is the simple isModifierKey()
function:
function isModifierKey (key) {
switch (key) {
case 'Shift':
case 'Control':
case 'Alt':
case 'Meta':
case 'Win':
return true;
default:
return false;
}
};
The function is as straightforward as it can be.
Next, let's look at the function that determines if the browser will fire
a keypress
event or not:
function firesKeyPress (ev) {
if (!lib.browser.msie && !lib.browser.webkit) {
return true;
}
// Check if the key is a character key, or not.
// If it's not a character, then keypress will not fire.
// Known exceptions: keypress fires for Space, Enter and Escape in MSIE.
if (key_ && key_ != 'Space' && key_ != 'Enter' && key_ != 'Escape' &&
key_.length != 1) {
return false;
}
// Webkit doesn't fire keypress for Escape as well ...
if (lib.browser.webkit && key_ == 'Escape') {
return false;
}
// MSIE does not fire keypress if you hold Control / Alt down, while Shift is off.
if (lib.browser.msie && !ev.shiftKey && (ev.ctrlKey || ev.altKey)) {
return false;
}
return true;
};
There is no way to detect if the keypress
event will really
fire or not. We simply rely on the knowledge we have based on testing and
online documentation.
The keyCode_
and key_
variables above are
determined by the findKeyCode()
function:
function findKeyCode (ev) {
/*
* If the event has no keyCode/which/keyIdentifier values, then simply do
* not overwrite any existing keyCode_/key_.
*/
if (ev.type == 'keyup' && !ev.keyCode && !ev.which && (!ev.keyIdentifier ||
ev.keyIdentifier == 'Unidentified' || ev.keyIdentifier == 'U+0000')) {
return;
}
keyCode_ = null;
key_ = null;
// Try to use keyCode/which.
if (ev.keyCode || ev.which) {
keyCode_ = ev.keyCode || ev.which;
// Fix Webkit quirks
if (lib.browser.webkit) {
// Old Webkit gives keyCode 25 when Shift+Tab is used.
if (keyCode_ == 25 && this.shiftKey) {
keyCode_ = lib.dom.keyNames.Tab;
} else if (keyCode_ >= 63232 && keyCode_ in lib.dom.keyCodes_Safari2) {
// Old Webkit gives wrong values for several keys.
keyCode_ = lib.dom.keyCodes_Safari2[keyCode_];
}
}
// Fix keyCode quirks in all browsers.
if (keyCode_ in lib.dom.keyCodes_fixes) {
keyCode_ = lib.dom.keyCodes_fixes[keyCode_];
}
key_ = lib.dom.keyCodes[keyCode_] || String.fromCharCode(keyCode_);
return;
}
// Try to use ev.keyIdentifier. This is only available in Webkit and
// Konqueror 4, each having some quirks. Sometimes the property is needed,
// because keyCode/which are not always available.
var key = null,
keyCode = null,
id = ev.keyIdentifier;
if (!id || id == 'Unidentified' || id == 'U+0000') {
return;
}
if (id.substr(0, 2) == 'U+') {
// Webkit gives character codes using the 'U+XXXX' notation, as per spec.
keyCode = parseInt(id.substr(2), 16);
} else if (id.length == 1) {
// Konqueror 4 implements keyIdentifier, and they provide the Unicode
// character directly, instead of using the 'U+XXXX' notation.
keyCode = id.charCodeAt(0);
key = id;
} else {
/*
* Common keyIdentifiers like 'PageDown' are used as they are.
* We determine the common keyCode used by Web browsers, from the
* lib.dom.keyNames object.
*/
keyCode_ = lib.dom.keyNames[id] || null;
key_ = id;
return;
}
// Some keyIdentifiers like 'U+007F' (127: Delete) need to become key names.
if (keyCode in lib.dom.keyCodes && (keyCode <= 32 || keyCode == 127 || keyCode
== 144)) {
key_ = lib.dom.keyCodes[keyCode];
} else {
if (!key) {
key = String.fromCharCode(keyCode);
}
// Konqueror gives lower-case chars
key_ = key.toUpperCase();
if (key != key_) {
keyCode = key_.charCodeAt(0);
}
}
// Correct the keyCode, make sure it's a common keyCode, not the Unicode
// decimal representation of the character.
if (key_ == 'Delete' || key_.length == 1 && key_ in lib.dom.keyNames) {
keyCode = lib.dom.keyNames[key_];
}
keyCode_ = keyCode;
};
The findKeyCode()
function tries to determine the key and
its code using ev.keyCode
or ev.which
. If these two
properties are not available, then the ev.keyIdentifier
property
is checked.
When the keyCode
or the which
property are
available, the value is adjusted/corrected for each browser. Mainly, Webkit
is known to give the key code 25 when the key combination Shift
+ Tab is used. Additionally, older Webkit versions gave incorrect
values for several keys. There are several incorrect key codes in several
browsers, so we use the lib.dom.keyCodes_fixes
map.
We have the lib.dom.keyCodes
object holding the list of key
codes associated to key names/identifiers. This is needed so we can provide
an easy to remember, human-readable value for the key_
property,
which is added to the DOM Event object. If the key code is not found in the
list, the code is used as a Unicode decimal value, converted to a string
with the String.fromCharCode()
function.
If the keyIdentifier
event property is used, we will try
to parse the "U+XXXX" notation. We also check to see if it is a single character,
like in Konqueror 4. If the value is not in one of these forms, it is
considered to hold the key name, like Enter or
PageDown. We have the lib.dom.keyNames
object holding
a list of common key names associated to their key code. The key_
value will be the same as the one in the keyIdentifier
property.
The DOM 3 Events specification defines that for some keys, like
Delete; the value for the keyIdentifier
property is
that of the Unicode character. So, from the Unicode specification, or from
the ASCII table, "U+007F" is Delete, "U+0009" is Tab,
etc. In these cases, the findKeyCode()
tries to also determine
the key name.
The function that determines the charCode_
and the
char_
properties is as follows:
function findCharCode (ev) {
charCode_ = null;
char_ = null;
// Webkit and Gecko implement ev.charCode.
if (ev.charCode) {
charCode_ = ev.charCode;
char_ = String.fromCharCode(ev.charCode);
return;
}
if (ev.keyCode || ev.which) {
var keyCode = ev.keyCode || ev.which;
var force = false;
// We accept some keyCodes.
switch (keyCode) {
case lib.dom.keyNames.Tab:
case lib.dom.keyNames.Enter:
case lib.dom.keyNames.Space:
force = true;
}
// Do not consider the keyCode a character code, if during the keydown
// event it was determined the key does not generate a character, unless
// it's Tab, Enter or Space.
if (!force && key_ && key_.length != 1) {
return;
}
// If the keypress event at hand is synthetically dispatched by keydown,
// then special treatment is needed. This happens only in Webkit and MSIE.
if (ev.type_ == 'keydown') {
var key = lib.dom.keyCodes[keyCode];
// Check if the keyCode points to a single character.
// If it does, use it.
if (key && key.length == 1) {
charCode_ = key.charCodeAt(0); // keyCodes != charCodes
char_ = key;
}
} else if (keyCode >= 32 || force) {
// For normal keypress events, we are done.
charCode_ = keyCode;
char_ = String.fromCharCode(keyCode);
}
if (charCode_) {
return;
}
}
/*
* Webkit and Konqueror do not provide a keyIdentifier in the keypress
* event, as per spec. However, in the unlikely case when the keyCode is
* missing, and the keyIdentifier is available, we use it.
*
* This property might be used when a synthetic keypress event is generated
* by the keydown event, and keyCode/charCode/which are all not available.
*/
var c = null,
charCode = null,
id = ev.keyIdentifier;
if (id && id != 'Unidentified' && id != 'U+0000' &&
(id.substr(0, 2) == 'U+' || id.length == 1)) {
// Characters in Konqueror...
if (id.length == 1) {
charCode = id.charCodeAt(0);
c = id;
} else {
// Webkit uses the 'U+XXXX' notation as per spec.
charCode = parseInt(id.substr(2), 16);
}
if (charCode == lib.dom.keyNames.Tab ||
charCode == lib.dom.keyNames.Enter ||
charCode >= 32 && charCode != 127 &&
charCode != lib.dom.keyNames.NumLock) {
charCode_ = charCode;
char_ = c || String.fromCharCode(charCode);
return;
}
}
// Try to use the key determined from the previous keydown event, if it
// holds a character.
if (key_ && key_.length == 1) {
charCode_ = key_.charCodeAt(0);
char_ = key_;
}
};
The findCharCode()
first tries to use the
charCode
event property. If it is available, then we are
done.
When the keyCode
event/which
property need to be
used, we must check if the key determined during the keydown
event generates a character or not. If it is PageDown or some
other non-character key, then we cannot use the keyCode
property
of the current keypress
event.
If the keyCode
event and the which
event properties are
not available, we try the keyIdentifier
property. This property
is not available in the keypress
event, but it can be available
when the keydown
event synthetically dispatches the
keypress
event. The keyIdentifier
property is
checked for a Unicode character value of the form "U+XXXX". For Konqueror
compatibility, we also check if the property value is only one character.
These values are used only if they do not hold something like "U+007F" (the
Delete key).
The event dispatcher function code:
function dispatch (type, ev) {
if (!handlers_[type]) {
return;
}
var handler = handlers_[type];
if (type == ev.type) {
handler.call(elem_, ev);
} else {
// This happens when the keydown event tries to dispatch a keypress event.
var ev_new = {};
lib.extend(ev_new, ev);
ev_new.type = type;
// Make sure preventDefault() still works
ev_new.preventDefault = function () {
ev.preventDefault();
};
handler.call(elem_, ev_new);
}
};
This function checks if you provided an event handler for the given event
type
. If you have one, then the handler is invoked with the
updated DOM Event object. The DOM Event object is faked if the event type
being dispatched does not match the type of the original event - this
happens when the keydown
event handler dispatches
a keypress
event.
That is all! You should take a look into the new lib.js file and check out how everything ties together.
Summary
This three-part article series has provided pretty much everything you need to know to implement cross platform keyboard controls for complicated web applications. It's now up to you to start taking these techniques, experimenting with them, and making use of them in your own applications.
In this article series we tackled one aspect from what it takes to make a mature web application; there are always other things to do, more improvements to make! Some more improvements you may want to think about when you get time are:
- Refining user interaction. For example, you could provide an option to allow the user to click to start drawing and click again to end the drawing operation. Some find it hard to hold down the mouse button down while drawing.
- Application packaging. We have created this application with nice structured source code, with jsdoc comments and multiple files, making it easy to manage. However, in a production environment, you will need to minify your code with tools like the YUI Compressor or Packer.
- Always keep an eye on application performance. If you have a fast system, you might want to consider testing on a slower one. For example, dispatching synthetic DOM events is slower than actually calling the event handler you want, if you already know which function will handle the event you want to fire.
If you want to learn more, stick around at DevOpera and also follow the upcoming major changes to PaintWeb - the open-source web application this article is based on.
This article is licensed under a Creative Commons Attribution, Non Commercial - Share Alike 2.5 license.
Comments
The forum archive of this article is still available on My Opera.