最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

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

programmeradmin1浏览0评论

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>

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.

Share Improve this question edited May 23, 2017 at 12:24 CommunityBot 11 silver badge asked Jan 3, 2017 at 16:21 HendrikEngHendrikEng 6841 gold badge11 silver badges32 bronze badges 5
  • 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 Commented Jan 3, 2017 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 Commented Jan 5, 2017 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 Commented Jan 5, 2017 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 Commented Jan 5, 2017 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 Commented Jan 5, 2017 at 16:58
Add a comment  | 

3 Answers 3

Reset to default 9 +50

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>

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

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/

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论