13

I am trying to rephrase my question and will go through all the steps i did and especially where i failed. I don't have a deep knowledge of JS but the will to learn by practice as well as the help of the community.

I stumbled across this answer and realized the benefit. Since i don't want to use jQuery i started to rewrite it in JS.

  1. First step was a to write a basic simple function to open the menu on 'click' and close it on a click outside of the focused element using the blur(); method.

Reference jQuery code from @zzzzBov :

$('a').on('click', function () {
  $(this.hash).toggleClass('active').focus();
});

$('div').on('focusout', function () {
  $(this).removeClass('active');
});

My JS code:

var navToggle = document.getElementsByClassName('js-site-nav-btn--toggle')[0];
var navMenu = document.getElementsByClassName('js-site-nav')[0];

navToggle.addEventListener('click', function() {
  this.focus();
  navMenu.classList.toggle('js-site-nav--open');
});

navMenu.addEventListener('blur', function() {
  this.classList.remove('js-site-nav--open');
}, true);

Opening the menu works, the problem is that it will only close on 'click' outside of the menu if the focused element (Menu) is clicked once before:

var navToggle = document.getElementsByClassName('js-site-nav-btn--toggle')[0];
var navMenu = document.getElementsByClassName('js-site-nav')[0];

navToggle.addEventListener('click', function() {
  this.focus();
  navMenu.classList.toggle('js-site-nav--open');
});

navMenu.addEventListener('blur', function() {
  this.classList.remove('js-site-nav--open');
}, true);
.c-site-nav {
  color: black;
  list-style-type: none;
  padding-top: 20px;
  position: fixed;
  overflow: hidden;
  top: 0;
  right: -200px;
  width: 200px;
  height: 100%;
  transition: right .6s cubic-bezier(0.190, 1.000, 0.220, 1.000);
  opacity: .9;
  background-color: green;
}
.js-site-nav--open {
  right: 0;
}
.c-site-nav-btn:hover {
  cursor: pointer;
  background-color: red;
}
.c-site-nav-btn {
  position: fixed;
  top: 20px;
  right: 20px;
  border: 0;
  outline: 0;
  background-color: black;
  position: fixed;
  width: 40px;
  height: 40px;
}
.c-site-nav-btn__line {
  width: 20px;
  height: 2px;
  background-color: white;
  display: block;
  margin: 5px auto;
}
<button class="c-site-nav-btn js-site-nav-btn--toggle">
  <span class="c-site-nav-btn__line"></span>
  <span class="c-site-nav-btn__line"></span>
  <span class="c-site-nav-btn__line"></span>
</button>
<nav class="c-site-nav js-site-nav" tabindex="-1" role="navigation">
  <ul class="c-site-nav__menu">
    <li>
      <a class="c-site-nav__item js-site-nav__item" href="/">TOPMENU</a>
    </li>
    <li>SUBMENU
      <ul>
        <li>
          <a class="c-site-nav__item js-site-nav__item" href="/">MENU</a>
        </li>
        <li>
          <a class="c-site-nav__item js-site-nav__item" href="/">MENU</a>
        </li>
        <li>
          <a class="c-site-nav__item js-site-nav__item" href="/">MENU</a>
        </li>
      </ul>
    </li>
    <li>
      <a class="c-site-nav__item js-site-nav__item" href="/portfolio">TOPMENU</a>
    </li>
  </ul>
</nav>
  1. I tried to continue with the second step, that was addressing to the two major issues:

The first is that the link in the dialog isn't clickable. Attempting to click on it or tab to it will lead to the dialog closing before the interaction takes place. This is because focusing the inner element triggers a focusout event before triggering a focusin event again.

The fix is to queue the state change on the event loop. This can be done by using setImmediate(...), or setTimeout(..., 0) for browsers that don't support setImmediate. Once queued it can be cancelled by a subsequent focusin:

The second issue is that the dialog won't close when the link is pressed again. This is because the dialog loses focus, triggering the close behavior, after which the link click triggers the dialog to reopen.

Similar to the previous issue, the focus state needs to be managed. Given that the state change has already been queued, it's just a matter of handling focus events on the dialog triggers:

Reference jQuery code from @zzzzBov :

$('a').on('click', function () {
  $(this.hash).toggleClass('active').focus();
});

