PDA

View Full Version : Dropdown menu w. keyboard access



billyboy
10-20-2008, 06:49 PM
Greetings, Its been awhile since I've posted here, but I've come seeking help again.

With my limited knowledge of javascript I've been working on a script to make a dropdown menu accessible using the keyboard. I've gone as far as I can and realized I'm in over my head. I don't know where to go from here to keep the appropriate submenus open when the anchor in the parent level loses focus. Simply replacing the className again onblur obviosly won't work.

Any help is much appreciated, thank you.

function menuHide() {
var a = document.getElementById("nav").getElementsByTagName("a");
for (var x = 0; x < a.length; x++) {
var li = a[x].parentNode;
while (li.nodeName != 'LI') li = li.parentNode;
if (li.childNodes.length > 1) {
for (var y = 0; y < li.childNodes.length; y++) {
if (li.childNodes[y].nodeName == 'UL') li.className += " js-hide";
}
}
li.onmouseover = function() {
if(/\bjs-hide\b/.test(this.className)) this.className = this.className.replace("js-hide", "js-show");
}
li.onmouseout = function() {
if(/\bjs-show\b/.test(this.className)) this.className = this.className.replace("js-show", "js-hide");
}
a[x].onfocus = function() {
var thisLi = this.parentNode;
while (thisLi.nodeName != 'LI') thisLi = thisLi.parentNode;
if(/\bjs-hide\b/.test(thisLi.className)) thisLi.className = thisLi.className.replace("js-hide", "js-show");
}
// a[x].onblur = function() {}
}
}
window.onload = menuHide;

Jesdisciple
10-21-2008, 12:11 AM
Please post the menus' HTML source as well so we can test them together.

