Pure Javascript mouseenter and mouseleave event delegation with React issues - javascript

I would like to achieve something like this on my React project (https://creativesfeed.com/code/custom-cursors/index2.html)
I want to have a custom mouse cursor (a dot in this case which is in a "div") which follows the coordinates the mouse and that's fine, I can make it work.
The other thing that I want to achieve is that when hovering on ANY tag, a custom class will be added to the custom mouse cursor and removed when the custom cursor leave the link. The problem is that whenever I hover on the tag, the class appear and disappear immediately. I know that eventListener in Javascript are different from jQuery and I even tried with jQuery (which I hate to have in React) and I have the same issue. So what am I doing wrong? Could it be because I run the function on componentDidMount() ?
import React, { Component } from 'react';
import Homepage from './components/Homepage';
class App extends Component {
componentDidMount() {
let mouse__follower = document.getElementById('mouse__follower');
let links = document.getElementsByTagName('a');
document.addEventListener("mousemove", (e) => {
var x = e.clientX;
var y = e.clientY;
mouse__follower.style.top = `${y}px`;
mouse__follower.style.left = `${x}px`;
});
let addClassToLink = (e) => {
mouse__follower.classList.add('active');
};
let removeClassToLink = (e) => {
mouse__follower.classList.remove('active');
};
// ALSO, IS THIS VERY BAD IN TERMS OF MEMORY CONSUMING?
for(var i = 0, len = links.length; i < len; i++) {
links[i].addEventListener('mouseenter', addClassToLink, false);
links[i].addEventListener('mouseleave', removeClassToLink, false);
};
}
render() {
return (
<div className="nu-creative">
<div id="mouse__follower">
<div id="mouse__follower__circle"></div>
</div>
<Homepage />
</div>
);
}
}
export default App;
The link is in another file, but I think that's not the issue.
EDIT: I have found the issue. If I remove
mouse__follower.style.top = `${y}px`;
mouse__follower.style.left = `${x}px`;
from the document.addEventListener("mousemove", ...) the class is added and removed correctly, so there's must something there that I can do. Maybe is because two eventListener are triggered together.
I will investigate, thanks for the answers so far :)

Looks like the react life cycle is messing around with you. The thing is that react re-renders the html whenever it decides it should be done, and in your render method you are removing the inline classes.
So, the way to do this react-wise would be to set a flag in the state of your component which you change with setState, and check for this flag in your render method to decide wether or not add the class to the cursor.
Something like this:
<div id="mouse__follower" className={this.state.cursorActive? "active":""}>
Edit:
btw, this is not related but, remember to unbind all your event listeners that were bound in componentDidMount inside the componentWillUnmount.

ISSUES SOLVED :)
The z-index of the div containing the "dot" wasn't set and this was causing the event to trigger too many times. By setting the "z-index" to -1 the issues was solved.

Related

React onMouseEnter and onMouseLeave bug, not working consistently

