192

I have an input field that brings up a custom drop-down menu. I would like the following functionality:

  • When the user clicks anywhere outside the input field, the menu should be removed.
  • If, more specifically, the user clicks on a div inside the menu, the menu should be removed, and special processing should occur based on which div was clicked.

Here is my implementation:

The input field has an onblur() event which deletes the menu (by setting its parent's innerHTML to an empty string) whenever the user clicks outside the input field. The divs inside the menu also have onclick() events which execute the special processing.

The problem is that the onclick() events never fire when the menu is clicked, because the input field's onblur() fires first and deletes the menu, including the onclick()s!

I solved the problem by splitting the menu divs' onclick() into onmousedown() and onmouseup() events and setting a global flag on mouse down which is cleared on mouse up, similar to what was suggested in this answer. Because onmousedown() fires before onblur(), the flag will be set in onblur() if one of the menu divs was clicked, but not if somewhere else on the screen was. If the menu was clicked, I immediately return from onblur() without deleting the menu, then wait for the onclick() to fire, at which point I can safely delete the menu.

Is there a more elegant solution?

The code looks something like this:

<div class="menu" onmousedown="setFlag()" onmouseup="doProcessing()">...</div>
<input id="input" onblur="removeMenu()" ... />
var mouseflag;
function setFlag() {
 mouseflag = true;
}
function removeMenu() {
 if (!mouseflag) {
 document.getElementById('menu').innerHTML = '';
 }
}
function doProcessing(id, name) {
 mouseflag = false;
 ...
}
asked Jul 21, 2013 at 4:20
0

9 Answers 9

249

I was having the exact same issue as you, my UI is designed exactly as you describe. I solved the problem by simply replacing the onClick for the menu items with an onMouseDown. I did nothing else; no onMouseUp, no flags. This resolved the problem by letting the browser automatically re-order based on the priority of these event handlers, without any additional work from me.

Is there any reason why this wouldn't have also worked for you?

Aliaksandr Sushkevich
12.6k8 gold badges41 silver badges46 bronze badges
answered Mar 10, 2015 at 12:36
Sign up to request clarification or add additional context in comments.

9 Comments

This actually works. I have tested it in FF and it works like charm.
It works. It looks like onmousedown is executed before onblur Tested in Firefox, Chrome and Internet Explorer.
Worth noting this does change the behaviour a bit naturally - the click interaction is then handled on mouse down rather than mouse up. For most people that may be fine (self included in this case) but there are a few a drawbacks. Most notably I often click then drag off the button if I've misclicked, which prevents onclick being called - if the button performs a non-Pure function (delete, post etc) you might want to preserve this and go with the flag approach.
What about accessibility at the moment you set mouseDown instead of onClick you kill accessibility for all <button> elements :/
The answer listed below by @ian-macfarlane is the correct way of dealing with this issue. In Summary... onMouseDown with event.preventDefault() and onClick with your desired action.
|
194

onClick should not be replaced with onMouseDown.

While this approach somewhat works, the two are fundamentally different events that have different expectations in the eyes of the user. Using onMouseDown instead of onClick will ruin the predictability of your software in this case. Thus, the two events are noninterchangeable.

To illustrate: when accidentally clicking on a button, users expect to be able to hold down the mouse click, drag the cursor outside of the element, and release the mouse button, ultimately resulting in no action. onClick does this. onMouseDown doesn't allow the user to hold the mouse down, and instead will immediately trigger an action, without any recourse for the user. onClick is the standard by which we expect to trigger actions on a computer.

In this situation, call event.preventDefault() on the onMouseDown event. onMouseDown will cause a blur event by default, and will not do so when preventDefault is called. Then, onClick will have a chance to be called. A blur event will still happen, only after onClick.

After all, the onClick event is a combination of onMouseDown and onMouseUp, if and only if they both occur within the same element.

answered Aug 23, 2019 at 16:41

11 Comments

This was the perfect solution for me and comment is totally correct - replacing onMouseDown is confusing for user experience. Have up voted.
this should be the correct answer. thank you for posting this
It's worth noting that this does have some minor side effects as well, e.g. not being able to select text by clicking within the element. Can't think of too many cases where this would be an issue, just that it feels a little funny.
If I use event.preventDefault() on onMouseDown event, my onFocus event is not called
The focus location is important in many applications. The marked solution does not allow the focus to be restored to the initial input box by the callback. The onBlur will happen and remove the focus. This implementation with event.preventDefault() does allow the onClick handler to set the focus to the text box.
|
2

Replace on onmousedown with onfocus. So this event will be triggered when the focus is inside the textbox.

Replace on onmouseup with onblur. The moment you take out your focus out of textbox, onblur will execute.

I guess this is what you might need.

UPDATE:

when you execute your function onfocus-->remove the classes that you will apply in onblur and add the classes that you want to be executed onfocus

and

when you execute your function onblur-->remove the classes that you will apply in onfocus and add the classes that you want to be executed onblur

I don't see any need of flag variables.

UPDATE 2:

You can use the events onmouseout and onmouseover

onmouseover-Detects when the cursor is over it.

onmouseout-Detects when the cursor leaves.

Thibaut
6276 silver badges7 bronze badges
answered Jul 21, 2013 at 5:31

5 Comments

I've edited my question for clarity. How does this solution avoid the need for setting a flag? Also, I use onmousedown() and onmouseup() on the menu, not the textbox / input field.
I can't accept this answer yet because I don't know that it's correct. Could you respond to the concerns I raised in my comment above?
Sure, I can see how you could use onmouseover and onmouseout similarly to what I'm currently doing, to set a flag when the mouse is over the menu. However, I still think you would need to set a flag in onmouseover which is cleared in onmouseout, so that you would know whether you were over the menu when you clicked. Can you think of a way that does not require setting a flag?
Can i see you code...put it in a fiddle...just put the relevant part and its completely ok if i dont see the results..just the code..
2

onFocus / onBlur are events that don't bubble. There are however focus events that do bubble. These being focusin and focusout.

Now to the solution: We wrap both the input and our dropdown into a div-element and set the tabindex of that div to -1 (so that it can recieve focus / but does not appear in the tab order). We now add an eventlistener for focusin and focusout to this div. And since these events do bubble a click on our input element will trigger our divs focusin event (which opens the drop-down)

The neat part now is that a click on our dropdown will also trigger the focusin event on our div (so we basically maintain focus which means: focusout/blur never fires and our dropdown stays open)

You can try this out with the code snippit below (the dropdown only closes on loss of focus - but if you want it to close when clicking on the dropdown aswell just uncomment the one line of JS)

const container = document.getElementById("container")
const dropDown = document.getElementById("drop-down")
container.addEventListener("focusin", (event) => {
 dropDown.classList.toggle("hidden", false)
})
container.addEventListener("focusout", (event) => {
 dropDown.classList.toggle("hidden", true)
})
dropDown.addEventListener("click", (event) => {
 console.log("I - the drop down - have been clicked");
 //dropDown.classList.toggle("hidden", true);
});
.container {
 width: fit-content;
}
.drop-down {
 width: 100%;
 height: 200px;
 border: solid 1px black
}
.hidden {
 display: none
}
<div class="container" id="container" tabindex="-1">
 <input id="input" />
 <div class="drop-down hidden" id="drop-down" > Hi I'm a drop down </div>
</div>

there arises however one issue if you want to add your dropdown into the tabbing order, have buttons in your dropdown or in general have an element in the dropdown, that can recieve focus. Because then a click will give the element in the dropdown focus first. This triggers our container div to lose focus which closes the dropdown so the focus event can't bubble further and therefore can't trigger the focusin on our container.

We can solve this issue by expanding the focusout eventlistener a bit.

The new eventlistener is as follows:

container.addEventListener("focusout", (event) => {
 dropDown.classList.toggle("hidden", !container.matches(":hover"))
})

We basically say: "don't you close that dropDown if someone is hovering over it" (This solution only considers mouse-use; but in that case this is fine, because the problem this tries to fix only ever occured when using a mouse, when tabbing onto/through the dropDown everything worked fine from the start)

answered Oct 1, 2022 at 22:15

Comments

1

onMouseDown disable onblur event when item from dropdown is clicked

in my case i needed to disable onblur event when suggetion list item was getting clicked

<li
onMouseDown={(e) => e.preventDefault()}
onClick={() => onPressDropdownOption(productId)}>{productName}</li>
answered Jan 17, 2024 at 11:20

Comments

1

When onblur runs, it will hide the div and makes it unclickable. So onblur, give it some time and then hide the div:

element.onblur = (event) => {
 setTimeout(() => {
 event.target.style.display = 'none';
 }, 250); 
};
answered Mar 14, 2024 at 10:24

Comments

0

change onclick by onfocus

even if the onblur and onclick do not get along very well, but obviously onfocus and yes onblur. since even after the menu is closed the onfocus is still valid for the element clicked inside.

I did and it worked.

answered Jan 9, 2018 at 16:38

Comments

-1

An ideal solution I found to work for me was to simply add a timeout in my onBlur function. I used 250ms, that provided smooth behaviour for my blur event and allowed my onClick to fire before the onBlur. I used this example as a reference https://erikmartinjordan.com/onblur-prevents-onclick-react

answered Oct 9, 2022 at 19:48

2 Comments

having your code/app rely on timing to function properly is quite bad. Especially in larger projects maintaining code that relies on timing is really difficult, if not impossible. If you start to "fix" a problem by setting timeouts (therefore making your code reliant on the right timing) you will certainly do it again. The more you do it and the larger the codebase the more probable it gets, that those timeouts will interfere with each other, and if that happens: good luck debugging
Yes you are right, I don’t really prefer my code rely on timing too because it won’t always yield consistent results. I already implemented a more efficient solution to fit my need, timing was just a quick solution that I stumbled upon
-10

You can use a setInterval function inside your onBlur handler, like this:

<input id="input" onblur="removeMenu()" ... />
function removeMenu() {
 setInterval(function(){
 if (!mouseflag) {
 document.getElementById('menu').innerHTML = '';
 }
 }, 0);
}

the setInterval function will remove your onBlur function out from the call stack, add because you set time to 0, this function will be called immediately after other event handler finished

t3dodson
4,0273 gold badges31 silver badges44 bronze badges
answered Nov 12, 2015 at 3:56

4 Comments

setInterval is a bad idea, you are never cancelling it so it will continually run your function and most likely cause your site to use max cpu. Use setTimeout instead if you are going to use this technique (which I don't recommend). Making your app depend on the timing of certain events is risky business.
DANGER WILL ROBINSON! DANGER! Race condition ahead!
I originally came up with this solution (well setTimeout not setInterval) but I had to bump it up to 250ms before the timing happen in the correct order. This bug will probably still exist on slower devices and also it makes the delay noticeable. I tried the onMouseDown and it works fine. I wonder if it needs the touch events as well.
Something like an interval is not bad if you really want to use onClick. But you'd want a recursive timeout with a condition rather than an interval so it doesn't run forever. This is the same pattern used by Selenium conditional waits.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.