diff --git a/packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js b/packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js index 9a9c3786b771..7310a9c49b95 100644 --- a/packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js @@ -13,6 +13,7 @@ let React; let ReactDOM; let Scheduler; +let act; describe('ReactDOMNativeEventHeuristic-test', () => { let container; @@ -23,6 +24,7 @@ describe('ReactDOMNativeEventHeuristic-test', () => { React = require('react'); ReactDOM = require('react-dom'); Scheduler = require('scheduler'); + act = require('react-dom/test-utils').unstable_concurrentAct; document.body.appendChild(container); }); @@ -225,4 +227,68 @@ describe('ReactDOMNativeEventHeuristic-test', () => { // Therefore the form should have been submitted. expect(formSubmitted).toBe(true); }); + + // @gate experimental + // @gate enableDiscreteEventMicroTasks && enableNativeEventPriorityInference + it('mouse over should be user-blocking but not discrete', async () => { + const root = ReactDOM.unstable_createRoot(container); + + const target = React.createRef(null); + function Foo() { + const [isHover, setHover] = React.useState(false); + React.useLayoutEffect(() => { + target.current.onmouseover = () => setHover(true); + }); + return
{isHover ? 'hovered' : 'not hovered'}
; + } + + await act(async () => { + root.render(); + }); + expect(container.textContent).toEqual('not hovered'); + + await act(async () => { + const mouseOverEvent = document.createEvent('MouseEvents'); + mouseOverEvent.initEvent('mouseover', true, true); + dispatchAndSetCurrentEvent(target.current, mouseOverEvent); + + // 3s should be enough to expire the updates + Scheduler.unstable_advanceTime(3000); + expect(Scheduler).toFlushExpired([]); + expect(container.textContent).toEqual('hovered'); + }); + }); + + // @gate experimental + // @gate enableDiscreteEventMicroTasks && enableNativeEventPriorityInference + it('mouse enter should be user-blocking but not discrete', async () => { + const root = ReactDOM.unstable_createRoot(container); + + const target = React.createRef(null); + function Foo() { + const [isHover, setHover] = React.useState(false); + React.useLayoutEffect(() => { + target.current.onmouseenter = () => setHover(true); + }); + return
{isHover ? 'hovered' : 'not hovered'}
; + } + + await act(async () => { + root.render(); + }); + expect(container.textContent).toEqual('not hovered'); + + await act(async () => { + // Note: React does not use native mouseenter/mouseleave events + // but we should still correctly determine their priority. + const mouseEnterEvent = document.createEvent('MouseEvents'); + mouseEnterEvent.initEvent('mouseenter', true, true); + dispatchAndSetCurrentEvent(target.current, mouseEnterEvent); + + // 3s should be enough to expire the updates + Scheduler.unstable_advanceTime(3000); + expect(Scheduler).toFlushExpired([]); + expect(container.textContent).toEqual('hovered'); + }); + }); }); diff --git a/packages/react-dom/src/events/DOMEventNames.js b/packages/react-dom/src/events/DOMEventNames.js index 42d72bc61ac2..5c8b5916f386 100644 --- a/packages/react-dom/src/events/DOMEventNames.js +++ b/packages/react-dom/src/events/DOMEventNames.js @@ -17,6 +17,8 @@ export type DOMEventName = // 'animationend | // 'animationstart' | | 'beforeblur' // Not a real event. This is used by event experiments. + | 'beforeinput' + | 'blur' | 'canplay' | 'canplaythrough' | 'cancel' @@ -44,9 +46,12 @@ export type DOMEventName = | 'encrypted' | 'ended' | 'error' + | 'focus' | 'focusin' | 'focusout' + | 'fullscreenchange' | 'gotpointercapture' + | 'hashchange' | 'input' | 'invalid' | 'keydown' @@ -58,6 +63,8 @@ export type DOMEventName = | 'loadedmetadata' | 'lostpointercapture' | 'mousedown' + | 'mouseenter' + | 'mouseleave' | 'mousemove' | 'mouseout' | 'mouseover' @@ -74,12 +81,15 @@ export type DOMEventName = | 'pointerout' | 'pointerover' | 'pointerup' + | 'popstate' | 'progress' | 'ratechange' | 'reset' | 'scroll' | 'seeked' | 'seeking' + | 'select' + | 'selectstart' | 'selectionchange' | 'stalled' | 'submit' diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 358c303b7918..0a89d2cb70a7 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -397,6 +397,16 @@ export function getEventPriority(domEventName: DOMEventName): * { // eslint-disable-next-line no-fallthrough case 'beforeblur': case 'afterblur': + // Not used by React but could be by user code: + // eslint-disable-next-line no-fallthrough + case 'beforeinput': + case 'blur': + case 'fullscreenchange': + case 'focus': + case 'hashchange': + case 'popstate': + case 'select': + case 'selectstart': return InputDiscreteLanePriority; case 'drag': case 'dragenter': @@ -413,6 +423,10 @@ export function getEventPriority(domEventName: DOMEventName): * { case 'toggle': case 'touchmove': case 'wheel': + // Not used by React but could be by user code: + // eslint-disable-next-line no-fallthrough + case 'mouseenter': + case 'mouseleave': return InputContinuousLanePriority; default: return DefaultLanePriority;