I am having issues with React onMouseEnter and onMouseLeave, sometimes the events are not firing when it should
I am using onMouseEnter and onMouseLeave to show/hide a React Portal Modal tooltip panel:
the issue:
if the mouse cursor hovers over the images slowly, onMouseEnter and onMouseLeave works as expected, but if the mouse cursor hovers over the image rapidly ( horizontally ), the code breaks, and multiple tooltip panels are shown ( because onMouseLeave failed to trigger )
here is the video link showing the issue:
https://www.veed.io/view/7669d6c4-6b18-4251-b3be-a3fde8a53d54?sharingWidget=true
here is the codesandbox link for bug I mentioned:
https://codesandbox.io/s/epic-satoshi-rjk38e
any help is appreciated
The cause for the "lost events" is not the event listeners not firing. The problem is that you mix React code, which operates on the virtual DOM, with "classical" JS code, which operates on the browser DOM. These two DOMs are out-of-sync most of the time, which causes the hiccups that you see.
Try to remove all code that directly accesses the DOM and operate on React components (i.e. in the virtual DOM) only.
Here is an stripped-down example how to implement your tooltip in React-only style:
https://codesandbox.io/s/musing-villani-c0xv24?file=/src/App.js
As Steffen Frank correctly pointed out, using DOM events is often risky...
I used another strategy using the React onMouseMove on the main container, in index.js
const [hoveredClasses, setHoveredClasses] = useState([]);
const handleMouseMove = (e) => {
var x = e.clientX;
var y = e.clientY;
let elementMouseIsOver =
document.elementFromPoint(x, y)?.closest(".c-justified-box") ||
document.elementFromPoint(x, y)?.closest(".tooltip");
let classes = elementMouseIsOver?.classList
? Array.from(elementMouseIsOver?.classList)
: [];
console.log(
"[ c-justified-box ] > Mouse Move ----------------------------->",
x,
y,
elementMouseIsOver,
classes
);
// check if we need to cal setState
if (
(classes.length === 0 && hoveredClasses.length !== 0) ||
!classes.every((classItem) => hoveredClasses.includes(classItem))
) {
setHoveredClasses(classes);
}
};
And
<main className={styles.main} onMouseMove={handleMouseMove}>
So on mouse move:
we get the element under the mouse
we check if that element classes are exactly the same as the classes we previously stored in state.
If no, we set the state with those classes. That triggers a redraw for the whole main component, so each of the JustifiedBox are also redrawn.
We are passing them the hoveredClasses array... so within each of them, a boolean is set to decide to show the modal or not.
const showFloatingDetailPanel = props.hoveredClasses.includes(
props.new_Album.slug
);
Has you can see... We use the props.new_Album.slug which was already used as a div class for c-justified-box
className={"c-justified-box" + " " + props.new_Album.slug}
We just need to also pass it to the AlbumDetailsPanel as a props, to do the same with the modal main div.
JustifiedBox.js:
<AlbumDetailsPanel
slug={props.new_Album.slug}
t_ref={floating}
//...
AlbumDetailPanel.js:
<div
className={"tooltip" + " " + props.slug}
So every times the hoveredClasses do not match, we setState... And that state will be used by each component to decide to show their associated AlbumDetailsPanel. It is now impossible to have more than on shown.
And this simplifies everything in JustifiedBox.js, since we got rid of:
handleAlbumMouseEnter
handleAlbumMouseOut
handleTooltipMouseEnter
handleTooltipMouseOut
the 3 useEffect
The updated CodeSandbox
Great question I had a similar problem with one of my components and global state management.
After looking at your code it looks you should add the onFocus and onBlur events.
Example
<div
onMouseOver={() => handleEnter()}
onMouseOut={() => handleExit()}
>
Becomes
<div
onMouseOver={() => handleEnter()}
onFocus={() => handleEnter()}
onMouseOut={() => handleExit()}
onBlur={() => handleExit()}
>
This also solves a problem of mobile not having the same mouse state.
Hope this helps happy coding!

React onMouseEnter and onMouseLeave not behaving consistently