billyboy
10-21-2008, 01:29 AM
Test page with the full code is uploaded here (http://webwanderers.awardspace.com/menu-test.php)

Jesdisciple
10-21-2008, 01:37 AM
What browser are you testing in? In Firefox, the menus never hide while I navigate via Ctrl. In fact, I wish a menu would hide when I navigate to a separate menu.

I think you'll need to record which menu item has focus and make sure only its ancestors are expanded.

billyboy
10-21-2008, 03:16 AM
the menus never hide while I navigate via Ctrl.
Exactly. Thats what I need help with. How to keep appropriate submenus open while hiding the others

jscheuer1
10-21-2008, 04:10 AM
Just a note on concept. Using the keyboard to activate javascript will almost always run afoul of user defined and browser default hot keys. Since you have no control or knowledge as regards the former, and the later vary with each browser, and potentially with each browser version, it (using hot keys in javascript) is generally a fruitless approach.

The mechanism(s) for detecting keystrokes in javascript are best applied in getting your code to integrate users' normal expectations for non-javascript code into your script. For example, if you have an onmouseover event on a link, it will not be activated onfocus unless you detect that the user is intending to do that and have that action also activate the event. This can be accomplished (among other ways) in part by detecting actions on the part of the user that constitute navigating links on a page via the keyboard.

But to attempt to take wholesale control over keystrokes for the exclusive use of a script will almost always prove unworkable due to conflicts with some reserved action for those keystrokes in at least enough situations as to render the code very browser specific at best.

billyboy
10-21-2008, 05:26 AM
A user expects anchors to have focus as they use tab in FF or IE, Ctrl + arrow keys in Opera, etc. as those are the defaults in those browsers. No hot keys are being set, nothing is being overridden in the code. Events are fired when focus is set on an anchor, however that happens, whatever key or key combination is used for that, whether its the default setting or a user defined preference.

jscheuer1
10-21-2008, 05:38 AM
I see, my mistake. Please accept my apologies. I'll have a look at the code. However, I notice that the menu doesn't even react intuitively onmouseover/out, at least not all the time. Once you get that working, one would think that making onmouseover = onfocus, and onmouseout = onblur (as it were) could do the trick.

jscheuer1
10-21-2008, 05:52 AM
OK, after looking at and testing the code further, I see that it does respond well onmouseover/out in FF and IE 7, but not in Opera. I also see that the mouse events are tied to the li elements, and that the pseudo :hover class style is used. The pseudo :hover class will not work in IE 6, except on linked anchors and their children. However, to get this working with focus, it would probably be best to skip any dynamic action on the part of style and to set things up so that only events (mouse and otherwise) on the anchor links themselves do anything. Once you get that worked out for the mouse, porting it to onfocus and onblur should be easy.

billyboy
10-21-2008, 06:19 AM
I made an edit to the CSS to fix a problem in IE6 and forgot to check Opera again afterward. Its fixed now.

Applying :hover and the class generated by javascript for IE6 to the li is based on the code for son of suckerfish. If I change that part so mouseover on the anchor is what does anything I'm going to up against the same wall with that as what I am now with onfocus on the anchor.

jscheuer1
10-21-2008, 06:44 AM
At least that would force you to either deal with it or abandon trying. I see that you already have at least some of the anchors display:block. Basically you need to have them all be links and all be as large as the li elements that are their parent (fill the parent completely). Once you have that, and abandon the style pseudo classes, it should all come into 'focus' (pun intended). Who knows, at that point you may even be able to bring back some or all of Son of Suckerfish. But to make it work in the first place, you will find it easier to get rid of anything that could possibly be confusing the issue.

BTW, there is a :focus pseudo class, but I know it's less well supported than :hover.

billyboy
10-21-2008, 07:43 AM
All the anchors are set to display block, fill the parent, etc. I'm not sure what you mean by style psuedo classes, the classes created by javascript that styles are applied to in the CSS? Anything I've read states the prefered method is to use javascript to apply classes and CSS to apply styles. Any up to date examples of code I've seen for dropdowns use that. If you're mean abandoning that and using display: block/none in the javascript instead, it accomplishes the same thing as far as hiding the sub menus on graphic based user agents, but display: none hides the content from the majority of screen readers.

I'm really lost, what do I gain by changing the part that already works, that is totally separate from the onfocus function, that is based on a code that is widely used and has been proven to work repeatedly?

jscheuer1
10-21-2008, 04:27 PM
I'm really lost, what do I gain by changing the part that already works, that is totally separate from the onfocus function, that is based on a code that is widely used and has been proven to work repeatedly?

You gain seeing what exactly is happening. Since the anchors don't contain each other, the way that the list items do, events on the anchors do not effect anything but that anchor, and only one anchor may have focus at a time, while the mouse can be over several list items at once. Some method must be found to walk up and down the list tree(s) to see what should be done at each focus and blur event. I think it would be rather unwieldy perhaps even impossible. When I have more time, I'll have a whack at it.

billyboy
10-22-2008, 06:46 PM
John? What if you could apply focus to the li??? Then it would be no more complicated than what's required for the mouse events. Can't be done because focus only works on anchors and form inputs, or so we've always been told. But I found this over at quirksmode.org last night and tried it out: http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html

Here's my latest version (http://webwanderers.awardspace.com/menu-test4.php) using Peter-Paul Koch's technique to apply onfocus and onblur to the li. I've also added some rules to the CSS so without javascript there's partial keyboard funcionality. The code is still rough around the edges and because Opera fires the events twice its not working in that browser. Need a fix for that and I'd also like to know how to add a delay to the submenus closing on mouseout so cursor positioning isn't so critical for a user who has difficulties with motor skills.

jscheuer1
10-22-2008, 08:26 PM
That's pretty exciting! The bit with Opera, as I was playing around with this some, is that Opera fires the onmouseover and onmouseout events as if they are triggered by focus and blur. Opera doesn't need event capturing or focusin/out. So, just exclude it from both of those (using the standard window.opera object present only in Opera browsers):


if(!window.opera){
li.onfocusin = openSubMenu; // focus for ie
li.onfocusout = closeSubMenu;
if (li.addEventListener) { // focus for ff, safari, konquerer, etc.
li.addEventListener('focus', openSubMenu, true);
li.addEventListener('blur', closeSubMenu, true);
}
};

Cancelable delays can be worked in with setTimeouts, something like (untested):


function openSubMenu() {
var el = this;
openSubMenu.timer = setTimeout(function(){
if(/\bjs-hide\b/.test(el.className)) {
el.className = el.className.replace(/\bjs-hide\b/, "js-show");
};
}, 500);
}

and:


function closeSubMenu() {
if(openSubMenu.timer)
clearTimeout(openSubMenu.timer);
if(/\bjs-show\b/.test(this.className)) {
this.className = this.className.replace(/\bjs-show\b/, "js-hide");
}
}

billyboy
10-23-2008, 05:51 AM
Sorry, guess I wasn't clear. It's the javascript onfocus/onblur events that cause the problem in Opera. I realize that mouseover/mouseout are only needed by ie6. If you look at PPK's example page (http://www.quirksmode.org/focusblurexample.html) and navigate through the links using Ctrl + arrow, you can see blur, focus, blur occurs each time. I didn't know about the window.opera object but I'm thinking it might be used to add an extra condition (not sure what) to hideSubmenus for Opera only to account for that extra blur event.

Its only onmouseout that I want to set a delay, its not needed for the blur events. From everything I've read it should be as simple as adding something like:
li.onmouseout = function() {setTimeout(closeSubMenu, 500);}Or
li.onmouseout = delayedClose;

function delayedClose() {
delay = setTimeout(closeSubMenu, 500);
}I don't understand why that doens't work???

jscheuer1
10-23-2008, 10:22 AM
I did test my use of the window.opera object and the menu worked well in Opera with it the way I used it in my previous post. Feel free to use it however you like though.

The problem with:


li.onmouseout = function() {setTimeout(closeSubMenu, 500);}

and:


li.onmouseout = delayedClose;

function delayedClose() {
delay = setTimeout(closeSubMenu, 500);
}

is that by the time:


function closeSubMenu() {
if(/\bjs-show\b/.test(this.className)) {
this.className = this.className.replace(/\bjs-show\b/, "js-hide");
}
}

runs, there is no reference available for 'this' other than the window. Doing it like so should preserve the reference:


li.onmouseout = function(){delayedClose(this);};

function delayedClose(el) {
var delay = setTimeout(function(){closeSubMenu.apply(el);}, 500);
};

function closeSubMenu() {
if(/\bjs-show\b/.test(this.className))
this.className = this.className.replace(/\bjs-show\b/, "js-hide");
};

However, you now have a situation where the submenu will still close regardless of what your user who has difficulties with motor skills does during their 500 milliseconds. To have any real value, you need to clear the timeout should their mouse re-enter the trigger to open the submenu 'while the clock is ticking' on closing it.

So I would suggest:


li.onmouseout = function(){delayedClose(this);};

function delayedClose(el) {
delayedClose.delay = setTimeout(function(){closeSubMenu.apply(el);}, 500);
};

function openSubMenu() {
if(delayedClose.delay)
clearTimeout(delayedClose.delay);
if(/\bjs-hide\b/.test(this.className))
this.className = this.className.replace(/\bjs-hide\b/, "js-show");
};

function closeSubMenu() {
if(/\bjs-show\b/.test(this.className))
this.className = this.className.replace(/\bjs-show\b/, "js-hide");
};

Which works out well (just tested), except if we mouseout of 'page1' and then immediately mouseover page2, as the delay is still cancelled, but shouldn't be in that case. Not sure what to do about that just yet.

In any case, this is what I have so far:


<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<title>Menu Test 4</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<style type="text/css">
* {
margin: 0;
padding: 0;
}
img {
border: 0;
}
body {
font: 75% arial, helvetica, sans-serif;
color: #000;
background: #fff;
padding: 5em
}
#nav, #nav ul { /* all levels */
list-style: none;
}
#nav li {
position: relative;
float: left;
width: 60px;
}
#nav a {
display: block;
width: 100%;
padding: 2px 0;
text-align: center;
text-decoration: none;
color: #930;
background: #fec;
border: 1px solid #930;
}
#nav a:focus, #nav a:hover, #nav a:active {
background: #fcdfaf;
color: #600;
}
#nav ul { /* submenus */
position: absolute;
margin-left: -10000px;
width: 60px;
}
#nav li:hover ul, #nav li.js-show ul { /* open 2nd level */
margin-left: 0;
}
#nav li:hover li ul, #nav li.js-show li ul { /* hide 3rd level when 2nd level open */
margin-left: -10000px;
}
#nav li li:hover ul, #nav li li.js-show ul { /* open 3rd level */
margin: -21px 0 0 61px;
}

