Can YOU predict `event.target` for click events in each browser?
A while ago I fixed a frustrating fun bug involving browser-specific behavior. The behavior differences occurred with a combination of click events, input elements, and event.target
.
I thought the root cause was pretty interesting, so here's a short blog post about it!
What was the bug?
To start off, imagine an app that looks something like this screenshot. The relevant app behavior is:
- "Create post" is a modal overlay on top of the page.
- Clicking outside of the modal will close it.
- The "Content" textarea will stay vertically expanded while it has focus (a UI decision we made).
Seems simple enough, right? But there was a bug—in some cases, clicking the input labelled "Tags" would cause the modal to close.
Reproduction steps:
- Use Chrome or Edge (Bug doesn't occur in Firefox or Safari!)
- Starting editing the textarea labelled "Content."
- Click the "Tags" input.
Expected result: The "Tags" input focuses:
Actual result: The "Tags" input very briefly focuses, then the modal disappears:
That's not what we want to happen! 😔
Why is this happening to me??
I started by finding the code that caused the modal to close. It looked something like this:
document.addEventListener("click", function onClick(event) {
if (isElementOutsideOfModal(event.target)) {
closeModal();
}
});
You can think of this code as saying "whenever the user clicks outside of the modal, close the modal."
ℹ️ What does event.target
refer to?
event.target
refers to the specific element that the user clicked on. In this case, it could be anything in the document, such as a div
or a button
.
Don’t confuse it with event.currentTarget
, which refers to the element that the event handler was attached to.
What did I actually click on?
It was odd to me that this code was closing the modal. I had clicked on the input element inside the modal, so why did the browser think I was clicking outside of the modal?
To investigate further, I logged event.target
to see on which element the browser thought the click event occurred.
It was #app
, a container for most of the page. I was surprised, because I thought event.target
would be the input element I had clicked on.
If you look closer at the repro steps, however, you may notice something. When I press the mouse button, it's on the input:
But when I release the mouse button, it's outside of the modal, since the "Content" textarea only stays expanded while focused.
Huh! Looks like we found our "click outside of the modal."
How is event.target determined for click events?
Does that observation mean that event.target
for click
events is determined by where the mouse button is released? Not quite:
If the button is pressed on one element and the pointer is moved outside the element before the button is released, the event is fired on the most specific ancestor element that contained both elements.
Ok, so event.target
is the common ancestor between the mouse "press" and "release" positions. That knowledge cleared up my understanding of why the modal got closed:
- Mouse press causes the "Tags" input to focus.
- Since focus moved off of the "Content" textarea, the textarea shrinks vertically, which shrinks the modal as well.
- Next, the mouse release happens beneath the modal, on
#app
, firing a click event. event.target
is set to#app
, the common ancestor between the "Tags" input and#app
. (In this case, the common ancestor happens to be the same as the element we released the mouse on, but that's not always true.)- Since
event.target
is#app
, which is outside of the modal, the modal gets closed.
That explanation solved most of the confusion for me, but I still had one more question...
Why did this bug only exist in Chrome/Edge?
I wanted to understand the specific browser differences that caused this bug to only exist in Chrome and Edge.
Exploration with a minimal working example
To get there, I started by making a demo app of the bug (try it out). With less code and a simpler app, it'd be easier to pinpoint the differences.
My demo app would show a red circle where I pressed the mouse, then another red circle where I released the mouse, then print event.target
. It looked like this:
In that case, event.target
was the left
div, which makes sense. I pressed and released the mouse button in that same div, so the "common ancestor" of left
and left
is itself.
I tried it again, but I dragged the mouse from left
to right
before releasing:
As we expected, event.target
was container
, the common ancestor of the left
div (where I pressed the mouse) and the right
div (where I released the mouse).
Chrome, Edge, Firefox, and Safari all had this same behavior. I was hoping to find a difference here, one that could explain the browser-specific bug I was fixing. Because I couldn't find a difference, it seemed like my "demo app" wasn't quite representative of the actual bug.
To make my demo app more accurate, I pulled in another detail from the actual bug I was trying to fix: Clicking on an input
element instead of a div
.
I added a left-input
input element inside the left
div.
Looks good so far.
Browser behavior differences
After that, I tried something closer to the original bug: Press the mouse on the left-input
, then release it outside of the left
container. Here is how it behaved in each browser:
Chrome
Edge
Nothing unexpected so far.
Firefox
Safari
Hmmmm.
It appears that event.target
, specifically for input elements, behaves differently in Chrome and Edge than in Firefox and Safari.
That behavior difference is the source of our Chrome/Edge-only bug.
In Firefox, if you press the mouse button on an input
element, event.target
is always that input
element. It doesn't matter where you release the mouse. Our "should modal close?" code then sees that event.target
is within the modal, so it doesn't close the modal.
Safari doesn't even fire a click event at all, so it won't run the "should modal close?" code.
But in Chrome and Edge, event.target
depends on where you release your mouse.
How can we fix it?
In this specific situation, we added a workaround. In the click handler, we'd check if an input was focused before closing the modal.
document.addEventListener("click", function onClick(event) {
if (!isElementOutsideOfModal(document.activeElement)) {
return;
}
if (isElementOutsideOfModal(event.target)) {
closeContainer();
}
});
It was a low-risk change we could ship quickly within legacy code, but I imagine there are plenty of other solutions we could explore.