Understanding event listeners can be challenging, particularly when working with third-party sites and Chrome extensions. The complexity of the issue is even more evident when considering how different types of events are fired and propagate. This post explores a specific challenge experienced while handling mousedown and pointerdown events, leading to some fascinating browser behavior discoveries.
In this particular scenario, a developer was trying to listen for a mousedown event on a document object in a Chrome extension. The event was set to use capturing (
useCapture: true), implying that it should fire immediately, regardless of where the user clicks on the document.
However, this was not the case. The mousedown event fired correctly for most elements on the page, but it did not fire when the user clicked on certain elements in a specific section of the page. These elements were part of a third-party site that the extension was working with.
The first hypotheses revolved around the firing order of the event listeners and the use of
stopPropagation(). However, it was clarified that
stopPropagation() does not affect event handlers when
useCapture is set to true. This is because
useCapture effectively reverses the firing order of the event handlers, meaning the event listener at the highest level (the document object in this case) is fired first.
Upon further investigation, it was discovered that switching the event from mousedown to pointerdown solved the problem. By using
getEventListeners(), it was observed that the document object had
pointerdown events defined also at the capture phase. This sparked the theory that if a
pointerdown event is fired, the
mousedown event might not be fired at all.
Upon recreating the situation, it was found that the third-party site called
preventDefault() in their
pointerDown event, which was also using
useCapture: true. This seemed to prevent the
mousedown event from firing.
While one might think that
stopImmediatePropagation() should have been the methods to block the event, it turned out that they didn't affect this scenario. That's because
preventDefault() cancels the event without necessarily stopping its propagation. In this case, both the
mousedown handlers were set at the capturing phase on the same element (document), and were not bubbling events.
Furthermore, it was discovered that the browser fires
mousedown. If the
pointerdown handler cancels the event with
mousedown event does not fire.
Interestingly, even when the
mousedown event did not fire because of the
preventDefault() in the
pointerdown event, the
click event still did fire. This was particularly interesting because the
click event is typically fired after the
mouseup event, which, in turn, is generally fired after the
mousedown and the effect of
Through this process, it was demonstrated how changing the type of the event listener from
Working with event listeners can be tricky, but with careful exploration and understanding of how different event types
transform the following question and its discovered solution from an IRC chat (delimited by backticks) into a concise blog post:
[09:51:30] <bttf> I need help with event listeners. I have a mousedown event attached to the document object, with useCapture: true. However, it _does not_ fire when I click on a particular set of elements on a page ... It's a third party site, and I'm working within a chrome extension. However, any other part of the page fires the mousedown event as expected. Since I'm using the capturing phase, it should be fired always and immediately, right? [09:51:30] <bttf> What could be blocking it? [10:07:57] <deepSleep> can you get your listener to fire first? [10:09:45] <stenno> stoppropagation earlier will prevent that [10:16:14] <bttf> stopPropogation does not affect event handlers that have useCapture set to true [10:16:36] <bttf> useCapture essentially reverses the firing order - the highest event listener is fired first [10:17:00] <bttf> it fires first for all other elements on teh page .... except for these few elements in the hero banner section [10:19:42] <bttf> ok - i figured it out. honestly it's fkin weird. i think there is some undocumented (or hard to find behavior) wrt chrome and mousedown vs pointerdown events [10:20:28] <deepSleep> how did you solve it bttf [10:20:38] <bttf> when i switch to pointerdown, errthang works. i noticed the document obj had pointerdown events defined also at capture phase (by using getEventListeners) .... it may be that if pointerdown event is fired that mousedown is not?? [10:21:22] <bttf> i would expect pointerdown and mousedown to be fired simultaneously [11:07:55] <bttf> deepSleep i recreated it ..... i think the third party site called preventDefault in their pointerDown event which was also using useCapture: true [11:07:56] eXtr3m0 (~eXtr3m0@user/eXtr3m0) joined the channel [11:08:33] <stenno> wouldn't that be stoppropagation and not preventdefault? [11:08:54] <bttf> neither stoppropagation nor stopimmediatepropogation blocked it [11:09:13] <stenno> preventdefault doesn't prevent propagation [11:09:23] <bttf> its techinically not propagating? [11:09:32] <bttf> preventDefault cancels the event however [11:09:49] <bttf> these two event handlers are not in the bubbling phase - they are capturing, the same element (document) [11:10:43] <bttf> so the browser fires pointerdown before mousedown apparently, and since the pointerdown handler cancels the event, mousedown does not fire. i have a brand new app made to repro this [11:10:52] <deepSleep> bttf, cool cool [11:11:24] <deepSleep> interesting behavior [11:13:41] <bttf> omg it gets more interesting lol [11:13:56] <bttf> so mousedown does not fire ..... but click DOES lol [11:14:04] <bttf> i hate this [11:14:18] <stenno> click is after mouseup, right? [11:14:23] <stenno> hmm [11:15:04] <bttf> yea i think its mouse down --> mouse up --> click