Skip to content

Commit eea14c8

Browse files
committed
Use over events instead of enter events because of react bug
See facebook/react#13956
1 parent 8241c03 commit eea14c8

File tree

1 file changed

+19
-12
lines changed

1 file changed

+19
-12
lines changed

src/index.tsx

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ const initialState: InteractiveState = {
5454

5555
// event listeners set by RI
5656
const eventMap: Record<string, string> = {
57-
mouseenter: 'onMouseEnter',
57+
mouseover: 'onMouseOver',
5858
mouseleave: 'onMouseLeave',
5959
mousedown: 'onMouseDown',
6060
mouseup: 'onMouseUp',
61-
pointerenter: 'onPointerEnter',
61+
pointerover: 'onPointerOver',
6262
pointerleave: 'onPointerLeave',
6363
pointerdown: 'onPointerDown',
6464
pointerup: 'onPointerUp',
@@ -367,7 +367,7 @@ const InteractiveNotMemoized: PolymorphicForwardRefExoticComponent<
367367
////////////////////////////////////
368368

369369
// handleEvent handles all events that change the interactive state of the component
370-
// for example <As onMouseEnter={handleEvent} onPointerEnter={handleEvent} etc...>
370+
// for example <As onMouseOver={handleEvent} onPointerOver={handleEvent} etc...>
371371

372372
// always set all pointer/mouse/touch event listeners
373373
// instead of just pointer event listeners (when supported by the browser)
@@ -376,7 +376,7 @@ const InteractiveNotMemoized: PolymorphicForwardRefExoticComponent<
376376
// - 1. the pointer events implementation is buggy on some devices
377377
// so pointer events on its own is not a good option
378378
// for example, on iPadOS pointer events from mouse will cease to fire for the entire page
379-
// after using mouse and touch input at the same time on the same element (note that mouse events are unaffected)
379+
// after using mouse and touch input at the same time (note that mouse events are unaffected)
380380
// - 2. the pointercancel event is useful for syncing the touchActive state with browser generated click events
381381
// as it fires as soon as the browser uses the touch interaction for another purpose (e.g. scrolling)
382382
// and this can't be replicated with touch events (touchcancel behaves differently)
@@ -388,6 +388,13 @@ const InteractiveNotMemoized: PolymorphicForwardRefExoticComponent<
388388
// and bail on updating state in setIState if the state hasn't changed to prevent unnecessary renders
389389
// also note that setting listeners for events not supported by the browser has no effect
390390

391+
// note: use onMouseOver and onPointerOver instead of onMouseEnter and onPointerEnter because
392+
// of a react bug where enter events are not dispatched on an element when the element above it is removed,
393+
// this also effects when navigating around a single page app where the user clicks on a link
394+
// and the element that's rendered on the new page under the mouse doesn't receive enter events
395+
// see: https://github.com/facebook/react/issues/13956
396+
// note that since stateChange is idempotent the extra mouse/pointer over events will have no effect
397+
391398
// useCallback so event handlers passed to <As> are referentially equivalent between renders
392399
const handleEvent = React.useCallback(
393400
(
@@ -411,14 +418,14 @@ const InteractiveNotMemoized: PolymorphicForwardRefExoticComponent<
411418
// default switch on eventFrom(e)
412419
// eventFrom mouse
413420
// switch on e.type
414-
// mouse/pointer enter -> hover: enter true
421+
// mouse/pointer over -> hover: enter true
415422
// mouse/pointer leave, pointercancel -> hover: enter false, active: exit mouseActive
416423
// mouse/pointer down -> active: enter mouseActive
417424
// mouse/pointer up -> active: exit mouseActive
418425
// eventFrom touch
419426
// switch on e.type
420427
// touchstart/pointerdown -> active: enter touchActive
421-
// touchend/pointerup/touchcancel/pointercancel/mouseenter -> active: exit touchActive
428+
// touchend/pointerup/touchcancel/pointercancel/mouseover -> active: exit touchActive
422429
// mouseleave -> hover: enter false, active: exit mouseActive
423430
switch (e.type) {
424431
case 'focus':
@@ -487,8 +494,8 @@ const InteractiveNotMemoized: PolymorphicForwardRefExoticComponent<
487494
switch (eventFrom(e)) {
488495
case 'mouse':
489496
switch (e.type) {
490-
case 'mouseenter':
491-
case 'pointerenter':
497+
case 'mouseover':
498+
case 'pointerover':
492499
stateChange({
493500
iStateKey: 'hover',
494501
state: true,
@@ -545,11 +552,11 @@ const InteractiveNotMemoized: PolymorphicForwardRefExoticComponent<
545552
case 'touchcancel':
546553
case 'pointerup':
547554
case 'pointercancel':
548-
// exit touchActive on mouseenter eventFrom touch
555+
// exit touchActive on mouseover eventFrom touch
549556
// because on android, mouse events (and focus and context menu) fire ~500ms after touch start
550557
// once they fire, click will never be fired, so exit touchActive
551558
// eslint-disable-next-line no-fallthrough
552-
case 'mouseenter':
559+
case 'mouseover':
553560
// if useExtendedTouchActive then only exit touchActive on touchend and touchcancel events
554561
// which won't fire until the touch point is removed from the screen
555562
if (
@@ -570,7 +577,7 @@ const InteractiveNotMemoized: PolymorphicForwardRefExoticComponent<
570577
// but RI is now stuck in the hover state
571578
// so RI needs to exit the hover state on a mouseleave eventFrom touch,
572579
// note that if no scrolling is involved this is not too bad because
573-
// when the mouse is used again it will be over the RI element (and will fire mouseenter event),
580+
// when the mouse is used again it will be over the RI element (and will fire mouseover event),
574581
// but if the user scrolled with the touch interaction then RI will be stuck in the hover state
575582
// until the user re-hovers and exits the RI element with their mouse, which is bad,
576583
// also note that on touch only devices this stateChange call will have no effect because RI is never in the hover or mouseActive states
@@ -600,7 +607,7 @@ const InteractiveNotMemoized: PolymorphicForwardRefExoticComponent<
600607
}
601608
},
602609
// handleEvent is dependent on event handler props that are also in eventMap
603-
// for example, restProps.onMouseEnter, restProps.onTouchStart, etc
610+
// for example, restProps.onMouseOver, restProps.onTouchStart, etc
604611
// this generates an array of event handler props that are also in eventMap
605612
// handleEvent is also dependent on stateChange, but this will always be referentially equivalent
606613
// eslint-disable-next-line react-hooks/exhaustive-deps

0 commit comments

Comments
 (0)