/* These rules add partial keyboard functionality if js is not available */

#nav ul a:focus, #nav ul a:active { /* show 2nd level anchors on focus */
margin-left: 10000px;
}
#nav ul ul a:focus, #nav ul ul a:active { /* show 3rd level anchors on focus */
margin: -21px 0 0 20061px;
}
/* cancel out the last 2 rules if javascript is available */
#nav li.js-show ul a:focus, #nav li.js-show ul a:active, #nav li.js-show ul ul a:focus, #nav li.js-show ul ul a:active {
margin: 0;
}
</style>

<script type="text/javascript">
function menuHide(objId) {
var el, li, x, y;
el = document.getElementById(objId).getElementsByTagName("LI");
for (x in el) {
li = el[x];
for (y in li.childNodes)
if (li.childNodes[y].nodeName == 'UL')
li.className += " js-hide";
li.onmouseover = openSubMenu; // hover for ie6
li.onmouseout = function(){delayedClose(this);};
if(!window.opera){
li.onfocusin = openSubMenu; // focus for ie
li.onfocusout = closeSubMenu;
if (li.addEventListener) { // focus for ff, safari, konquerer, etc.
li.addEventListener('focus', openSubMenu, true);
li.addEventListener('blur', closeSubMenu, true);
};
};
};
};

