Pure JS Slide-Menu, ability to close it "on click outside of the menu"

Question

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.


Show source
| javascript   | jquery   | html   | css   2017-01-03 17:01 3 Answers

Answers to Pure JS Slide-Menu, ability to close it &quot;on click outside of the menu&quot; ( 3 )

  1. 2017-01-03 17:01

    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...

  2. 2017-01-05 18:01

    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>

  3. 2017-01-10 13:01

    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/

Leave a reply to - Pure JS Slide-Menu, ability to close it "on click outside of the menu"

◀ Go back