Appendix JS_N_A1
Summary
Requirement: All functionality is operable through a keyboard interface.
Details: Dynamic menus should be fully accessible to the keyboard, using ‘Tab’ and/or Arrow Key events.
Examples
Correct code
Refer to the JS_N_A1 live demo for a working example.
<div id="demo"> <h3>Main Menu (<a href="#demo-skipped">skip</a>)</h3> <ul> <li><h4><a href="...">Home</a></h4></li> <li> <h4> <a href="..."> <abbr title="New South Wales">NSW</abbr> </a> </h4> <ul> <li> <a href="...">Forecasts</a> <ul> <li> <a href="...">Sydney Forecast</a> </li> <li> <a href="...">NSW Forecast Area Map</a> </li> </ul> </li> <li> ... </li> </ul> </li> <li> ... </li> <li><h4><a href="...">Antarctica</a></h4></li> </ul> </div> <p id="demo-skipped"> ... </p>
var container = document.getElementById('demo'); var menu = container.getElementsByTagName('ul').item(0); var branch = null; var zindex = 0; var arrow = new Image(); arrow.src = 'images/arrow-right-hover.png'; function bind(item) { var opentimer, closetimer, link = item.getElementsByTagName('a').item(0), child = item.getElementsByTagName('ul').length == 0 ? null : item.lastChild; item.contains = function(node) { if(node == null) { return false; } if(node == this) { return true; } else { return this.contains(node.parentNode); } }; if(child) { item.className = 'hasmenu'; } item.addEventListener('mouseover', function(e) { window.clearTimeout(closetimer); if(branch == item) { branch = null; } var target = e.target; while(target.nodeName.toLowerCase() != 'li') { target = target.parentNode; } if(target != item) { return; } if(child) { opentimer = window.setTimeout(function() { if(branch) { reset(branch); branch = null; } reset(item.parentNode); show(child); }, 250); } }, false); item.addEventListener('mouseout', function(e) { window.clearTimeout(opentimer); if(!item.contains(e.relatedTarget)) { branch = item; if(child) { closetimer = window.setTimeout(function() { hide(child); }, 600); } } }, false); link.addEventListener('focus', function(e) { window.clearTimeout(closetimer); reset(item.parentNode); if(child) { show(child); } }, false); } function show(child) { child.style.left = '0'; child.style.top = child.parentNode.parentNode == menu ? 'auto' : '0'; child.style.display = 'block'; child.parentNode.style.zIndex = ++ zindex; reposition(child); } function hide(child) { child.style.display = 'none'; } function reset(root) { var menus = root.getElementsByTagName('ul'); for(var i = 0; i < menus.length; i++) { hide(menus[i]); } } function reposition(child) { var extent = { x : child.getBoundingClientRect().left + child.offsetWidth, y : child.getBoundingClientRect().top + child.offsetHeight }, viewport = { x : document.documentElement.clientWidth - 10, y : document.documentElement.clientHeight - 10, }; if(extent.x > viewport.x) { if(child.parentNode.parentNode == menu) { child.style.left = -(child.offsetWidth - child.parentNode.offsetWidth) + 'px'; } else { child.style.left = -(child.offsetWidth + child.parentNode.offsetWidth) + 'px'; } } if(extent.y > viewport.y) { if(child.parentNode.parentNode == menu) { child.style.top = (child.parentNode.parentNode.offsetHeight) + (viewport.y - extent.y) + 'px'; } else { child.style.top = (viewport.y - extent.y) + 'px'; } } } function clean(element) { for(var x = 0; x < element.childNodes.length; x ++) { var child = element.childNodes[x]; if(child.nodeType == 8 || (child.nodeType == 3 && !/\S/.test(child.nodeValue))) { element.removeChild(element.childNodes[x --]); } if(child.nodeType == 1) { clean(child); } } } clean(menu); var items = menu.getElementsByTagName('li'); for (var i = 0; i < items.length; i++) { bind(items[i]); } function map(keycode, type) { switch(type) { case 0 : if(keycode == 37) { keycode = 39; } else if(keycode == 39) { keycode = 37; } break; case 1 : if(keycode % 2) { keycode ++; } else { keycode --; } break; case 2 : if(keycode == 38) { keycode = 37; } break; } return keycode; } menu.contains = function(node) { if(node == null) { return false; } if(node == this) { return true; } else { return this.contains(node.parentNode); } }; document.addEventListener('keydown', function(e) { if(e.ctrlKey || e.cmdKey || e.metaKey || e.shiftKey || e.altKey) { return; } var target = e.target, keycode = e.keyCode; if(menu.contains(target) && target.getAttribute('href') && /^(27|3[789]|40)$/.test(keycode.toString())) { if(keycode == 27) { menu.getElementsByTagName('a').item(0).focus(); e.preventDefault(); return; } var item = target.parentNode.nodeName.toLowerCase() == 'li' ? target.parentNode : target.parentNode.parentNode, parent = item.parentNode, child = item.getElementsByTagName('ul').length == 0 ? null : item.lastChild; if(parent != menu) { if(child) { if(child.getBoundingClientRect().left < parent.getBoundingClientRect().left) { keycode = map(keycode, 0); } } else { if(parent.parentNode.parentNode .getBoundingClientRect().left > parent.getBoundingClientRect().left) { keycode = map(keycode, 0); } } } if(parent == menu) { keycode = map(keycode, 1); } else if(parent.parentNode.parentNode == menu && item == item.parentNode.firstChild) { keycode = map(keycode, 2); } switch (keycode) { case 37: parent = parent.parentNode; if(menu.parentNode == parent) { parent = null; } if(parent) { parent.getElementsByTagName('a') .item(0).focus(); } break; case 38: var previous = item.previousSibling; if(!previous) { previous = item.parentNode.lastChild; } previous.getElementsByTagName('a') .item(0).focus(); break; case 39: if(child) { child.getElementsByTagName('a') .item(0).focus(); } break; case 40: var next = item.nextSibling; if(!next) { next = item.parentNode.firstChild; } next.getElementsByTagName('a') .item(0).focus(); break; } e.preventDefault(); } }, false); document.addEventListener('click', function(e) { if(!menu.contains(e.target)) { reset(menu); } }, false);