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!
Related
The closest to my question are: Mapbox gl js - overlapping layers and mouse event handling, Mapbox add popup on hover (layer), close on mouseleave, but keep open on popup hover and How to ignore mouse events on a mapbox layer - but they don't answer it.
I have two layers, let's imagine one is for a country, and another is for a city. Both of them a handled like this:
// CountryLayer.js
map.on("mousemove", "country-layer", e => {
// show info about the country
featureProps = e.features[0] // display some notification from the props
...
// CityLayer.js
map.on("mousemove", "city-layer", e => {
// show info about the city
featureProps = e.features[0] // display some notification from the props
...
It's done in different components. But when I mouseover city-layer mapbox thinks that I'm still "mousemoving" on top of the country-layer as well, so I get two notifications from separate components, where I need only one - in that case from the city-layer cause it's on top of country-layer.
Handling the mousemove without layerId in one place is gonna be a mess and breaks all the good rules about programming. Creating external "event manager" which will track whether I'm hovering the city and if is so will remove mousemove event from country-layer - is complex. I didn't find any good alternatives. At least, I would be glad to disable pointer events for a layer like this:
map.on("mousemove", "city-layer", e => {
map.getLayer("country-layer").setStyle({ "pointer-events": false })
featureProps = e.features[0]
...
or something like this. Is it possible? Is there more adequate way around it?
e.originalEvent.stopPropagation(); does not work
It appears, that you can do e.originalEvent.preventDefault(); in the city-layer and add a check e.originalEvent.defaultPrevented in the country-layer from the example.
However, I have issues with z-index position of layers: map.moveLayer("city-layer", "country-layer"); or vice-vise doesn't actually change the way, event propogates, so for some reason my country-layer always come first, so when it checks for e.originalEvent.defaultPrevented, preventDefault() wasn't fired yet, so it comes always false.
But technically, this answers my question.
map.on can listen on multiple layers at once. With events like mousemove it returns features in reverse layer order (from "highest" to "lowest").
So the way to do this is:
map.on("mousemove", ["city-layer", "country-layer"], e => {
feature = e.features[0]
if (feature.layer.id === 'city-layer') {
// ...
} else {
// ...
}
});
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
Absolutely stumped here...
I have a dynamically generated list of divs. It goes through each of those divs and applies an event listen on hover. Inside each of those divs is some text, a H1, some body copy and a link. Unfortunately the trigger only fires if I hover on a bit of 'blank space' in the div where there's no text.
I've tried pointer-events:none which does fix the issue but doesn't solve an issue where I have a link. Essentially I just want the whole div to respond when hovered. I swear I've done this before?!
Here's a video demonstrating: Video Recording
Here's how my JS is set up (the projectsText is getting each project element)
projectsText.forEach(project => {
project.classList.remove("active")
})
event.target.classList.add("active")
}
const changeWorkImage = (event) => {
projects.forEach(image => {
if (image.id === event.target.id) {
workImage.style.backgroundImage = "url(" + image.heroImage + ")"
} else {
return
}
})
}
projectsText.forEach(project => {
project.addEventListener('mouseover', (event) => {
changeWorkImage(event)
changeActive(event)
})
})
And how my component is structured
<div className="project" id={id}>
<p className="company">{company}</p>
<h2 className="title">{title}</h2>
<LearnMore url={url} />
</div>
The mouseover event goes to one, and only one, event handler at a time. It gives priority starting with the innermost child (nested/z-index). If you move the cursor from a parent ("project") to one of its children ("title"), then the parent loses the focus of the mouseover event.
There is a similar event, mouseenter which ignore transitions within an element. In other words, it doesn't pay attention when you move the mouse over children elements, so it in effect works from the outside in. This may work better in your case. (The converse, mouseleave, can be used to detect when the mouse has left.)
I've written a javascript file which has some event listeners in for things like tabs, accordions and so on. However these aren't present in every single page so it's looking for elements that don't exist and throws the entire js out of wack.
I know I could get around it by using multiple if statements, but it doesn't sound like it would be correct.
// Accordion
const accordions = document.querySelectorAll('.accordion li');
accordions.forEach(accordion =>{
accordion.addEventListener('click', (e) => {
e.preventDefault();
accordion.classList.toggle('open');
})
});
// Inline toggle
const inlineToggle = document.getElementById('inline-toggle');
inlineToggle.addEventListener('click', () => {
inlineToggle.nextElementSibling.classList.toggle('active');
});
const inlineToggleOptions = document.querySelectorAll('.inline-toggle-options button');
inlineToggleOptions.forEach(option => {
option.addEventListener('click', (e) => {
// Prevent default
e.preventDefault();
// Update sentence text
inlineToggle.innerHTML = option.dataset.payType;
// Remove selected class from options
inlineToggleOptions.forEach(option => {
option.classList.remove('selected');
});
// Add selected class to chosen option
option.classList.add('selected');
// Close dialog
inlineToggle.nextElementSibling.classList.remove('active');
})
});
// Cover bubbles
// Create the slidepanel
const placeholder = document.getElementById('slidepanel');
// Find all buttons
const button = document.querySelectorAll('.trigger-aside');
button.forEach((button => {
// Listen for clicks on buttons
button.addEventListener('click',(e) => {
// Prevent default
e.preventDefault();
// Get the target
const target = button.dataset.target;
console.log(target);
// Call the API
fetch(`http://****.****.uk/****/****/****/${target}`)
.then((res) => res.json())
.then(function(res) {
// Load HTML into slider panel
placeholder.innerHTML = res.object.content;
// Stop body overflow
document.body.classList.add('overflow-hidden');
// Create overlay and append
const overlay = document.querySelector('.overlay');
overlay.classList.add('active');
document.body.appendChild(overlay);
// Show the panel
placeholder.classList.add('active');
document.body.appendChild(placeholder);
// Listen for close
overlay.addEventListener('click', (e) =>{
// Close requested
document.body.classList.remove('overflow-hidden');
placeholder.classList.remove('active');
overlay.classList.remove('active');
});
})
.catch(function(err) {
// Log error
console.log(err);
});
})
}));
How do other people generally get around this issue? Any guidance appreciated!!
Event Delegation Pattern
If you have these UI elements grouped under a parent element in common across all pages(like a div wrapper), you could try making use of the Event Delegation pattern. Essentially, you can assign a click event to that parent element and make use of a function to only take action if the desired element is returned - i.e. your buttons. It would go something like...
const parent = document.querySelector('div.wrapper'); //Change selector to suit a common parent
const buttons = [...document.querySelectorAll('.inline-toggle-options button')]; // convert to array to make it easier to work with
const elementsToChange = document.querySelectorAll('.elements .to .change');
parent.addEventListener('click', toggleOptions);
function getEventTarget(e) {
e = e || window.event;
return e.target || e.srcElement; // For IE compatibility
}
function toggleOptions {
let target = getEventTarget(e);
if(buttons.includes(target)) {
// Trigger options on UI elements if any of the buttons are among the clicked elements
// Target refers to the buttons in particular, not the UI elements you want to change
}
}
Whichever way you want to refactor the code to take action on specific elements is up to you. You can group buttons by specific functionality into distinct arrays. If you had 2 or 3 arrays, you'd only need to write 2 or 3 options from a conditional statement.
For this purpose, you'll also save memory with this pattern since you're only assigning one event handler and letting child events on child elements bubble up to be taken care of by that single handler. Also, you shouldn't run into errors since the initial event handler is assigned to a parent element that's common across all pages.
Some Caveats
From the guide linked above:
Not all events bubble. The blur, focus, load and unload events don’t
bubble like other events. The blur and focus events can actually be
accessed using the capturing phase (in browsers other than IE) instead
of the bubbling phase but that’s a story for another day.
You need caution when managing some mouse events. If your code is
handling the mousemove event you are in serious risk of creating a
performance bottleneck because the mousemove event is triggered so
often. The mouseout event has a quirky behaviour that is difficult to
manage with event delegation.
I'd generally advise to think first about code organisation and second about efficiency. I agree with StevenB.'s suggestion and Luke Tubby's answer, to make the best of your current situation with a minimum of effort.
For more elaborate solutions, I would suggest to familiarise with build / packaging tools (e.g. Webpack ), that offer you ways to structure your code (and assets) in files and directories of your choice, and create page specific minified packages.
Another (completely different and independent) approach to the problem (of improving efficiency and code organisation) would be to build a single page application...
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.