diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 63fdb08a3ae84..8de2f2c5c7ca4 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -232,10 +232,13 @@ export function prepareForCommit(containerInfo: Container): Object | null { return activeInstance; } -export function beforeActiveInstanceBlur(): void { +export function beforeActiveInstanceBlur(internalInstanceHandle: Object): void { if (enableCreateEventHandleAPI) { ReactBrowserEventEmitterSetEnabled(true); - dispatchBeforeDetachedBlur((selectionInformation: any).focusedElem); + dispatchBeforeDetachedBlur( + (selectionInformation: any).focusedElem, + internalInstanceHandle, + ); ReactBrowserEventEmitterSetEnabled(false); } } @@ -499,12 +502,17 @@ function createEvent(type: DOMEventName, bubbles: boolean): Event { return event; } -function dispatchBeforeDetachedBlur(target: HTMLElement): void { +function dispatchBeforeDetachedBlur( + target: HTMLElement, + internalInstanceHandle: Object, +): void { if (enableCreateEventHandleAPI) { const event = createEvent('beforeblur', true); // Dispatch "beforeblur" directly on the target, // so it gets picked up by the event system and // can propagate through the React internal tree. + // $FlowFixMe: internal field + event._detachedInterceptFiber = internalInstanceHandle; target.dispatchEvent(event); } } diff --git a/packages/react-dom/src/events/DOMPluginEventSystem.js b/packages/react-dom/src/events/DOMPluginEventSystem.js index 74b95dea3d4eb..be09ccd390b01 100644 --- a/packages/react-dom/src/events/DOMPluginEventSystem.js +++ b/packages/react-dom/src/events/DOMPluginEventSystem.js @@ -34,6 +34,7 @@ import { HostComponent, HostText, ScopeComponent, + SuspenseComponent, } from 'react-reconciler/src/ReactWorkTags'; import getEventTarget from './getEventTarget'; @@ -70,6 +71,7 @@ import * as ChangeEventPlugin from './plugins/ChangeEventPlugin'; import * as EnterLeaveEventPlugin from './plugins/EnterLeaveEventPlugin'; import * as SelectEventPlugin from './plugins/SelectEventPlugin'; import * as SimpleEventPlugin from './plugins/SimpleEventPlugin'; +import {Deletion} from 'react-reconciler/src/ReactFiberFlags'; type DispatchListener = {| instance: null | Fiber, @@ -658,10 +660,11 @@ export function accumulateSinglePhaseListeners( nativeEventType: string, inCapturePhase: boolean, accumulateTargetOnly: boolean, + nativeEvent: AnyNativeEvent, ): Array { const captureName = reactName !== null ? reactName + 'Capture' : null; const reactEventName = inCapturePhase ? captureName : reactName; - const listeners: Array = []; + let listeners: Array = []; let instance = targetFiber; let lastHostComponent = null; @@ -740,6 +743,20 @@ export function accumulateSinglePhaseListeners( if (accumulateTargetOnly) { break; } + // If we are processing the onBeforeBlur event, then we need to take + // into consideration that part of the React tree might have been hidden + // or deleted (as we're invoking this event during commit). We can find + // this out by checking if intercept fiber set on the event matches the + // current instance fiber. In which case, we should clear all existing + // listeners. + if ( + enableCreateEventHandleAPI && + nativeEvent.type === 'beforeblur' && + // $FlowFixMe: internal field + nativeEvent._detachedInterceptFiber === instance + ) { + listeners = []; + } instance = instance.return; } return listeners; diff --git a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js index 6a3662962a740..e4fbc4f2157ed 100644 --- a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js @@ -2424,6 +2424,58 @@ describe('DOMPluginEventSystem', () => { expect(log).toEqual(['beforeblur', 'afterblur']); }); + // @gate experimental + it('beforeblur has the correct propagation mechancis after a nested focused element is unmounted', () => { + const onBeforeBlur = jest.fn(); + const innerRef = React.createRef(); + const innerRef2 = React.createRef(); + const setBeforeBlurHandle = ReactDOM.unstable_createEventHandle( + 'beforeblur', + ); + const ref2 = React.createRef(); + + const Component = ({show}) => { + const ref = React.useRef(null); + + React.useEffect(() => { + const clear1 = setBeforeBlurHandle(ref.current, onBeforeBlur); + let clear2; + if (ref2.current) { + clear2 = setBeforeBlurHandle(ref2.current, onBeforeBlur); + } + + return () => { + clear1(); + clear2 && clear2(); + }; + }); + + return ( +
+ {show && ( +
+ +
+ )} +
+
+ ); + }; + + ReactDOM.render(, container); + Scheduler.unstable_flushAll(); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.focus(); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + + ReactDOM.render(, container); + Scheduler.unstable_flushAll(); + + expect(onBeforeBlur).toHaveBeenCalledTimes(1); + }); + // @gate experimental it('beforeblur and afterblur are called after a focused element is suspended', () => { const log = []; @@ -2510,6 +2562,85 @@ describe('DOMPluginEventSystem', () => { document.body.removeChild(container2); }); + // @gate experimental + it('beforeblur has the correct propagation mechancis after a nested focused element is suspended', () => { + const onBeforeBlur = jest.fn(); + const innerRef = React.createRef(); + const innerRef2 = React.createRef(); + const setBeforeBlurHandle = ReactDOM.unstable_createEventHandle( + 'beforeblur', + ); + const ref2 = React.createRef(); + const Suspense = React.Suspense; + let suspend = false; + let resolve; + const promise = new Promise( + resolvePromise => (resolve = resolvePromise), + ); + + function Child() { + if (suspend) { + throw promise; + } else { + return ; + } + } + + const Component = () => { + const ref = React.useRef(null); + + React.useEffect(() => { + const clear1 = setBeforeBlurHandle(ref.current, onBeforeBlur); + let clear2; + if (ref2.current) { + clear2 = setBeforeBlurHandle(ref2.current, onBeforeBlur); + } + + return () => { + clear1(); + clear2 && clear2(); + }; + }); + + return ( +
+ +
+ +
+
+
+
+ ); + }; + + const container2 = document.createElement('div'); + document.body.appendChild(container2); + + const root = ReactDOM.createRoot(container2); + + act(() => { + root.render(); + }); + jest.runAllTimers(); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.focus(); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + + suspend = true; + act(() => { + root.render(); + }); + jest.runAllTimers(); + + expect(onBeforeBlur).toHaveBeenCalledTimes(1); + resolve(); + + document.body.removeChild(container2); + }); + // @gate experimental it('regression: does not fire beforeblur/afterblur if target is already hidden', () => { const Suspense = React.Suspense; diff --git a/packages/react-dom/src/events/plugins/SimpleEventPlugin.js b/packages/react-dom/src/events/plugins/SimpleEventPlugin.js index fafccff92014c..03bf9484b2c04 100644 --- a/packages/react-dom/src/events/plugins/SimpleEventPlugin.js +++ b/packages/react-dom/src/events/plugins/SimpleEventPlugin.js @@ -200,6 +200,7 @@ function extractEvents( nativeEvent.type, inCapturePhase, accumulateTargetOnly, + nativeEvent, ); if (listeners.length > 0) { // Intentionally create event lazily. diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 4f40bde182310..b844413196ad4 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -2172,7 +2172,7 @@ function commitBeforeMutationEffectsImpl(fiber: Fiber) { doesFiberContain(fiber, focusedInstanceHandle) ) { shouldFireAfterActiveInstanceBlur = true; - beforeActiveInstanceBlur(); + beforeActiveInstanceBlur(current); } } @@ -2206,7 +2206,7 @@ function commitBeforeMutationEffectsDeletions(deletions: Array) { if (doesFiberContain(fiber, ((focusedInstanceHandle: any): Fiber))) { shouldFireAfterActiveInstanceBlur = true; - beforeActiveInstanceBlur(); + beforeActiveInstanceBlur(fiber); } } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index c0fcb9c5c09b2..22bdb9fe0f967 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -2261,7 +2261,7 @@ function commitBeforeMutationEffects() { if ((nextEffect.flags & Deletion) !== NoFlags) { if (doesFiberContain(nextEffect, focusedInstanceHandle)) { shouldFireAfterActiveInstanceBlur = true; - beforeActiveInstanceBlur(); + beforeActiveInstanceBlur(nextEffect); } } else { // TODO: Move this out of the hot path using a dedicated effect tag. @@ -2271,7 +2271,7 @@ function commitBeforeMutationEffects() { doesFiberContain(nextEffect, focusedInstanceHandle) ) { shouldFireAfterActiveInstanceBlur = true; - beforeActiveInstanceBlur(); + beforeActiveInstanceBlur(current); } } }