Once again, it took me several hours to figure out how to implement a seemingly simple feature with electrum, React and electron (or for the matter, Chrome).
Here is my scenario:
- The user presses the mouse button on a
<div>
element. - As a result, I want to set the focus on an
<input>
element. - I want to capture all mouse events from that point in time until the users releases the mouse button as I don’t want any other element seeing them.
- I want to be able to press a second time on the
<div>
and have everything work exactly as the first time. - I don’t want the browser to display hover effects on items
such as buttons or
<a>
links while the mouse button is down, even if the mouse gets over an otherwise live element.
Setting the capture
I was expecting to find an API to capture mouse events, like
SetCapture()
I had been using on Win32. And indeed, MDN had an explanation
of Element.setCapture()
which looked exactly like what I was
looking for.
The first surprise was the lack of such a setCapture()
function
in Chrome.
I digged around, found a lot of old and outdated information which
stated that Chrome had no setCapture()
. I guessed that this would
have been addressed since 2009. But no, see Stack Overflow
and a workaround here.
See also Example 19-2 in JavaScript, The Definitive Guide, 4th ed..
First attempt
I tried the workaround in my component:
render() {
return <div onMouseDown={e => this.handleMouseDown (e)}>Hi</div>;
}
handleMouseDown(e) {
inputElement.focus ();
captureMouseEvents ();
e.preventDefault ();
e.stopPropagation ();
}
and wrote a captureMouseEvents()
function which would add an
event listener on document
for both mouseup
and mousemove
,
while removing them when the mouseup
event is received:
function mousemoveListener (e) {
e.preventDefault ();
}
function mouseupListener (e) {
document.removeEventListener ('mouseup', mouseupListener, true);
document.removeEventListener ('mousemove', mousemoveListener, true);
e.preventDefault ();
}
function captureMouseEvents () {
document.addEventListener ('mouseup', mouseupListener, true);
document.addEventListener ('mousemove', mousemoveListener, true);
}
It should work, shouldn’t it?
My component did indeed call captureMouseEvents()
, it would get the
mouse movements and the final mouse up. It would properly unregister
its event listeners from the document.
However clicking a second time on the component would not call
handleMouseDown
. It just did not react any more!
React events and focus
See that line:
inputElement.focus ();
It was responsible for this misbehaviour. Commenting it out would fix the issue.
As soon as I set the focus on another element while capturing the events,
React gets confused. It would no longer route the mousedown
events to
the correct component, until I changed the focus by clicking on another
item.
So I decided to get rid of onMouseDown
and React synthetic events,
and manually add an event listener on my component’s DOM node. This
is easily done in the component’s componentDidMount()
life-cycle
method:
componentDidMount () {
const dom = ReactDOM.findDOMNode (this);
dom.addEventListener ('mousedown', e => this.handleMouseDown (e));
}
With that in place, I got a working implementation for points 1-4.
But wait, I don’t want items reacting to hover!
What I did not realize, however, was that even with this mouse event capture in place, the browser would continue to apply the hover effect on items below the mouse pointer, and change the shape of the cursor based on the underlying component.
Try to select some text in an electron-based editor such as atom or Visual Studio Code and you will see that they suffer from the same strange behavior. While selecting, you’ll get the double ended arrow as soon as you cross a splitter, or a hand when you get over an icon.
Huh? This does not make sense: I am selecting text and the visual feed-back I get is plain wrong.
In atom I even see a pop-up appear while I am selecting text if I move the cursor to the update indicator in the lower right corner of the window.
Radical measures
There is a little known CSS style which can be applied on any
element to disable all pointer events. Setting the pointer-events
style to none
on the body of the DOM completely disables
all pointer events. No hovers. No cursors. No nothing.
body {
pointer-events: none;
}
If I set this style dynamically when the user presses the mouse button, I completely disable the unwanted reactions to hovering elements. Fortunately enough, the component which has captured the mouse will still get the events, which allows me to restore a normal behaviour as soon as the user releases the button.
Here is the final piece of code:
const EventListenerMode = {capture: true};
function preventGlobalMouseEvents () {
document.body.style['pointer-events'] = 'none';
}
function restoreGlobalMouseEvents () {
document.body.style['pointer-events'] = 'auto';
}
function mousemoveListener (e) {
e.stopPropagation ();
// do whatever is needed while the user is moving the cursor around
}
function mouseupListener (e) {
restoreGlobalMouseEvents ();
document.removeEventListener ('mouseup', mouseupListener, EventListenerMode);
document.removeEventListener ('mousemove', mousemoveListener, EventListenerMode);
e.stopPropagation ();
}
function captureMouseEvents (e) {
preventGlobalMouseEvents ();
document.addEventListener ('mouseup', mouseupListener, EventListenerMode);
document.addEventListener ('mousemove', mousemoveListener, EventListenerMode);
e.preventDefault ();
e.stopPropagation ();
}