user7267879
user7267879

Reputation:

How to hide a dropdown when the user clicks outside of it

Below is a simplified version of the input dropdown I am working with.

A basic summary of what it does is: if you focus on the input, a dropdown appears. If you click one of the options in the dropdown, the option populates the input and the dropdown disappears. This is achieved using onfocus and a functions I called dropdown(); and undropdown();.

I'm in a dilemma, where I'm unable to make the dropdown disappear when someone clicks elsewhere. If I use onblur, it successfully hides the dropdown, but if you click on an option it doesn't populate the input, this is because, the onblur function runs first and, therefore, the input(); function doesn't not run because the dropdown is already hidden.

If you put an onclick on the body tag, or other parent, it considers the onfocus as a click, where it run's the dropdown(); function then the undropdown(); function immediately so the dropdown never appears since the functions overlap.

I would appreciate help on figuring out how to order the functions so that they are executed in the right order without overlapping with each other.

JSFiddle available here.

function input(pos) {
  var dropdown = document.getElementsByClassName('drop');
  var li = dropdown[0].getElementsByTagName("li");
  document.getElementsByTagName('input')[0].value = li[pos].innerHTML;
  undropdown(0);
}
function dropdown(pos) {
  document.getElementsByClassName('content')[pos].style.display = "block"
}
function undropdown(pos) {
  document.getElementsByClassName('content')[pos].style.display = "none";
}
.drop {
  position: relative;
  display: inline-block;
  vertical-align: top;
  overflow: visible;
}
.content {
  display: none;
  list-style-type: none;
  border: 1px solid #000;
  padding: 0;
  margin: 0;
  position: absolute;
  z-index: 2;
  width: 100%;
  max-height: 190px;
  overflow-y: scroll;
}
.content li {
  padding: 12px 16px;
  display: block;
  margin: 0;
}
<div class="drop">
  <input type="text" name="class" placeholder="Class" onfocus="dropdown(0)"/>
  <ul class="content">
    <li onclick="input(0)">Option 1</li>
    <li onclick="input(1)">Option 2</li>
    <li onclick="input(2)">Option 3</li>
    <li onclick="input(3)">Option 4</li>
  </ul>
</div>

PS: In addition to the above problem, I would appreciate suggestion for edits to get a better title for this question such that someone experiencing a similar problem could find it more easily.

Upvotes: 11

Views: 14670

Answers (4)

Franz Deschler
Franz Deschler

Reputation: 2574

W3Schools has a nice example of how to create a custom dropdown: https://www.w3schools.com/howto/howto_custom_select.asp

This is how the focus is handled in that example:

A global click-handler handles clicks outside of the dropdown list: document.addEventListener("click", closeAllSelect);. So as soon as the user clicks anywhere in the document, all dropdowns are closed.

But when the user selects an element of the dropdown list, the click-event is stopped by e.stopPropagation(); inside of the selection-handler.

This way, you don´t need the timer workaround.

Upvotes: 0

Dmitriy
Dmitriy

Reputation: 1972

Accepted answer is a naive and unreliable approach. I had a hard-to-catch bug in a complex application because I used a setTimeout to give ~200ms delay so the browser can process dropdown click before blur event happens. While it worked great on every setup I tested it with, some users did have issues, in particular users with slower machines.

The correct way is to test relatedTarget on focusout event:

input.addEventListener('focusout', function(event) {
  if(!isDropdownElement(event.relatedTarget)) {
    // hide dropdown
  }
});

relatedTarget for focusout contains an element reference, which is receiving focus. This reliably works in every browser I've tested so far (I didn't test IE10 and lower, only IE11 and Edge).

Upvotes: 2

Oriol
Oriol

Reputation: 288010

You could make the dropdown focusable with tabindex, and in the input's blur event listener only hide the dropdown if the focus didn't go to the dropdown (see When onblur occurs, how can I find out which element focus went to?)

<ul class="content" tabindex="-1"></ul>
input.addEventListener('blur', function(e) {
  if(!e.relatedTarget || !e.relatedTarget.classList.contains('content')) {
    undropdown(0);
  }
});

function input(e) {
  var dropdown = document.getElementsByClassName('drop');
  var li = dropdown[0].getElementsByTagName("li");
  document.getElementsByTagName('input')[0].value = e.target.textContent;
  undropdown(0);
}
[].forEach.call(document.getElementsByTagName('li'), function(el) {
  el.addEventListener('click', input);
});
function dropdown(pos) {
  document.getElementsByClassName('content')[pos].style.display = "block"
}
function undropdown(pos) {
  document.getElementsByClassName('content')[pos].style.display = "none";
}
var input = document.getElementsByTagName('input')[0];
input.addEventListener('focus', function(e) {
  dropdown(0);
});
input.addEventListener('blur', function(e) {
  if(!e.relatedTarget || !e.relatedTarget.classList.contains('content')) {
    undropdown(0);
  }
});
.drop {
  position: relative;
  display: inline-block;
  vertical-align: top;
  overflow: visible;
}
.content {
  display: none;
  list-style-type: none;
  border: 1px solid #000;
  padding: 0;
  margin: 0;
  position: absolute;
  z-index: 2;
  width: 100%;
  max-height: 190px;
  overflow-y: scroll;
  outline: none;
}
.content li {
  padding: 12px 16px;
  display: block;
  margin: 0;
}
<div class="drop">
  <input type="text" name="class" placeholder="Class" />
  <ul class="content" tabindex="-1">
    <li>Option 1</li>
    <li>Option 2</li>
    <li>Option 3</li>
    <li>Option 4</li>
  </ul>
</div>

Upvotes: 6

Nikhil Nanjappa
Nikhil Nanjappa

Reputation: 6632

In this case, On onblur you could call a function which fires the undropdown(0); after a very tiny setTimeout almost instantly. Like so:

function set() {
  setTimeout(function(){ 
    undropdown(0);
  }, 100);
}

HTML

<input type="text" name="class" placeholder="Class" onfocus="dropdown(0)" onblur="set()" />

No other change is required.

function input(pos) {
  var dropdown = document.getElementsByClassName('drop');
  var li = dropdown[0].getElementsByTagName("li");

  document.getElementsByTagName('input')[0].value = li[pos].innerHTML;
  undropdown(0);
}

function dropdown(pos) {
  document.getElementsByClassName('content')[pos].style.display= "block"
}

function undropdown(pos) {
  document.getElementsByClassName('content')[pos].style.display= "none";
}

function set() {
  setTimeout(function(){ 
    undropdown(0);
  }, 100);
}
        .drop {
            position: relative;
            display: inline-block;
            vertical-align:top;
            overflow: visible;
        }

        .content {
            display: none;
            list-style-type: none;
            border: 1px solid #000; 
            padding: 0;
            margin: 0;
            position: absolute;
            z-index: 2;
            width: 100%;
            max-height: 190px;
            overflow-y: scroll;
        }

        .content li {
            padding: 12px 16px;
            display: block;
            margin: 0;
        }
   <div class="drop">
        <input type="text" name="class" placeholder="Class" onfocus="dropdown(0)" onblur="set()" />
        <ul class="content">
            <li onclick="input(0)">Option 1</li>
            <li onclick="input(1)">Option 2</li>
            <li onclick="input(2)">Option 3</li>
            <li onclick="input(3)">Option 4</li>
        </ul>
    </div>

Upvotes: 6

Related Questions