0

I have small widget that has a list of links, which can open and close:

<div class="widget">
    <input class="focuser">
    <a href="javascript:;">View options</a>
    <ul>
        <li>Option 1</li>
        <li>Option 1</li>
    </ul>
</div>

So, when I click on View options - list appears, and when click on any option - do stuff and close list of options.

And I need to have click-outside-close functionality - when user clicks anywhere outside just close the list.

I achieved this by focusing an invisible input when showing the options, and on blur of the input - hide the options after some timeout. This way:

  1. when user clicks outside - the blur timeout runs out - hide the options.
  2. if user clicks any option - clear the timeout, hide options & do stuff.

And the problem now is that sometimes the timeout ends before the click. I increased the timeout interval to 200ms, but it's still not 100% sure, and also - this way user sees delay before the options list close.

Any idea how to fix and/or improve this?

EDIT:

This is simplified version of the JavaScript code:

CartSidebar.DwCustomSelect = Ember.View.extend({

    blurTimeout: false,

    focusOut: function(e) {
        if (e.target.className == 'focuser') {
            var self = this;
            this.blurTimeout = setTimeout(function() {
                clearTimeout(self.blurTimeout);
                $(self.element).removeClass('opened'); // hide options .. 
            }, 180);
        }
    },

    click: function(e) {

        var clickedElement = e.target;

        clearTimeout(this.blurTimeout);
        if (clickedElement.tagName.toLowerCase() == 'li') {
            $(this.element).removeClass('opened'); // hide options .. 
            doStuff();
        }
    }
}

EDIT 2:

I created a plunker, so you can see it in action:

https://plnkr.co/edit/boA6yC0sEbLAZU9tjyso?p=preview

(I did the timeout too small, so you see the problem. But even if you increase the interval - you'll see that the focusOut triggers before the click.)

pesho hristov
  • 1,946
  • 1
  • 25
  • 43
  • Why do you need a timeout? Why not just hide the list immediately after clicking outside the list? – Matt Spinks Jan 23 '17 at 19:43
  • Just like @MattSpinks said, get rid of `setTimeout`. If you insist on having the delay using `setTimeout`, then post your code to see where is the problem. – ibrahim mahrir Jan 23 '17 at 19:54
  • Because in the case you click on option - the sequence will be `blur of input` - `hiding options` - `click` - but the options won't be there, so `click` triggers on the element below the options. – pesho hristov Jan 23 '17 at 20:28

2 Answers2

1

If you want a functionality where you need to hide your div when user clicks anywhere except that div then you can add a click listener to whole document with the code to hide div.

var a = document.querySelector("a");
var ul = document.querySelector("ul");
a.addEventListener('click', function(e){
e.stopPropagation();
if(ul.classList.contains('show')){
    ul.classList.remove('show');
    ul.classList.add('hide');
}else{
    ul.classList.remove('hide');
    ul.classList.add('show');
}   
});

document.addEventListener('click', function(){
    ul.classList.remove('show');
    ul.classList.add('hide');
});

For detail example check the jsbin below

http://jsbin.com/mamexev/edit?html,css,js,output

Sam
  • 317
  • 3
  • 12
  • I was thinking of such solution too - globally listen to the document `click` - but the widgets is actually an `Ember` view, and it has it's own `click` handler, that I was/am supposed to use. That listener is bound to the top level element of the widget. This is where I'm checking if an option is clicked and clear the timeout from the input. – pesho hristov Jan 23 '17 at 21:41
  • Could you update your question with your current javascript code you have written to blur and hide list? – Sam Jan 24 '17 at 08:57
  • Sure. I just did :) – pesho hristov Jan 24 '17 at 18:59
  • I added a plunkr also. – pesho hristov Jan 25 '17 at 20:38
0

The timeout thing is one option. But you must define a large interval because it will depend on how fast the browser processes everything. It will work 99.99% of the times, but it's not bulletproof, as you said.

Other trick is to listen to the blur of the whole thing. If you assing a tabindex attribute to an element, it will receive focus events. If you set tabindex="0" it will get an automatic index and you won't have to worry about it.

So, instead of listening to the blur of the input, listen to the blur of the div .widget. When the focus goes from the input to the link or the lis, it will be still within the focus of widget and the blur event won't fire.

Gabriel
  • 2,170
  • 1
  • 17
  • 20
  • I tried your suggestion but when I click on an option from the list - the parent element looses focus, although it has `tabindex`. I debugged and saw that the focus goes to the clicked element - the option from the dropdown. – pesho hristov Jan 24 '17 at 20:54
  • Yes, the focus changes but the `blur` (or `focusOut`, is the same for this matter) event *of `.widget`* shouldn't fire (`blur` of the input will fire). You just have to listen to the proper element's events. Please, post your code to check on it if that isn't the problem. – Gabriel Jan 25 '17 at 03:27
  • I edited my question earlier today and added my JS code. Is it OK, or you want to take a look at something else? :) – pesho hristov Jan 25 '17 at 03:30
  • Yes, I meant the try you did with the `tabindex` suggestion. – Gabriel Jan 25 '17 at 06:47
  • I added a plunkr also. – pesho hristov Jan 25 '17 at 20:38