There is an asymmetry to EnterLeave event plugin. Since mouseenter
is created from the relativeTarget of the mouseout
event it fires even though the target is disabled. Since the mouseleave
is the inverse, i.e requires that the disabled element fire a mouseout, it doesn't fire a mouseleave
for the disabled element.
I am pretty sure the correct behavior here is that neither event should fire if its target is disabled, since this mirrors mouseout
. No idea if none-chrome browsers have the same behavior for which mouse events fire on disabled elements.
Additional caveat I just realized, React is probably also not firing mousenter
events in the case where the mouse leaves a disabled element into a non disabled element
So here is an initial attempt at a fix but I can't figure out how to properly use EventPropagators
here. Is there a way I am missing to listen for child events (i.e mouseout/over) but also return an event that doesn't bubble itself?
the below only listens for mouseout/over on the element that has the callback attached :/
var EventConstants = require("./EventConstants");
var EventPropagators = require("./EventPropagators");
var SyntheticMouseEvent = require("./SyntheticMouseEvent");
var containsNode = require("./containsNode");
var ReactMount = require("./ReactMount");
var keyOf = require("./keyOf");
var topLevelTypes = EventConstants.topLevelTypes;
var getFirstReactDOM = ReactMount.getFirstReactDOM;
var eventTypes = {
mouseEnter: {
registrationName: keyOf({ onMouseEnter: null }),
dependencies: [topLevelTypes.topMouseOut, topLevelTypes.topMouseOver]
},
mouseLeave: {
registrationName: keyOf({ onMouseLeave: null }),
dependencies: [topLevelTypes.topMouseOut, topLevelTypes.topMouseOver]
}
};
var extractedEvents = [null, null];
var EnterLeaveEventPlugin = {
eventTypes: eventTypes,
/**
* For almost every interaction we care about, there will be both a top-level
* `mouseover` and `mouseout` event that occurs. Only use `mouseout` so that
* we do not extract duplicate events. However, moving the mouse into the
* browser from outside will not fire a `mouseout` event. In this case, we use
* the `mouseover` top-level event.
*
* @param {string} topLevelType Record from `EventConstants`.
* @param {DOMEventTarget} topLevelTarget The listening component root node.
* @param {string} topLevelTargetID ID of `topLevelTarget`.
* @param {object} nativeEvent Native browser event.
* @return {*} An accumulation of synthetic events.
* @see {EventPluginHub.extractEvents}
*/
extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) {
if (topLevelType !== topLevelTypes.topMouseOut && topLevelType !== topLevelTypes.topMouseOver) {
// Must not be a mouse in or mouse out - ignoring.
return null;
}
var win;
if (topLevelTarget.window === topLevelTarget) {
// `topLevelTarget` is probably a window object.
win = topLevelTarget;
} else {
// TODO: Figure out why `ownerDocument` is sometimes undefined in IE8.
var doc = topLevelTarget.ownerDocument;
if (doc) {
win = doc.defaultView || doc.parentWindow;
} else {
win = window;
}
}
var eventType;
var target = getFirstReactDOM(nativeEvent.target) || win;
var related = getFirstReactDOM(nativeEvent.relatedTarget || nativeEvent.toElement);
//console.log('hii!!')
if (!related || related !== target && !containsNode(target, related)) {
related = related || win;
if (topLevelType === topLevelTypes.topMouseOut) {
eventType = 'mouseLeave';
} else {
eventType = 'mouseEnter';
}
var event = SyntheticMouseEvent.getPooled(eventTypes[eventType], topLevelTargetID, nativeEvent);
event.type = eventType.toLowerCase();
//event.target = target;
event.relatedTarget = related;
// this isn't right!~!!
EventPropagators.accumulateDirectDispatches(event);
return event;
}
return null;
}
};
module.exports = EnterLeaveEventPlugin;
@syranide @sebmarkbage
I am pretty sure the correct behavior here is that neither event should fire if its target is disabled.
An example of when you _would_ want mouseleave
to fire on a disabled element is if you disabled the button onclick
.
Think of a payment button: mouseenter
is called changing the hover state {hover:true}
then onclick
sets state to disabled {disabled:true}
, meanwhile, user moves mouse away from button and the button state is changed back to enabled {disabled:false}
. The button now is in a state that is incorrect as it currently has the state {hover:true}
.
Ping @jimfb @sebmarkbage @syranide
Got bit by this again, I'd be happy to PR something, but the current mouseleave/enter code seems really deeply integrated (has its own event propagator?). My current stumbling block is just the below:
Is there a way I am missing to listen for child events (i.e mouseout/over) but also return an event that doesn't bubble itself?
The problem is that I need to emit an event that does not fire when the dependent DOM events (out/leave) fire. tests seem using other examples seem either to emit for every child event, or not respond to bubbled child events at all.
adding @spicyj.
@syranide and @spicyj: either of you know of a good example where we normalize in this way? Can either of you provide some tips here?
No, I'm not sure. I would be inclined to think that both mouseenter and mouseleave should fire. It's a little weird to me that click doesn't but I can kind of justify that one in my mind.
@spicyj I tend to agree with you, the problem though is that both mouseout and mouseover do not fire on disabled elements. I am not sure why...all spec stuff I've seen suggests that just click events shouldn't, but browsers seem to go with "all mouse events". In that context it might more consistent to also not allow mouse enter/leave as well. But to be honest its all a moot discussion unless there is an implementation that doesn't rely on either mouseout or mouseover, which may be possible but not common it seems.
There is also a problem for elements, containing disabled element.
onMouseEnter
works, onMouseLeave
doesn't.
<div onMouseEnter={e => console.log("ok")}
onMouseLeave={e => alert("doesn't work")}
>
<button disabled={true} style={{ width: "100%" }}>Test</button>
</div>
Native mouseleave
event works as expected in the same situation.
A have same issue. I expect to get onMouseLeave
event on disabled button but in doesn't work
More on this: it works proper in Firefox but doesn't work in Chrome and Safari
After react 15, onClick on disabled elements does not fire. That makes me a ton of trouble. Should I open an other issue?
@attilaaronnagy fairly certain it's not a React issue, but a browser one.
I'm using the latest chrome on the latest mac and with react 0.14.4 this worked perfectly... So I don't think so. Just create an <input type="text" onClick={this.whatever} disabled />
, and the onClick won't fire.
@attilaaronnagy I believe this is expected and matches the HTML/DOM spec. @gaearon Did we miss this in the changelog?
@spicyj and if I need an input, select etc. that is disabled, but has an onClick event what can I do now? (we made a graphical html editor, and I would like to select the components with onClick, but when the edit mode active I would like to disable the component so I can't 'use' it can just 'select' it)
@spicyj even if I try this:
this.refs.textbox.addEventListener(
'click',
clickEvent,
true
);
<input ref="textbox" />
it's still not working. I don't know what kind of magic you have guys cooked up, but when it's disabled, you are blocking every possible thing no matter whatever I do. I'm thinking about puting a global click event listener on the body and backward calculating which one of the components has been clicked from the coordinates, but that is absurd... Common...
@attilaaronnagy Browsers don't support click events on inputs. We don't do anything that would affect how addEventListener works.
@spicyj if the element is not disabled: this code works and the "normal" onClick is also working on an input.
I just tried with the same browser in a jsbin, with the older react (0.13) and it's working with the disabled input also. So if the only difference is the react version number... but whatever I gave up.
@attilaaronnagy This jsbin using React 0.13.3 doesn't work for me:
http://react.jsbin.com/qesulefepu/edit?html,js,output
Let me know if you're seeing otherwise.
@spicyj https://jsfiddle.net/qfLzkz5x/
works for me...
https://jsfiddle.net/qfLzkz5x/ doesn't have a disabled input; if you disable it then you see the behavior I posted. That is the browser's doing, not React's.
If you want to capture the onClick event on a disabled input, you have to put a wrapper node around it (or listen to it at the top level, if you prefer). This matches the standard DOM behavior. I personally think this behavior is surprising and not desirable, but we find it valuable to match the DOM spec for this so we're planning to leave it this way.
Any updates on this onMouseLeave issue? I'm having the same issue with almost identical code to what @andykog posted.
https://jsfiddle.net/qfLzkz5x/1/
As a workaround, I've updated my component to watch for the native mouseleave event on the parent of the disabled element, which seems to work, but fires more than expected. My workaround is using code similar to this:
https://jsfiddle.net/qfLzkz5x/8/
Edit: Same code with the events bound directly to the disabled button. The native mouseleave event does not work in this case, either. (on Chrome and FF)
There is also a css workaroud:
button[disabled] { pointer-events: none; }
Updated @CoryDanielson's fiddle: https://jsfiddle.net/Sl1v3r/sLsut3cy/
More experiments http://www.webpackbin.com/VJeejto1Kf
Any updates?
There still seems to be an inconsistency with the browser behavior.
Here's the demonstration which shows the differences between adding the mouseenter
/ mouseleave
listeners with react and with plain js:
https://jsfiddle.net/everdimension/Lkgapb3t/
In chrome (61.0.3163.100):
React triggers a mouseenter
event on the disabled button when it shouldn't.
React doesn't trigger a mouseleave
event on the parent node when it should.
In safari (11.0):
React triggers a mouseenter
event on the disabled button when it shouldn't.
In firefox (55.0.3):
behavior is consistent with the expected browser behavior
If there were updates they would be on this issue 😉
Would you like to look into why this happens?
Yeah, well, finding out why this happens is not the hardest part.
Basically react doesn't listen to mouseleave
/ mouseenter
events at all (probably because they do not bubble) and instead listens only to the mouseout
event (and in one special case to the mouseover
, too) on the document.
And uses the reverse logic (the element you moved "out" from is sent a synthetic mouseleave event and the element you're entering (e.relatedTarget
) is sent a synthetic mouseenter event).
Mouseout event is typically fired when you move the cursor from any element to any element (or out of the viewport).
It also fires when you move the cursor into a disabled
button, this butting becoming the e.relatedTarget
of the native event. That's why react consistently sends the mouseenter
event when you enter a disabled button, even if the browser doesn't consider it a mouseenter.
But when you leave the disabled button, no browser except firefox sends a mouseout event. So react doesn't send it and we're left with this asymmetry.
The next subtle difference is that if you wrap a disabled button with another element, chrome still doesn't fire any mouseout events, but safari does fire a mouseout event for the wrapper element.
When we move the cursor away from disabled button, all browsers fire a mouseover event. The disabled button becomes the e.relatedTarget
again in this case, and again using the "reverse" logic we can consistently send the appropriate mouseleave event. Therefore obtaining consistent (and probably expected) behavior in all browsers.
React already uses the mouseover
event for one special case — when mouse enters the viewport. One more special case for disabled form elements will be the least invasive solution.
But it has the following cons:
mouseleave
event on a parent element if it has the same size as the button, because chrome doesn't send a mouseover event for it.We should attach mouseenter
/ mouseleave
listeners to the node directly. I don't know if this is possible with current Event System, but I think I remember there were talks about this concerning the scroll
events, because delegating scroll events to the document can sometimes actually be worse for performance.
I personally think this solution would be the best one — it would directly reflect native browser behavior.
In any case, this is basically a bug and produces inconsistency with the browser behavior.
May be at least let's decide which path to take in fixing it?
Thanks for the analysis! I don't have enough context on this but this will be helpful to the next person who visits. I'll try to compile a list of issues we need to fix, and this will be on it.
This seems to be related to https://bugs.chromium.org/p/chromium/issues/detail?id=120132
In Chromium 68.0.3440.106 mouseleave fires for disabled elements, in Chrome 73.0.3683.20 it does not. pointer-events: none;
workaround works for me.
Any progress or solution?
So it seems pointer-events: none
actually helps trigger the onMouseLeave event which seems counterintuitive to me but it works.
Guys, any progress please? Just came over it in real situation when I have text explaining functionality of some actions user can make. However In one case (in compare mode) the download mode of CSV is disabled. Then OnMouseEnter shows the text correctly but the text stays there even user scroll away because the OnMouseLeave doesn't work. Very inconsistent for sure a bug without a doubt. Hoping for quick fix! Thanks.
button[disabled] { pointer-events: none; }
will disable even OnMouseEnter event. But I want to allow OnMouseLeave.
<button
onMouseEnter={() => setHoveredText(dataToCompare ? advices.csvDisabled : advices.csv)} // works
onMouseLeave={() => setHoveredText(null)} // doesn't work !!!
onClick={() => exportTableToCSV('TIL-table-data.csv')}
disabled={dataToCompare}
> ICON </button>
As this seems to be caused by an underlying chrome bug, I'm not sure if a solution is forthcoming.
Should be fixed in 16.13.
https://reactjs.org/blog/2020/03/02/react-v16.13.0.html
If not please create a new issue with a reproducing example.
To be clear, the change in 16.13 seems to be that mouseenter
will no longer fire for disabled inputs.
onMouseOut still works on disabled elements though, so it can be used as an alternative to onMouseLeave
This is still broken: https://github.com/facebook/react/issues/19419 opened a new issue for it
Most helpful comment
There is also a css workaroud:
Updated @CoryDanielson's fiddle: https://jsfiddle.net/Sl1v3r/sLsut3cy/