react: mouseenter fires on disabled inputs whereas mouseleave does not

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

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Reactions: 26
  • Comments: 39 (11 by maintainers)

Commits related to this issue

Most upvoted comments

There is also a css workaroud:

button[disabled] { pointer-events: none; }

Updated @CoryDanielson’s fiddle: https://jsfiddle.net/Sl1v3r/sLsut3cy/

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.

To be clear, the change in 16.13 seems to be that mouseenter will no longer fire for disabled inputs.

So it seems pointer-events: none actually helps trigger the onMouseLeave event which seems counterintuitive to me but it works.

Any progress or solution?

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.

More on this: it works proper in Firefox but doesn’t work in Chrome and Safari

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.


possible solutions?

  1. 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:

    • (–) You have detect the user agent and do this only where needed to avoid sending duplicate events.
    • (–) You have to keep a list of node elements which have this problem when they are disabled. In chrome, for example, disabled textarea doesn’t suffer from this problem and according to mdn, some input types don’t, either.
    • (–) In chrome (and probably IE), we will still miss the 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.
  2. 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?

This is still broken: https://github.com/facebook/react/issues/19419 opened a new issue for it

If there were updates they would be on this issue 😉 Would you like to look into why this happens?

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.

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}.

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)

https://jsfiddle.net/qfLzkz5x/6/

@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…