175

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;
    ...
}
Community
  • 1
  • 1
1''
  • 26,823
  • 32
  • 143
  • 200

7 Answers7

236

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
  • 11,550
  • 7
  • 37
  • 44
johnbakers
  • 24,158
  • 24
  • 130
  • 258
  • Sorry, I haven't done web design in a while so I've forgotten the intricacies of the issue. Could someone else comment on whether this works? – 1'' Mar 11 '15 at 00:34
  • 4
    This actually works. I have tested it in FF and it works like charm. – Supreme Dolphin Feb 15 '16 at 11:49
  • 7
    It works. It looks like `onmousedown` is executed before `onblur` Tested in Firefox, Chrome and Internet Explorer. – Tonatio Aug 16 '16 at 10:48
  • 26
    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. – Brizee Nov 19 '17 at 14:03
  • 4
    What about accessibility at the moment you set mouseDown instead of onClick you kill accessibility for all – ncubica Aug 18 '18 at 01:31
  • 5
    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. – Conal Da Costa May 14 '20 at 12:51
  • But how does it work with the left mouse only? I'm using reactjs – Nam Lee Nov 24 '20 at 02:49
  • @NamLêQuý this should work with the left button, are you saying you want to *prevent* it from working if the right button is pressed? – johnbakers Nov 24 '20 at 08:07
  • Is there any source on the priority of event? – Minh Nghĩa Feb 16 '23 at 07:57
162

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.

An example here

ofir_aghai
  • 3,017
  • 1
  • 37
  • 43
Ian MacFarlane
  • 2,300
  • 1
  • 16
  • 9
  • 18
    This was the perfect solution for me and comment is totally correct - replacing onMouseDown is confusing for user experience. Have up voted. – Paul Bartlett Dec 19 '19 at 03:00
  • 9
    this should be the correct answer. thank you for posting this – user1189352 Jan 31 '20 at 18:26
  • 2
    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. – Rei Miyasaka May 06 '20 at 05:21
  • 8
    If I use `event.preventDefault()` on `onMouseDown` event, my `onFocus` event is not called – Batman Jun 22 '20 at 20:51
  • But my mouse pointer is still in the input after I hit the submit button. This makes it validate the input again when I click elsewhere. – Nam Lee Nov 24 '20 at 02:55
  • 1
    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. – Michel Dec 30 '20 at 19:46
  • With `preventDefault`, not able to select the text. I was looking for this solution to fix outside click issue when user wanted to actually select the text but ends up releasing the mouse outside the boundary. In fact, `focusout` event helped than doing `blur` and `click`. – Ritesh Jagga Jul 10 '21 at 16:18
  • Perfect solution for me, instead of listening to every click just to check if it was outside of menu and close it, I just add to button onBlur event that closes menu and on whole menu onMouseDown with e.preventDefault so onBlur event won't fire. For my case works perfectly. – Dawid Frankiewicz Feb 21 '23 at 20:30
  • after read your answer deeply. i agree with you. thank you for this. put an example for being clear. https://jsfiddle.net/iofirag/7L6do5be/ – ofir_aghai Apr 17 '23 at 14:31
1

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
  • 607
  • 6
  • 7
HIRA THAKUR
  • 17,189
  • 14
  • 56
  • 87
  • 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. – 1'' Jul 21 '13 at 16:04
  • 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? – 1'' Jul 26 '13 at 14:06
  • 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? – 1'' Jul 26 '13 at 14:36
  • 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.. – HIRA THAKUR Jul 26 '13 at 14:41
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/34255/discussion-between-messiah-and-1) – HIRA THAKUR Jul 26 '13 at 18:45
1

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)

Lord-JulianXLII
  • 1,031
  • 1
  • 6
  • 16
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.

Brasil
  • 1
  • 1
0

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

  • 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 – Lord-JulianXLII Oct 27 '22 at 17:55
  • 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 – James Mtendamema Oct 28 '22 at 18:18
-9

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
  • 3,949
  • 2
  • 29
  • 40
  • 7
    `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. – casey Mar 28 '16 at 23:59
  • 8
    DANGER WILL ROBINSON! DANGER! Race condition ahead! – GoreDefex Jan 30 '18 at 13:17
  • I originally came up with this solution (well `setTimeout` not `setInterval`) but I had to bump it up to `250`ms 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. – Brennan Cheung Aug 10 '18 at 00:38
  • 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. – Will Apr 25 '19 at 14:37