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);