I have a div that is simply supposed to display 'HOVERING' if the cursor is hovering over it, and 'NOT HOVERING' otherwise. For some reason, it behaves as expected if I slowly hover each div on the page; however, if I quickly move my cursor across the screen, some of the divs become switched. Meaning, they will display "NOT HOVERING" when my cursor moves over the div, and "HOVERING" when my cursor is not over the div.
This error occurs in both Chrome and Safari.
Sandbox:
https://codesandbox.io/s/aged-butterfly-r2g6x?file=/src/Geo.js
Move your cursor quickly over the boxes to see the issue.
Issue
I think the main issue with your implementation is with the way asynchronous event callbacks are queued up and processed in the event loop. I can't find any hard details about the latency of processing event callbacks but the docs here and here may shed some more light on the matter if you care to do a deep dive.
Basically the issue is two-fold:
There is a minute duration a single event loop takes to process, i.e. detect an event and add it to the queue. I suspect the mouse is moving fast enough off/out the screen or into another div it isn't detected. The divs "jumping"/"moving" when hovering also doesn't help much.
The component logic assumes all events can and will be detected and simply toggled the previous existing state. As soon as an event is missed though the toggling is inverted, thus the issue you see. Even in the updated sandbox this latency can cause one of the elements to get "stuck" hovered
Proposed Solution
Add a mouse move event listener to the window object and check if the mouse move event target is contained by one of your elements. If not currently hovered and element contains event target, set isHovered true, and if currently hovered and the element does not contain event target, set isHovered false.
This isn't a full replacement for the enter/leave|over/out event listeners attached to the containing div as I was still able to reproduce an edge-case. I noticed your UI is most susceptible to this issue when moving the mouse quickly and leaving the window.
Combining the window and div event listeners gives a pretty good resolution (though I was still able to reproduce edge-case it is much more difficult to do). What also seems to have helped a bit is not defining anonymous callback functions for the div.
import React, { createRef } from "react";
export default class Geo extends React.Component {
state = {
isHovering: false
};
mouseMoveRef = createRef();
componentDidMount() {
window.addEventListener("mousemove", this.checkHover, true);
}
componentWillUnmount() {
window.removeEventListener("mousemove", this.checkHover, true);
}
setHover = () => this.setState({ isHovering: true });
setUnhover = () => this.setState({ isHovering: false });
checkHover = e => {
if (this.mouseMoveRef.current) {
const { isHovering } = this.state;
const mouseOver = this.mouseMoveRef.current.contains(e.target);
if (!isHovering && mouseOver) {
this.setHover();
}
if (isHovering && !mouseOver) {
this.setUnhover();
}
}
};
render() {
var textDisplay;
if (this.state.isHovering) {
textDisplay = <span>HOVERING</span>;
} else {
textDisplay = <h1>NOT HOVERING</h1>;
}
return (
<div
ref={this.mouseMoveRef}
onMouseEnter={this.setHover}
onMouseLeave={this.setUnhover}
style={{ width: 300, height: 100, background: "green" }}
>
{textDisplay}
</div>
);
}
}
As far as I can see, you have a problem with the way you update the state. Bear in mind that React may update the state asynchronously.
Changing toggleHoverState function will solve the issue
toggleHoverState() {
this.setState(state => ({isHovering: !state.isHovering}));
}
Go to this section in React docs for more info

REACT: How could we detect user click on canvas?

Hello and thank you for reading this question.
I had an use case in which I needed to detect mouse coordinates on canvas, and thank you to #Carlos Martinez : getting mouse coordinates in React it works as expected.
I tryed to go further and detect if user clicks on canvas and then put on a h2 it, using state, and log it; and here is the code I have tryed:
import React from 'react';
class Canvas extends React.Component {
//A canvas to display images with a title
constructor(props) {
super(props);
this.state = {x: 0, y: 0, inside: ''};
this.handleClick = this.handleClick.bind(this);
}
_onMouseMove(e) {
this.setState({x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY});
}
componentDidMount() {
document.addEventListener('click', this.handleClick);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleClick);
}
handleClick(e) {
console.log('INSIDE');
this.setState({inside: 'inside'});
}
render() {
const {x, y, inside} = this.state;
return (
<div className="previewComponent">
<div className="imgPreview">
{this.props.title}
<img src={this.props.image} alt="" onMouseMove={this._onMouseMove.bind(this)}
onClick={this.handleClick.bind(this)}/>
<h1>Mouse coordinates: {x} {y}</h1>
<h2>Inside?: {inside}</h2>
</div>
</div>
)
}
}
export {Canvas};
I expected that it would log it just once, and also put it on h2 tag.
However, unexpectedly it logs INSIDE, and then in addition logs two INSIDE:
INSIDE
2 Canvas.js:29 INSIDE
Could you explain me this behaviour? Also, it would be appreciated some tips to fix it and just print one log per click!
EDIT:
I have tried #Or B answer and I understood it however it looks like it stills showing the same behaviour, 3 logs of INSIDE instead of one:
The code is:
handleClick(e) {
e.stopPropagation();
console.log('INSIDE');
this.setState({inside: 'inside'});
}
This happens due to event propagation and the fact that you're listening to click events on the entire document.
Clicking on the <img> not only generates a click event for the image, but also for the two wrapping <div> elements. These two are captured by the document, which is why it's being logged two more times. If you log e.target you could see which element triggered the event.
In order to prevent it from propagating, use event.stopPropagation():
handleClick(e) {
e.stopPropagation();
console.log('INSIDE');
this.setState({inside: 'inside'});
}
This is because click handler is being called twice. Once by the canvas onClick component and the second time by the click handler inside the componentWillMount function. Also, the componentWillMount function listens to the clicks on the entire document because of the document.addEventListener part. I would simply use the componentWillMount to set states.
Remove the document.addEventListener part and it should be good to go.

