1

I am just learning Javascript (very beginning) and I am having an issue when trying to make a hidden "sub navigation" menu appear when focus is applied to an element. I am keeping it very basic (I think).

The idea is the same as displaying a hidden "sub navigation" menu on hover. When user tabs to element and focus is applied the menu shows and the links in the menu become focusable. When the user tabs off of the menu, the menu hides itself again. I want this happen on any number of navigation elements.

The HTML:

<nav role="navigation">
   <ul>
      <li><a href="#">Link 1</a></li>
      <li><a href="#" class="has-dropdown">Link 2</a>
         <ul class="dropdown">
            <li><a href="#">Link 2a</a></li>
            <li><a href="#">Link 2b longer</a></li>
            <li><a href="#">Link 2c longest text</a></li>
         </ul>
      </li>
      <li><a href="#">Link 3</a></li>
      <li><a href="#">Link 4</a></li>
   </ul>
</nav>

Not sure you need it but here is the CSS:

nav[role=navigation] {
  width: 100%;
  background: #505050;
  font-family: Oswald,sans-serif;
  font-weight: 200;
}

nav[role=navigation] ul {
  margin: 0;
  padding: 0;
  list-style: none;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
  -webkit-flex-wrap: wrap;
  -ms-flex-wrap: wrap;
  flex-wrap: wrap;
  -webkit-justify-content: flex-start;
  -ms-flex-pack: start;
  justify-content: flex-start;
}

nav[role=navigation] ul li {
  -webkit-flex: 0 1 auto;
  -ms-flex: 0 1 auto;
  flex: 0 1 auto;
  text-align: center;
  padding: .8333rem;
  border-right: 1px solid #252525;
  position: relative;
}

nav[role=navigation] ul li:last-of-type {
  border-right: none;
}

nav[role=navigation] ul li:hover a + ul {
  display: block;
}

nav[role=navigation] ul ul {
  list-style: none;
  position: absolute;
  top: 100%;
  left: 0;
  background: #252525;
  padding: 0;
  width: 200%;
  text-align: left;
  display: none;
}

nav[role=navigation] ul ul.active {
  display: block;
}

nav[role=navigation] ul a {
  padding: 0 1rem;
  color: #fff;
  text-decoration: none;
}

nav[role=navigation] ul a:focus,
nav[role=navigation] ul a:hover {
  text-decoration: underline;
  color: #ccc;
}

The Javascript:

var topLevel = document.getElementsByClassName("has-dropdown");

for (var i = 0; i < topLevel.length; i++) {
   topLevel[i].onfocus = function() {
      console.log("you have activated the dropdown");
      topLevel.nextElementSibling.className = "active";
   }
}

When I tab to the link which triggers this, I get the log message correctly on any link which has a sub menu but the sub menu does not display. When I run from the console I get the error:

"Cannot set property "className" of undefined."

I believe this has something to do with the variable being an array because if I set the index of topLevel such as topLevel[0] it works perfectly.

I want to use vanilla JS. No JQuery.

I have searched for similar questions but cant find any answers which are over my head or only give a JQuery solution.

I've looked at:

Drop down menu to hide on click event
For-each over an array in JavaScript? --> This one made my brain bleed.
Target items in an array on click?
How to split an array with retrieved values from DOM

The answer may be hiding in one of the above posts but if it is, I can't find it.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
djlotus
  • 19
  • 1
  • 6

2 Answers2

0

I came into the same problem in my project https://github.com/arthurdd/scales/blob/master/index.js#L60

Basically what I did was define a function add_listeners_all (you probably wouldn't need such a function if you were only doing this once)

Here's the code

function add_listeners_all( selector, fun ) {
    Array.prototype.slice.apply(document.querySelectorAll(selector)).forEach(el) {
        el.addEventListener('click', fun);
    });
});

I don't remember the semantics of how I got it to do that, but it works perfectly, as you can see in the live demo http://backtick.town/~jay/scales (under development, don't expect for it to work yet)

As you can see, you can replace document.querySelectorAll(selector) with your toplevel array, and fun for your function.

