Reputation:
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
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
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
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
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