$('div').on({
  focusout: function () {
    $(this).data('timer', setTimeout(function () {
      $(this).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this).data('timer'));
  }
});

$('a').on({
  focusout: function () {
    $(this.hash).data('timer', setTimeout(function () {
      $(this.hash).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this.hash).data('timer'));  
  }
});

My JS code:

var navToggle = document.getElementsByClassName('js-site-nav-btn--toggle')[0];
var navMenu = document.getElementsByClassName('js-site-nav')[0];
var navLink = document.getElementsByClassName('js-site-nav__item')[0];

navToggle.addEventListener('click', function() {
  this.focus();
  navMenu.classList.toggle('js-site-nav--open');
});

navMenu.addEventListener('focus', function() {
  this.blur(function() {
    setTimeout(function() {
      this.classList.remove('js-site-nav--open');
    }.bind(this), 0);
  });
  this.focus(function() {
    clearTimeout();
  });
});

navLink.addEventListener('blur', function() {
  navLink.blur(function() {
    setTimeout(function() {
      navMenu.classList.remove('js-site-nav--open');
    }.bind(), 0);
  });
  navLink.focus(function() {
    clearTimeout();
  });
});

Opening the menu still works, but closing on click outside stoped working, after research i figured that blur and focus are the right methods but i guess i am missing something essential.

var navToggle = document.getElementsByClassName('js-site-nav-btn--toggle')[0];
var navMenu = document.getElementsByClassName('js-site-nav')[0];
var navLink = document.getElementsByClassName('js-site-nav__item')[0];

navToggle.addEventListener('click', function() {
  this.focus();
  navMenu.classList.toggle('js-site-nav--open');
});

navMenu.addEventListener('focus', function() {
  this.blur(function() {
    setTimeout(function() {
      this.classList.remove('js-site-nav--open');
    }.bind(this), 0);
  });
  this.focus(function() {
    clearTimeout();
  });
});

navLink.addEventListener('blur', function() {
  navLink.blur(function() {
    setTimeout(function() {
      navMenu.classList.remove('js-site-nav--open');
    }.bind(), 0);
  });
  navLink.focus(function() {
    clearTimeout();
  });
});
.c-site-nav {
  color: black;
  list-style-type: none;
  padding-top: 20px;
  position: fixed;
  overflow: hidden;
  top: 0;
  right: -200px;
  width: 200px;
  height: 100%;
  transition: right .6s cubic-bezier(0.190, 1.000, 0.220, 1.000);
  opacity: .9;
  background-color: green;
}
.js-site-nav--open {
  right: 0;
}
.c-site-nav-btn:hover {
  cursor: pointer;
  background-color: red;
}
.c-site-nav-btn {
  position: fixed;
  top: 20px;
  right: 20px;
  border: 0;
  outline: 0;
  background-color: black;
  position: fixed;
  width: 40px;
  height: 40px;
  z-index:9999;
}
.c-site-nav-btn__line {
  width: 20px;
  height: 2px;
  background-color: white;
  display: block;
  margin: 5px auto;
}
<button class="c-site-nav-btn js-site-nav-btn--toggle">
  <span class="c-site-nav-btn__line"></span>
  <span class="c-site-nav-btn__line"></span>
  <span class="c-site-nav-btn__line"></span>
</button>
<nav class="c-site-nav js-site-nav" tabindex="-1" role="navigation">
  <ul class="c-site-nav__menu">
    <li>
      <a class="c-site-nav__item js-site-nav__item" href="/">TOPMENU</a>
    </li>
    <li>SUBMENU
      <ul>
        <li>
          <a class="c-site-nav__item js-site-nav__item" href="/">MENU</a>
        </li>
        <li>
          <a class="c-site-nav__item js-site-nav__item" href="/">MENU</a>
        </li>
        <li>
          <a class="c-site-nav__item js-site-nav__item" href="/">MENU</a>
        </li>
      </ul>
    </li>
    <li>
      <a class="c-site-nav__item js-site-nav__item" href="/portfolio">TOPMENU</a>
    </li>
  </ul>
</nav>

I am sure there is still a lot i have to learn, but help would be much appreciated. Thanks a lot guys.

Community
  • 1
  • 1
HendrikEng
  • 654
  • 1
  • 10
  • 32
  • It actually works as intended. The issue is that because the menu is absolutely positioned, the body has no height, so there's no way to click on it. If you add `body { height: 600px }` or something like that to your CSS, you should see it work as you're expecting – jmcgriz Jan 03 '17 at 16:38
  • Have you thought about adding a transparent overlay element that is when clicked on closes the menu? That will make it much easier and you won't have to deal with focus/blur events. – Ali Abdelfattah Jan 05 '17 at 16:34
  • Hey Ali, yes i thought about that, but i kinda thought, especially when reading through the mentioned answer above, that its more like a temporary solution, and it wouldn't it prohibit the ability to scroll through the site with the nav open i.e. ? – HendrikEng Jan 05 '17 at 16:36
  • No, you'll be able to scroll the site below the overlay and you won't be able to interact with it with the menu open but you wouldn't have interaction with blur event as well. – Ali Abdelfattah Jan 05 '17 at 16:55
  • Hm, i guess thats more like a last resort option then, i think i just adapted the given jQuery code wrong – HendrikEng Jan 05 '17 at 16:58

3 Answers3

9

You could set the focus on navmenu as soon as it is displayed. If the user clicks outside of it, the blur event would be triggered and the menu would be removed. Since clicking on the links also triggers the blur event, we have to keep the menu on the screen when the users clicks anywhere inside of the menu. This can be monitored with a isMouseDown flag.

Here is an enhanced version of the code snippet given in Part 1 of your question.

var navToggle = document.getElementsByClassName('js-site-nav-btn--toggle')[0];
var navMenu = document.getElementsByClassName('js-site-nav')[0];
var isMouseDown = false;

navToggle.addEventListener('click', function() {
  this.focus();
  navMenu.classList.toggle('js-site-nav--open');
  navMenu.focus();
});

navMenu.addEventListener('mousedown', function() {
  isMouseDown = true;  
});

navMenu.addEventListener('mouseup', function() {
  isMouseDown = false;  
});

navMenu.addEventListener('mouseleave', function() {
  isMouseDown = false;  
});

navMenu.addEventListener('blur', function() {
  if (!isMouseDown) {
    navMenu.classList.remove('js-site-nav--open');
  }
}, true);
.c-site-nav {
  color: black;
  list-style-type: none;
  padding-top: 20px;
  position: fixed;
  overflow: hidden;
  top: 0;
  right: -200px;
  width: 200px;
  height: 100%;
  transition: right .6s cubic-bezier(0.190, 1.000, 0.220, 1.000);
  opacity: .9;
  background-color: green;
}
.js-site-nav--open {
  right: 0;
}
.c-site-nav-btn:hover {
  cursor: pointer;
  background-color: red;
}
.c-site-nav-btn {
  position: fixed;
  top: 20px;
  right: 20px;
  border: 0;
  outline: 0;
  background-color: black;
  position: fixed;
  width: 40px;
  height: 40px;
}
.c-site-nav-btn__line {
  width: 20px;
  height: 2px;
  background-color: white;
  display: block;
  margin: 5px auto;
}
<button class="c-site-nav-btn js-site-nav-btn--toggle">
  <span class="c-site-nav-btn__line"></span>
  <span class="c-site-nav-btn__line"></span>
  <span class="c-site-nav-btn__line"></span>
</button>
<nav class="c-site-nav js-site-nav" tabindex="-1" role="navigation">
  <ul class="c-site-nav__menu">
    <li>
      <a class="c-site-nav__item js-site-nav__item" href="/">TOPMENU</a>
    </li>
    <li>SUBMENU
      <ul>
        <li>
          <a class="c-site-nav__item js-site-nav__item" href="/">MENU</a>
        </li>
        <li>
          <a class="c-site-nav__item js-site-nav__item" href="/">MENU</a>
        </li>
        <li>
          <a class="c-site-nav__item js-site-nav__item" href="/">MENU</a>
        </li>
      </ul>
    </li>
    <li>
      <a class="c-site-nav__item js-site-nav__item" href="/portfolio">TOPMENU</a>
    </li>
  </ul>
</nav>
ConnorsFan
  • 70,558
  • 13
  • 122
  • 146
  • Thanks a lot ConnorsFan, that really helped, and makes sense. So i guess i don't need the timeout part, it kinda confused me since the links worked form the start and i didn't encounter the problem that was addressed in the jQuery example. Just realized you explained it as well ...thanks ...really appreciated. – HendrikEng Jan 05 '17 at 17:27
  • I haven't really studied your second method, which uses timeouts. I just made sure that the menu is not hidden if the blur event is caused by a click inside the menu. That seems to work well in Chrome, FF and IE11. – ConnorsFan Jan 05 '17 at 17:48
3

I have recently come up against this same issue, and it's not as tricky as it sounds. You need to give your trigger a 'tabindex' (to make it focusable, 0 is good). Give it a 'click' event handler like so...

document.getElementById('myTrigger').addEventListener('click', function(){this.focus(); this.classList.toggle('openClass');});

Where 'openClass' is the one which triggers the menu. Then (assuming var myTrigger)...

myTrigger.addEventListener('blur', function(){ this.classList.remove('openClass');})

Here, clicking the toggle switches the open class on and off, but it also prgramatically sets the focus. When clicking away, the element loses focus, the 'blur' event fires and the handler removes the class...

allnodcoms
  • 1,244
  • 10
  • 14
  • i just started to understand what u mean , this question http://stackoverflow.com/questions/152975/how-do-i-detect-a-click-outside-an-element/ and especially the answer from @zzzzBev really helped as well as yours, i am trying to figure out how to adapt it to my code and will update my question as soon as i run into a wall ...thanks a lot – HendrikEng Jan 04 '17 at 14:52
  • BINGO! Just what i was looking for. Gonna wrap it into a class loop and run it through an if(else), a `+` for the approach and `+` on the vanilla approach :) – James Walker Jun 12 '18 at 21:55
1

I took a different approach. I use a toggleClass to determine wether or not the menu is open. Based on this classname I changed your css so the menu would open whenever the class 'showMenu' is added to our html tag.

The code in the clickOutside method checks if you are clicking outside the given classNames (in this case that is .js-site-nav and .js-site-nav-btn--toggle). If the elements you clicked aren't the elements with the given classnames, then the menu will close.

Sorry for the bad markup in this response, I'm at work so when I'm home I'll try to improve this message.

Here is the code I used:

HTML

<div class="container">

    <button class="c-site-nav-btn js-site-nav-btn--toggle">
        <span class="c-site-nav-btn__line"></span>
        <span class="c-site-nav-btn__line"></span>
        <span class="c-site-nav-btn__line"></span>
    </button>
    <nav class="c-site-nav js-site-nav" tabindex="-1" role="navigation">
        <ul class="c-site-nav__menu">
            <li>
                <a class="c-site-nav__item js-site-nav__item" href="/">TOPMENU</a>
            </li>
            <li>SUBMENU
                <ul>
                    <li>
                        <a class="c-site-nav__item js-site-nav__item" href="/">MENU</a>
                    </li>
                    <li>
                        <a class="c-site-nav__item js-site-nav__item" href="/">MENU</a>
                    </li>
                    <li>
                        <a class="c-site-nav__item js-site-nav__item" href="/">MENU</a>
                    </li>
                </ul>
            </li>
            <li>
                <a class="c-site-nav__item js-site-nav__item" href="/portfolio">TOPMENU</a>
            </li>
        </ul>
    </nav>
</div>

CSS

.c-site-nav {
  color: black;
  list-style-type: none;
  padding-top: 20px;
  position: fixed;
  overflow: hidden;
  top: 0;
  right: -200px;
  width: 200px;
  height: 100%;
  transition: right .6s cubic-bezier(0.190, 1.000, 0.220, 1.000);
  opacity: .9;
  background-color: green;
}
.showMenu .js-site-nav {
  right: 0;
}
.c-site-nav-btn:hover {
  cursor: pointer;
  background-color: red;
}
.c-site-nav-btn {
  position: fixed;
  top: 20px;
  right: 20px;
  border: 0;
  outline: 0;
  background-color: black;
  position: fixed;
  width: 40px;
  height: 40px;
  z-index:9999;
}
.c-site-nav-btn__line {
  width: 20px;
  height: 2px;
  background-color: white;
  display: block;
  margin: 5px auto;
}

.container
{
    width: 100%;
    height: 100%;
    background: red;
}

JavaScript

var $parent = $('html');
var toggleClass = 'showMenu';
var container = $(".js-site-nav, .js-site-nav-btn--toggle");

function init()
{
    $('.js-site-nav-btn--toggle').on('click touchend', toggleMenu);
    $(document).on('click touchend', clickOutside);
}

function toggleMenu()
{
    $parent.toggleClass(toggleClass);
}

function clickOutside(e)
{ 

    if (!container.is(e.target) // if the target of the click isn't the container...
    && container.has(e.target).length === 0
    && $parent.hasClass(toggleClass)) // ... nor a descendant of the container
    {
        $parent.removeClass(toggleClass);
    }
}
init();

https://jsfiddle.net/h7drcett/9/

Nicholas
  • 5,430
  • 3
  • 21
  • 21
  • Hey @Nicholas , thanks for the approach, it works but i tried to avoid jQuery. – HendrikEng Jan 11 '17 at 16:58
  • No problem, it should be possbile to take this approach in pure javascript. Not sure why you used the jquery tag though to label this post. – Nicholas Jan 12 '17 at 13:08