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
Related
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!
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 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.
I have a slider that I am currently making. I am making slow progress, but I am making progress nonetheless!
Currently I have this:
http://codepen.io/r3plica/pen/mEKyGG?editors=1011#0
There are 2 things you can do with this control, the first thing is you can drag left or right. The second thing you can do is click a "point" and it will scroll to the center.
The problem I have is that if I start dragging from a point, when I let go it will invoke the moveToCenter method.
I have tried to prevent this by adding
// Stop from accessing any child events
e.preventDefault();
e.stopPropagation();
to the end of the dragEventHandler, but this did not work.
I also have 2 boolean values options.drag and options.start. I though I might be able to use them somehow (if the drag has started and is enabled then don't perform the moveToCenter but this didn't work either.
Do anyone have any idea how to get this to work?
Maybe this will help. You can register your events in bubbling or capturing mode, using addEventListener method. It defines orders of processing your events - child -> parent (bubbling), or vice versa (capturing).
http://www.quirksmode.org/js/events_advanced.html
So, if you use addEventListener(event, handler, true), it will use capturing event mode.
Codepen:
http://codepen.io/anon/pen/bZKdqV?editors=1011
divs.forEach(function (div) {
div.addEventListener('click', function(e) {
e.stopPropagation();
console.log('parent');
}, true);
});
Be aware of browser support (IE9+). All modern browsers - yes, of course.
http://caniuse.com/#search=addeventlistener
Update
So it turned out to be easier than first approach. (no need for capturing)
Check out codepen:
http://codepen.io/anon/pen/QExjzV?editors=1010
Changes from your sample:
At the beginning of moveToCenter: function(e, options, animate) function
if (options.started) {
return;
}
In if (['mouseup', 'mouseleave'].indexOf(e.type) > -1):
setTimeout(function() {
options.started = false;
} , 100);
instead of
options.started = false;
Hope this helps.
Edit: Solution
Thanks to Gaby for the help in finding a solution! Didn't quite work exactly how I wanted it to, found a better solution modified from the answers. What I do is only execute the mouseover/mouseout functions when the two elements (target and related target) do not share a parent.
Just modified Gaby's example a bit and have everything working great. As long as your popup is within the same div element as whatever spawns it (even if it's outside the main contents you can append it with overflow visible) and you don't go between non-shared elements on the way over to it, it'll stay alive.
divContents.addEventListener('mouseover', mouseEnter(showPopup, divContents));
divContents.addEventListener('mouseout', mouseEnter(hidePopup, divContents));
Now, the modified mouseEnter...
function mouseEnter(_fn, _parent) {
return function(_evt) {
if(!shareParent(_evt.target, _evt.relatedTarget, _parent)) {
_fn.call(this, _evt);
}
}
};
function shareParent(_object1, _object2, _parent) {
if (_object1 === _object2) {
return true;
}
while (_object1 && _object1 !== _parent) {
_object1 = _object1.parentNode;
}
while (_object2 && _object2 !== _parent) {
_object2 = _object2.parentNode;
}
return _object1 === _object2;
}
Solved my problem A-OK, because what I want to fire for mouseover and mouseout events will only happen when the elements don't share a parent - exactly how I intended on displaying the popups anyhow.
Thanks again for the code example from Gaby, though!
NOTE: No error checking for parent validity in shareParent function, haven't checked but it should always return true when it gets to the top of the tree (assuming the _parent isn't actually a parent of either _object1 or _object2). So make sure the parent object you pass to it is valid.
Original Question:
I'm having a problem in JavaScript right now.
I'm trying to create a div Element that pops up dynamically when something has a mouseover. The div is created directly adjacent to the object that spawns it.
divCreator.addEventListener('mouseover', createPopup, true);
divCreator.addEventListener('mouseout', hidePopup, true);
That creates the popup. Now, in the popup, when I create it, I run this before I append it to the document:
divPopup.addEventListener('mouseover', createPopup, true);
divPopup.addEventListener('mouseout', hidePopup, true);
This ensures that if I mouseover the popup, it keeps it alive (because the divCreator mouseout will fire) and when I mouseout of the popup it disappears.
However, with this method, whenever I mouseover a child element it detects a mouseout event from the parent element (divPopup) and closes the div.
Can I make the children 'transparent' to Events, so-to-speak?
There are two events that handle this case.
The mouseenter W3 DOM3 docs and mouseleave W3 DOM3 docs but they are currently in the DOM3 Events working draft.
They were introduced by Microsoft IE5.5, and Firefox has added support in v10.
A workaround is to manually check and cancel the execution of your handler if the newly moused-over element is a child of your top level one..
code adapted from http://blog.stchur.com/2007/03/15/mouseenter-and-mouseleave-events-for-firefox-and-other-non-ie-browsers/
divCreator.addEventListener('mouseover', mouseEnter(createPopup), true);
divCreator.addEventListener('mouseout', mouseEnter(hidePopup), true);
function mouseEnter(_fn)
{
return function(_evt)
{
var relTarget = _evt.relatedTarget;
if (this === relTarget || isAChildOf(this, relTarget))
{ return; }
_fn.call(this, _evt);
}
};
function isAChildOf(_parent, _child)
{
if (_parent === _child) { return false; }
while (_child && _child !== _parent)
{ _child = _child.parentNode; }
return _child === _parent;
}
Demo at http://jsfiddle.net/gaby/jMvHv/ (open your console for log messages)