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.)
Related
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
I’m stuck creating code that’s lean and want to use only JavaScript for now.
http://jsfiddle.net/TheOne_TheMany/30zdkrys
The problem I’m having is the mouseover state, when it goes over the <li> it works, but flickers when going over the <div> delete area. I know why it does that(After lots of research). So I tried mouseleave, but I need to have multiple ID on the <li> to make it work.
Is there a cleaner way of coding without creating so many ID or multiple event listeners. Especially if I'm going to add more <li> or delete them.
Thanks in advance for the help.
The issue is that the child <div> is in front of the <li> when you hover it, so the target is not a <li>, but a <div>. I'd strongly recommend you to use hover in CSS instead of JavaScript to solve it, but since you want like this, here you go:
Inside the mouseOver function, add another if statement to check if the mouse is inside delete button. Then, you add the classes to the <li> and the button.
const mouseOver = function(item){
// Btn animation'
if (item.target === btnSend) {
item.target.classList.add('btnOver');
}
// Li animation
if (item.target.matches('li')) {
item.target.classList.add('onLi');
item.target.lastChild.classList.add('containerDelete');
}
if (item.target.matches('.deleteMe')) {
item.target.parentNode.parentNode.classList.add('onLi');
item.target.classList.add('containerDelete');
}
item.stopPropagation;
}
Then, on mouseLeave function, add another if statement to check if the mouse is leaving from the <li> and delete button.
Inside it, you can also check if the the target is the <li> or the button <div>, because you have to remove the classes differently.
const mouseLeave = function(item){
// Btn animation
if(item.target === btnSend){
item.target.classList.remove('btnOver');
} else if (!item.toElement.matches('li') && !item.toElement.matches('.deleteMe')) {
if (item.target.tagName === 'LI') {
item.target.classList.remove('onLi');
item.target.lastChild.classList.remove('containerDelete');
}
if (item.target.tagName === 'DIV') {
item.target.parentNode.parentNode.classList.remove('onLi');
item.target.parentNode.classList.remove('containerDelete');
console.log('ok')
}
}
}
I have some overlay elements which are display: none initially but turn to display: inline when I hover over specific items on the page, and disappear again when the mouse hovers over something else. Exactly same behavior as tool-tips with the difference that this overlay objects have clickable and interactive elements (such as a jquery accordion).
Everything works perfectly, until I interact with these overlay elements, i.e. click on one of the clickable items in the overlay element. Then, once that overlay item becomes display:none again, the page becomes extremely laggy in terms of how long it takes when I hover over an item to find its corresponding overlay element (they are selected by their id) and for it to appear and disappear.
The strange thing is that if I click anywhere on the html body, the lag disappears and everything becomes fast as in the beginning.
Out of despair, I have tried to programmatically call blur, focus, trigger('click') once the overlay element is set back to display:none but none has helped so far, and I have to manually click on the page for the lag to go away.
Any idea what causes such behavior and how I can fix it? thanks,
Edit: code
CSS part:
span.overlay {
z-index:10;
display:none;
position:absolute;
}
span.visible { display:inline; }
HTML part: lots of such span elements, each with their own unique id.
<span class='overlay ui-widget-content' id='xyz'>
<!-- lots of stuff here -->
</span>
javascript part:
/* displays overlay element when user hovers over the first td */
$('table.foo > tbody > tr > td:first-child').hover(
function(e) {
$(this).parent().tooltip('disable');
var elem = $('#' + $(this).parent().data('overlay-id'));
if (!elem.hasClass('visible')) {
elem.css('left', e.pageX + 20).css('top', e.pageY).addClass('visible');
elem.find('.accordion:first').accordion('refresh');
}
}, function() {
var elem = $('#' + $(this).parent().data('overlay-id'));
if (! elem.is(':hover') && ! elem.hasClass('pin')) {
$(elem).removeClass('visible');
}
$(this).parent().tooltip('enable');
});
/* if mouse leaves span.visible and it is not pinned it will hide the span */
$('body').on('mouseleave', 'span.visible',
function() {
if (!$(this).hasClass('pin')) {
$(this).removeClass('visible');
}
});
Edit: profiling the code, it seems that get offsetHeight and get offsetWidth take way longer than before. Yet I do not know why this should happen and why it should go away by clicking on the page.
previously, when I do not observe the problem, these two functions each take less than 3%.
try binding the mouseleave event upon opening the "tooltip". Replace your code with this (not tested):
/* displays overlay element when user hovers over the first td */
$('table.foo > tbody > tr > td:first-child').on('mouseenter',
function(e) {
$(this).parent().tooltip('disable');
var elem = $('#' + $(this).parent().data('overlay-id'));
if (!elem.hasClass('visible')) {
elem.css('left', e.pageX + 20).css('top', e.pageY).addClass('visible');
elem.find('.accordion:first').accordion('refresh');
// notice the "ONE" handler, it'll unbind the event after execution
elem.one('mouseleave', function() {
if (!$(this).hasClass('pin')) {
$(this).removeClass('visible');
}
$(this).parent().tooltip('enable');
});
}
}
);
Notice the one listener to unbind the event after it's first execution.
I can't guarantee that this will fix your issue but I experienced lots of performance hits when a page has A LOT of elements and browsers need to check hover events that change very quickly.
This way the browser only needs to check one mouseleave event. And if it happened, it's gone again. It seems you may have too many bound events and don't clean them up properly.
I'm not sure if I replicated your desired functionality correctly so please add code if I missed something. I was unsure why exactly you'd need to bind a mouseleave event via body AND via .hover().
Basicllay i have a div with a class called .li-level-1, and inside that i have differnt ul's with lists. i Have it set up so when you click on a li-level-1 div displays the ul's and li's inside that div by animating a drop down and when you click on the next one it closes the one previously opened and slidesDown the next one.
the only thing is the a links that are inside the div's seem to trigger the slideUp/Down on level-1 and animation as well.
any Suggestions?
$('.sitemap_page .li-level-1').each(function(){
$(this).find('ul.ul-level-2').hide();
$(this).click(function(){
var this_list = $(this);
this_list.parent().find('.open').each(function(){
$(this).slideUp(function(){
this_list.find('ul.ul-level-2').addClass("open").slideDown();
}).removeClass('open');
});
if(this_list.find('ul.ul-level-2.open').length == 0) {
this_list.find('ul.ul-level-2').addClass("open").slideDown();
}
});
});
That's because of event bubbling: the click event raised on the <a> elements bubble up to their containing <div> and cause your event handler to execute.
One way to work around that problem would be to use event.target to determine the event's origin, and only perform the sliding animations if the event did not originate on a link:
$(this).click(function(event) {
if (!$(event.target).is("a")) {
var this_list = $(this);
this_list.parent().find('.open').each(function() {
$(this).slideUp(function() {
this_list.find('ul.ul-level-2').addClass("open").slideDown();
}).removeClass('open');
});
if (this_list.find('ul.ul-level-2.open').length == 0) {
this_list.find('ul.ul-level-2').addClass("open").slideDown();
}
}
});
The problem is with event bubbling as sugested by Frederic. The other possible solution is to divide your div into title and content divs. Hold data in content and check click on title (not on the parent list). This means rebuilding the handler but the code will be clearer and it won't depend on event.target.
I am having the following problem under Internet Explorer 7/8:
I have a popup that gets activated when user mouseover a link. The popup is a simple <div> that contains some data. Inside this <div> tag there is a <select> tag with some <option>s. I have attached mouseover/mouseout events to the <div>, so that this popup will stay open while cursor is over it. The problem comes when you click on the <select> and then move the cursor over any of the <option>s. This triggers the mouseout event of the <div> tag and respectively closes it.
How can I prevent the closing of the popup in IE ?
You should be able to detect if the situation is the one you want just with the values off the event. It is a little convoluted but it seems to work.
In the event handler of your outer div, do something like this:
<div onmouseover="if (isReal()) { toggle(); }"
onmouseout="if (isReal()) { toggle(); }">
</div>
Then implement the isReal method:
function isReal() {
var evt = window.event;
if (!evt) {
return true;
}
var el;
if (evt.type === "mouseout") {
el = evt.toElement;
} else if (evt.type === "mouseover") {
el = evt.fromElement;
}
if (!el) {
return false;
}
while (el) {
if (el === evt.srcElement) {
return false;
}
el = el.parentNode;
}
return true;
}
Basically the isReal method just detects if the event was coming from within the div. If so, then it returns false which avoids calling the hide toggle.
My suggestion would be to set another flag while the select box has focus. Do not close the div while the flag is set.
How about re-showing the div when the mouse is over the <options>s through mouseover events of <options>s.
Edit: execution order of mouseover of option and mouseout of div might cause problems though.
In the mouseout event for the div add a timeout to the div element that will hide the div in 200 milliseconds or so.
Then in the mouseover event for the div/select and the click event of the select clear the timeout stored in the div element.
This gives a very slight delay before hiding the div that allows the mouseover or click events to clear the timeout before it is executed. It's not pretty but it should work.
instead of using mouseout as the event to close the div, use mouseleave, then the event will only be triggered when the pointer leaves the boundary of the div, not when it moves onto other elements within it
you could try adding another mouseover event specifically for the options list.
Well, the reason for this behavior is because the mouseover/out events bubble, which effectively means that whenever you mouseover any of the elements inside the popup, the popup receives the event also.
You can read more here about these events, and here about event bubbling.
You have 3 possible solutions here:
Change the events to onmouseenter/leave. You've mentioned that this didn't help, which just sounds plain odd, since these aren't supposed to bubble.
Check srcElement in relation to from/toElement in the event.
An improved version of McKAMEY's check would be:
function isReal() {
var evt = window.event;
if (!evt) {
return true;
}
var el;
if (evt.type === "mouseout") {
el = evt.toElement;
} else if (evt.type === "mouseover") {
el = evt.fromElement;
}
if (!el) {
return false;
}
// this will also return true if el == evt.srcElement
return evt.srcElement.contains(el);
}
Does the same thing, just shorter.
3 . Another option would be to create a transparent, invisible div just under your popup that covers the area that the select box drops down into. I'm assuming that it's dropping outside the actual area of the popup.
Hope this helps!
have you tried hover instead of mouseover/out effects?
$(".myDiv").hover(function(){
$(this).show();
}, function {
$(this).hide();
});
What about something like this:
<div id="trigger">
Hover over me!
</div>
<div class="container">
<select>
<option>Blah</option>
<option>Blah</option>
</select>
</div>
$("#trigger").mouseover(function(){
$('.container).show();
});
$(".container").mouseleave(function(){
$(this).hide();
});
The basic idea is that you show the container element when you hover over the trigger then when you leave the container you hide the container. You'd need to position the container so it clipped the trigger element, otherwise it would hide straight away.
Why have mouseover / mouseout on the <div>? Why not just show the <div> on the mouse over, then set <body onmouseover="hidedivs();"> I don't know if this would work, but if the <div> is on top of the body, then the <div> should stay visible.
Many people posting solutions/examples do not seem to realize one thing: onmouseout event on <div> fires before onmouseover event on <select>.
When <div> loses focus (onmouseout) do not close it immediately, but after say, 500 milliseconds. If during this time <select> gets focus (mouseover) do not close <div> at all (clearTimeout).
Also, try to play with event propagation/bubling.
Given that selects in IE are a pain, especially when it comes to the whole layering issue where a select appears above a div even though it shouldn't, can I point you in the direction of YUI's Menu button controls. They look really nice, are easy to implement and won't cause this issue
Here is a link: http://developer.yahoo.com/yui/examples/button/btn_example07.html
You should use event.stopPropagation() while in <select>, or cancelBubble() in <select> element itself.