Include HTML DOM into addEventListener()

I am trying to create a simple drag and resize system and I am having problems with adding event listeners to my dividers. Instead of adding the horrendous amount of event listeners to my HTML to every single divider with the class "draggablePanel":
<div id="content">
<div class="draggablePanel" onmousedown='dragShift.dragStart(this,this.parentElement,event)' onmouseup='dragShift.dragEnd(this.parentElement); dragShift.dragHover(this,this.parentElement,event)' onmouseenter= 'dragShift.dragHover(this,this.parentElement,event)' onmouseleave='dragShift.dragEnd(this.parentElement),dragShift.dragLeave(this.parentElement)';>
Content
</div>
</div>
I'm trying to use addEventListeners() for the job:
function addDragPanels(panelArrayName) { //Add the mouse event listeners to draggable panels
if (document.getElementsByClassName) { //Access every divider with the "draggablePanel" class
var divEle;
var draggablePanels = document.getElementsByClassName(panelArrayName);
for(var ctr=0;ctr<draggablePanels.length;ctr++){
divEle = draggablePanels[ctr];
divEle.addEventListener('mousedown', dragShift.dragStart(this,this.parentElement,event),false); //How do i go about editing this?
divEle.addEventListener('mouseup', dragShift.dragEnd( .... etc
}
}
}
if (document.readyState === "complete") {
addDragPanels("draggablePanel");
}
Basically, I am using three arguments: this,this.parentElement,event. How do I correctly pass these arguments using Javascript when I am using addEventListener()? I need the "event" argument to track mouse position in my dragging Javascript functions.
Thanks in advance for any help rendered. Sorry if this question is stupid has been asked before but I just couldn't find the solution anywhere (or maybe I don't know where and what to look).
Where you get this 'dragShift' element? Please refer me to the source.
Some browsers don't support 'addEventListener()'.
Hence, you can use:
divEle.onmousedown = function(){dragShift.dragstart(this,this.parentElement,event);};
or make an anonymous function then put it inside:
divEle.addEventListener('mouseup',function(){dragShift.dragStart(this,this.parentElement,event);},false);
Please let me know if any of it works!

Url link shows upon drag, it should be hidden

I'm building a gallery in ReactJs and have draggable thumbs below main image.
The problem is, whenever I drag a thumb, its link url drags with the mouse.
http://prntscr.com/76kfrk
Is it possible to hide the url on drag?
Preferably using with Vanilla Js
Since it's the title of the a-tag (you didn't set an alt title, so it displays the url), you should be able to hide it with the following bit of Javascript:
var elements = document.getElementsByTagName('a');
for (var i = 0, len = elements.length; i < len; i++)
{
elements[i].removeAttribute('title');
}
This can be prevented with a preventDefault on the onMouseDown event.
//React Component
someFunc: function(e) {
e.preventDefault();
},
render: function() {
return (<img src="somepicture.jpg" onMouseDown={this.someFunc} />)
}
As pointed out below, the only way I know to prevent this is by calling event.preventDefault().
If you are using a special plugin for the slider then I would suggest you to take a look into its docs to see how you can receive the mousedown event when the user starts the drag.
This can also be prevented by using the CSS property -webkit-user-drag: none; for elements.

Categories

Resources