Again, this memory is all very blurry to me, so this may not work. Feel free to downvote and vent your rage at me if it doesn't work.

birdoftheday
  • 816
  • 7
  • 13
  • not sure how this applies. you state "I don't remember the semantics of how I got it to do that, but it works perfectly, as you can see in the live demo http://backtick.town/~jay/scales" but then state "(under development, don't expect for it to work yet)". I clicked on your project and your link but fail to see anything relating to my question. Maybe I am missing it. can you explain further? As I said I just started learning and may not be connecting the dots properly. – djlotus Dec 20 '15 at 18:02
  • Sorry. The way I dealt with giving elements event listeners was by looping through them and setting event listeners like you are trying to do here. You can easily apply this technique by using a forEach loop and `addEventListener`. – birdoftheday Dec 20 '15 at 18:15
  • I will look into that. – djlotus Dec 20 '15 at 18:17
  • Is there a fundamental flaw in using a simple for loop like I am trying to do? I would prefer to do it this way. Getting back to the original question; why is this not working? – djlotus Dec 21 '15 at 00:55
  • Looking into your code it seems as if the line `toplevel[i].onfocus` is causing the problem. `blabla.onfocus` and friends can only handle one event listener. Are you setting another focus event somewhere else? – birdoftheday Dec 21 '15 at 02:06
  • Currently there is no other event listener. The JS I posted is all the JS. – djlotus Dec 21 '15 at 13:23
0

In modern browsers you should add a listener:

var topLevel = document.getElementsByClassName("has-dropdown");

for (var i = 0; i < topLevel.length; i++) {
   topLevel[i].addEventListener('focus',function(event) {
      console.log("you have activated the dropdown");
     event.currentTarget.nextElementSibling.className  = "active";
   },false);
}

Reference: addEventListener not working in IE8

OLDER browser

var topLevel = document.getElementsByClassName("has-dropdown");

for (var i = 0; i < topLevel.length; i++) {
    if (topLevel[i].addEventListener) {
          topLevel[i].addEventListener('focus',function(event) {
          console.log("you have activated the dropdown");
          event.currentTarget.nextElementSibling.className  = "active";
       },false);
    }
    else {
        topLevel[i].attachEvent("focus", function(event) {
        console.log("you have activated the dropdown");
        event.currentTarget.nextElementSibling.className  = "active";
   });
}
Community
  • 1
  • 1
Mark Schultheiss
  • 32,614
  • 12
  • 69
  • 100
  • to remove the class is up to you (when) to do that but reference this: `.classList.remove("active");` – Mark Schultheiss Dec 21 '15 at 07:18
  • The reason I was avoiding `addEventListener` is because I need to support IE8. Sucks, I know, but necessary. I will default to this method if providing support for sad browsers is too much work for a rookie like me. – djlotus Dec 21 '15 at 13:26
  • Ah, makes sense for the IE thing then. You could review the jQuery code to see HOW they normalize this for events (and still not use jQuery)...it is pretty well vetted and tested there - say in the 1.11.x version. – Mark Schultheiss Dec 21 '15 at 13:57
  • Thanks for the update on your answer. The first part works great. I will test the second part tomorrow morning. I have it adding the class and removing the class properly. Now I just need to figure out how to target the parent `ul` from the second level child `a` to keep the menu visible while tabbing through the submenu. Is there any reason why I couldn't use a second focus event applied to a different element, say the link in the submenu? – djlotus Dec 22 '15 at 03:55
  • Normally for "menu" type elements, these are done with mouseover or mousein/mouseout and click (to select the menu link) - OR a "key" such as the return key press when an element is focused...up to you really but see how OTHER people do menus to not have surprises in yours – Mark Schultheiss Dec 22 '15 at 03:58
  • I have it working with `:hover` and `:focus` for mouse users. The whole purpose of this bit is to give keyboard users the same functionality. Having the menu activated by keypress is something I haven't thought of and will give serious thought to. Thanks again for your help. This may have been a bad choice of a starting point for learning JS. ha. – djlotus Dec 22 '15 at 13:26