function delayedClose(el) {
delayedClose.delay = setTimeout(function(){closeSubMenu.apply(el);}, 500);
};

function openSubMenu() {
if(delayedClose.delay)
clearTimeout(delayedClose.delay);
if(/\bjs-hide\b/.test(this.className))
this.className = this.className.replace(/\bjs-hide\b/, "js-show");
};

function closeSubMenu() {
if(/\bjs-show\b/.test(this.className))
this.className = this.className.replace(/\bjs-show\b/, "js-hide");
};

function addEvent(obj, evt, func) {
if(obj.addEventListener) obj.addEventListener(evt, func, false);
else if(obj.attachEvent) obj.attachEvent("on" + evt, func);
else obj['on' + evt] = func;
};

addEvent(window, "load", function() {
menuHide("nav");
}
);
</script>
</head>

<body>
<ul id="nav">
<li class="aaaa js-showShouldNotChange"><span><a href="#">Page 1</a></span>
<ul>
<li><a href="#">Page 1a</a></li>
<li><span><span><a href="#">Page 1b</a></span></span>
<ul>
<li><a href="#">Page 1aa</a></li>
<li><a href="#">Page 1bb</a></li>
</ul>
</li>
</ul>
</li>
<li class="aaaa bbbb"><a href="#"><span>Page 2</span></a>
<ul>
<li><a href="#">Page 2a</a></li>
<li class="js-hideShouldNotChange"><span><a href="#"><span>Page 2b</span></a></span>
<ul>
<li><a href="#">Page 2aa</a></li>
<li><a href="#">Page 2bb</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</body>
</html>