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 5c9f8e7ce38..a3a470e7560 100644 --- a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js @@ -2531,6 +2531,979 @@ describe('DOMPluginEventSystem', () => { expect(log).toEqual(['beforeblur', 'afterblur']); }); + // @gate www + it('beforeblur and afterblur are called after a focused element is hidden', async () => { + const log = []; + // We have to persist here because we want to read relatedTarget later. + const onAfterBlur = jest.fn(e => { + e.persist(); + log.push(e.type); + }); + const onBeforeBlur = jest.fn(e => log.push(e.type)); + const innerRef = React.createRef(); + const innerRef2 = React.createRef(); + const setAfterBlurHandle = + ReactDOM.unstable_createEventHandle('afterblur'); + const setBeforeBlurHandle = + ReactDOM.unstable_createEventHandle('beforeblur'); + + const Component = ({show}) => { + const ref = React.useRef(null); + + React.useEffect(() => { + const clear1 = setAfterBlurHandle(document, onAfterBlur); + const clear2 = setBeforeBlurHandle(ref.current, onBeforeBlur); + + return () => { + clear1(); + clear2(); + }; + }); + + return ( +
+ + + +
+
+ ); + }; + + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.focus(); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + expect(onAfterBlur).toHaveBeenCalledTimes(0); + + await act(() => { + root.render(); + }); + + if (gate('enableEventAPIActivityFix')) { + expect(onBeforeBlur).toHaveBeenCalledTimes(1); + expect(onAfterBlur).toHaveBeenCalledTimes(1); + expect(onAfterBlur).toHaveBeenCalledWith( + expect.objectContaining({relatedTarget: inner}), + ); + expect(log).toEqual(['beforeblur', 'afterblur']); + } else { + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + expect(onAfterBlur).toHaveBeenCalledTimes(0); + expect(log).toEqual([]); + } + }); + + // @gate www + it('beforeblur and afterblur are called after a nested focused element is hidden', async () => { + const log = []; + // We have to persist here because we want to read relatedTarget later. + const onAfterBlur = jest.fn(e => { + e.persist(); + log.push(e.type); + }); + const onBeforeBlur = jest.fn(e => log.push(e.type)); + const innerRef = React.createRef(); + const innerRef2 = React.createRef(); + const setAfterBlurHandle = + ReactDOM.unstable_createEventHandle('afterblur'); + const setBeforeBlurHandle = + ReactDOM.unstable_createEventHandle('beforeblur'); + + const Component = ({show}) => { + const ref = React.useRef(null); + + React.useEffect(() => { + const clear1 = setAfterBlurHandle(document, onAfterBlur); + const clear2 = setBeforeBlurHandle(ref.current, onBeforeBlur); + + return () => { + clear1(); + clear2(); + }; + }); + + return ( +
+ +
+ +
+
+
+
+ ); + }; + + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.focus(); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + expect(onAfterBlur).toHaveBeenCalledTimes(0); + + await act(() => { + root.render(); + }); + + if (gate('enableEventAPIActivityFix')) { + expect(onBeforeBlur).toHaveBeenCalledTimes(1); + expect(onAfterBlur).toHaveBeenCalledTimes(1); + expect(onAfterBlur).toHaveBeenCalledWith( + expect.objectContaining({relatedTarget: inner}), + ); + expect(log).toEqual(['beforeblur', 'afterblur']); + } else { + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + expect(onAfterBlur).toHaveBeenCalledTimes(0); + expect(log).toEqual([]); + } + }); + + // @gate www + it('beforeblur should skip handlers from a hidden subtree after the focused element is hidden via Activity', async () => { + 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(); + if (clear2) { + clear2(); + } + }; + }); + + return ( +
+ +
+ +
+
+
+
+ ); + }; + + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.focus(); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + + await act(() => { + root.render(); + }); + + if (gate('enableEventAPIActivityFix')) { + // The handler on ref2 (inside Activity) should be skipped since it's being hidden + expect(onBeforeBlur).toHaveBeenCalledTimes(1); + } else { + // Without the fix, blur events aren't fired when Activity is hidden + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + } + }); + + // @gate www && enableLegacyHidden + it('beforeblur and afterblur are not called after a focused element is hidden inside LegacyHidden', async () => { + const log = []; + // We have to persist here because we want to read relatedTarget later. + const onAfterBlur = jest.fn(e => { + e.persist(); + log.push(e.type); + }); + const onBeforeBlur = jest.fn(e => log.push(e.type)); + const innerRef = React.createRef(); + const innerRef2 = React.createRef(); + const setAfterBlurHandle = + ReactDOM.unstable_createEventHandle('afterblur'); + const setBeforeBlurHandle = + ReactDOM.unstable_createEventHandle('beforeblur'); + const LegacyHidden = React.unstable_LegacyHidden; + + const Component = ({show}) => { + const ref = React.useRef(null); + + React.useEffect(() => { + const clear1 = setAfterBlurHandle(document, onAfterBlur); + const clear2 = setBeforeBlurHandle(ref.current, onBeforeBlur); + + return () => { + clear1(); + clear2(); + }; + }); + + return ( +
+ + + +
+
+ ); + }; + + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.focus(); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + expect(onAfterBlur).toHaveBeenCalledTimes(0); + + await act(() => { + root.render(); + }); + + // LegacyHidden does not trigger blur events when hiding + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + expect(onAfterBlur).toHaveBeenCalledTimes(0); + expect(log).toEqual([]); + }); + + // @gate www && enableLegacyHidden + it('beforeblur and afterblur are not called after a nested focused element is hidden inside LegacyHidden', async () => { + const log = []; + // We have to persist here because we want to read relatedTarget later. + const onAfterBlur = jest.fn(e => { + e.persist(); + log.push(e.type); + }); + const onBeforeBlur = jest.fn(e => log.push(e.type)); + const innerRef = React.createRef(); + const innerRef2 = React.createRef(); + const setAfterBlurHandle = + ReactDOM.unstable_createEventHandle('afterblur'); + const setBeforeBlurHandle = + ReactDOM.unstable_createEventHandle('beforeblur'); + const LegacyHidden = React.unstable_LegacyHidden; + + const Component = ({show}) => { + const ref = React.useRef(null); + + React.useEffect(() => { + const clear1 = setAfterBlurHandle(document, onAfterBlur); + const clear2 = setBeforeBlurHandle(ref.current, onBeforeBlur); + + return () => { + clear1(); + clear2(); + }; + }); + + return ( +
+ +
+ +
+
+
+
+ ); + }; + + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.focus(); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + expect(onAfterBlur).toHaveBeenCalledTimes(0); + + await act(() => { + root.render(); + }); + + // LegacyHidden does not trigger blur events when hiding + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + expect(onAfterBlur).toHaveBeenCalledTimes(0); + expect(log).toEqual([]); + }); + + // @gate www && enableLegacyHidden + it('beforeblur is not called after the focused element is hidden inside LegacyHidden', async () => { + const onBeforeBlur = jest.fn(); + const innerRef = React.createRef(); + const innerRef2 = React.createRef(); + const setBeforeBlurHandle = + ReactDOM.unstable_createEventHandle('beforeblur'); + const ref2 = React.createRef(); + const LegacyHidden = React.unstable_LegacyHidden; + + 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(); + if (clear2) { + clear2(); + } + }; + }); + + return ( +
+ +
+ +
+
+
+
+ ); + }; + + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.focus(); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + + await act(() => { + root.render(); + }); + + // LegacyHidden does not trigger blur events when hiding + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + }); + + // @gate www && enableLegacyHidden + it('beforeblur is called when Activity hides a focused element nested inside LegacyHidden', async () => { + // + // Hiding Activity (outer) should trigger blur + const onBeforeBlur = jest.fn(); + const innerRef = React.createRef(); + const setBeforeBlurHandle = + ReactDOM.unstable_createEventHandle('beforeblur'); + const LegacyHidden = React.unstable_LegacyHidden; + + const Component = ({showActivity, showLegacyHidden}) => { + const ref = React.useRef(null); + + React.useEffect(() => { + const clear = setBeforeBlurHandle(ref.current, onBeforeBlur); + return () => clear(); + }); + + return ( +
+ + + + + +
+ ); + }; + + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + , + ); + }); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.focus(); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + + // Hide Activity (outer) - should trigger blur + await act(() => { + root.render( + , + ); + }); + + if (gate('enableEventAPIActivityFix')) { + expect(onBeforeBlur).toHaveBeenCalledTimes(1); + } else { + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + } + }); + + // @gate www && enableLegacyHidden + it('beforeblur is not called when LegacyHidden hides a focused element nested inside Activity', async () => { + // + // Hiding LegacyHidden (inner) should NOT trigger blur + const onBeforeBlur = jest.fn(); + const innerRef = React.createRef(); + const setBeforeBlurHandle = + ReactDOM.unstable_createEventHandle('beforeblur'); + const LegacyHidden = React.unstable_LegacyHidden; + + const Component = ({showActivity, showLegacyHidden}) => { + const ref = React.useRef(null); + + React.useEffect(() => { + const clear = setBeforeBlurHandle(ref.current, onBeforeBlur); + return () => clear(); + }); + + return ( +
+ + + + + +
+ ); + }; + + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + , + ); + }); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.focus(); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + + // Hide LegacyHidden (inner) - should NOT trigger blur + await act(() => { + root.render( + , + ); + }); + + // LegacyHidden does not trigger blur events + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + }); + + // @gate www && enableLegacyHidden + it('beforeblur is called once when both Activity and LegacyHidden hide simultaneously (Activity outer)', async () => { + // + // Hiding both at once - Activity should trigger blur + const onBeforeBlur = jest.fn(); + const innerRef = React.createRef(); + const setBeforeBlurHandle = + ReactDOM.unstable_createEventHandle('beforeblur'); + const LegacyHidden = React.unstable_LegacyHidden; + + const Component = ({showActivity, showLegacyHidden}) => { + const ref = React.useRef(null); + + React.useEffect(() => { + const clear = setBeforeBlurHandle(ref.current, onBeforeBlur); + return () => clear(); + }); + + return ( +
+ + + + + +
+ ); + }; + + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + , + ); + }); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.focus(); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + + // Hide both at once + await act(() => { + root.render( + , + ); + }); + + if (gate('enableEventAPIActivityFix')) { + // Activity (outer) triggers blur, LegacyHidden does not + expect(onBeforeBlur).toHaveBeenCalledTimes(1); + } else { + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + } + }); + + // @gate www && enableLegacyHidden + it('beforeblur is called when Activity hides after LegacyHidden already hid (Activity outer)', async () => { + // + // First hide LegacyHidden, then hide Activity + const onBeforeBlur = jest.fn(); + const innerRef = React.createRef(); + const setBeforeBlurHandle = + ReactDOM.unstable_createEventHandle('beforeblur'); + const LegacyHidden = React.unstable_LegacyHidden; + + const Component = ({showActivity, showLegacyHidden}) => { + const ref = React.useRef(null); + + React.useEffect(() => { + const clear = setBeforeBlurHandle(ref.current, onBeforeBlur); + return () => clear(); + }); + + return ( +
+ + + + + +
+ ); + }; + + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + , + ); + }); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.focus(); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + + // First hide LegacyHidden (inner) - should NOT trigger blur + await act(() => { + root.render( + , + ); + }); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + + // Then hide Activity (outer) - should trigger blur + await act(() => { + root.render( + , + ); + }); + + if (gate('enableEventAPIActivityFix')) { + expect(onBeforeBlur).toHaveBeenCalledTimes(1); + } else { + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + } + }); + + // @gate www && enableLegacyHidden + it('beforeblur is not called when LegacyHidden hides after Activity already hid (Activity outer)', async () => { + // + // First hide Activity, then hide LegacyHidden + const onBeforeBlur = jest.fn(); + const innerRef = React.createRef(); + const setBeforeBlurHandle = + ReactDOM.unstable_createEventHandle('beforeblur'); + const LegacyHidden = React.unstable_LegacyHidden; + + const Component = ({showActivity, showLegacyHidden}) => { + const ref = React.useRef(null); + + React.useEffect(() => { + const clear = setBeforeBlurHandle(ref.current, onBeforeBlur); + return () => clear(); + }); + + return ( +
+ + + + + +
+ ); + }; + + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + , + ); + }); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.focus(); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + + // First hide Activity (outer) - should trigger blur + await act(() => { + root.render( + , + ); + }); + + if (gate('enableEventAPIActivityFix')) { + expect(onBeforeBlur).toHaveBeenCalledTimes(1); + } else { + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + } + + onBeforeBlur.mockClear(); + + // Then hide LegacyHidden (inner) - should NOT trigger another blur + await act(() => { + root.render( + , + ); + }); + + // No additional blur event + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + }); + + // @gate www && enableLegacyHidden + it('beforeblur is not called when LegacyHidden (outer) hides a focused element nested inside Activity', async () => { + // + // Hiding LegacyHidden (outer) should NOT trigger blur + const onBeforeBlur = jest.fn(); + const innerRef = React.createRef(); + const setBeforeBlurHandle = + ReactDOM.unstable_createEventHandle('beforeblur'); + const LegacyHidden = React.unstable_LegacyHidden; + + const Component = ({showActivity, showLegacyHidden}) => { + const ref = React.useRef(null); + + React.useEffect(() => { + const clear = setBeforeBlurHandle(ref.current, onBeforeBlur); + return () => clear(); + }); + + return ( +
+ + + + + +
+ ); + }; + + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + , + ); + }); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.focus(); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + + // Hide LegacyHidden (outer) - should NOT trigger blur + await act(() => { + root.render( + , + ); + }); + + // LegacyHidden does not trigger blur events + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + }); + + // @gate www && enableLegacyHidden + it('beforeblur is called when Activity (inner) hides a focused element nested inside LegacyHidden', async () => { + // + // Hiding Activity (inner) should trigger blur + const onBeforeBlur = jest.fn(); + const innerRef = React.createRef(); + const setBeforeBlurHandle = + ReactDOM.unstable_createEventHandle('beforeblur'); + const LegacyHidden = React.unstable_LegacyHidden; + + const Component = ({showActivity, showLegacyHidden}) => { + const ref = React.useRef(null); + + React.useEffect(() => { + const clear = setBeforeBlurHandle(ref.current, onBeforeBlur); + return () => clear(); + }); + + return ( +
+ + + + + +
+ ); + }; + + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + , + ); + }); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.focus(); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + + // Hide Activity (inner) - should trigger blur + await act(() => { + root.render( + , + ); + }); + + if (gate('enableEventAPIActivityFix')) { + expect(onBeforeBlur).toHaveBeenCalledTimes(1); + } else { + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + } + }); + + // @gate www && enableLegacyHidden + it('beforeblur is called once when both Activity and LegacyHidden hide simultaneously (LegacyHidden outer)', async () => { + // + // Hiding both at once - Activity should trigger blur + const onBeforeBlur = jest.fn(); + const innerRef = React.createRef(); + const setBeforeBlurHandle = + ReactDOM.unstable_createEventHandle('beforeblur'); + const LegacyHidden = React.unstable_LegacyHidden; + + const Component = ({showActivity, showLegacyHidden}) => { + const ref = React.useRef(null); + + React.useEffect(() => { + const clear = setBeforeBlurHandle(ref.current, onBeforeBlur); + return () => clear(); + }); + + return ( +
+ + + + + +
+ ); + }; + + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + , + ); + }); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.focus(); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + + // Hide both at once + await act(() => { + root.render( + , + ); + }); + + if (gate('enableEventAPIActivityFix')) { + // Activity triggers blur, LegacyHidden does not + expect(onBeforeBlur).toHaveBeenCalledTimes(1); + } else { + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + } + }); + + // @gate www && enableLegacyHidden + it('beforeblur is called when Activity hides after LegacyHidden already hid (LegacyHidden outer)', async () => { + // + // First hide LegacyHidden, then hide Activity + const onBeforeBlur = jest.fn(); + const innerRef = React.createRef(); + const setBeforeBlurHandle = + ReactDOM.unstable_createEventHandle('beforeblur'); + const LegacyHidden = React.unstable_LegacyHidden; + + const Component = ({showActivity, showLegacyHidden}) => { + const ref = React.useRef(null); + + React.useEffect(() => { + const clear = setBeforeBlurHandle(ref.current, onBeforeBlur); + return () => clear(); + }); + + return ( +
+ + + + + +
+ ); + }; + + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + , + ); + }); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.focus(); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + + // First hide LegacyHidden (outer) - should NOT trigger blur + await act(() => { + root.render( + , + ); + }); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + + // Then hide Activity (inner) - should trigger blur + await act(() => { + root.render( + , + ); + }); + + if (gate('enableEventAPIActivityFix')) { + expect(onBeforeBlur).toHaveBeenCalledTimes(1); + } else { + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + } + }); + + // @gate www && enableLegacyHidden + it('beforeblur is not called when LegacyHidden hides after Activity already hid (LegacyHidden outer)', async () => { + // + // First hide Activity, then hide LegacyHidden + const onBeforeBlur = jest.fn(); + const innerRef = React.createRef(); + const setBeforeBlurHandle = + ReactDOM.unstable_createEventHandle('beforeblur'); + const LegacyHidden = React.unstable_LegacyHidden; + + const Component = ({showActivity, showLegacyHidden}) => { + const ref = React.useRef(null); + + React.useEffect(() => { + const clear = setBeforeBlurHandle(ref.current, onBeforeBlur); + return () => clear(); + }); + + return ( +
+ + + + + +
+ ); + }; + + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + , + ); + }); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.focus(); + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + + // First hide Activity (inner) - should trigger blur + await act(() => { + root.render( + , + ); + }); + + if (gate('enableEventAPIActivityFix')) { + expect(onBeforeBlur).toHaveBeenCalledTimes(1); + } else { + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + } + + onBeforeBlur.mockClear(); + + // Then hide LegacyHidden (outer) - should NOT trigger another blur + await act(() => { + root.render( + , + ); + }); + + // No additional blur event + expect(onBeforeBlur).toHaveBeenCalledTimes(0); + }); + // @gate www it('beforeblur and afterblur are called after a nested focused element is unmounted', async () => { const log = []; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 43db744007d..4fbbe5e4c0b 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -47,6 +47,7 @@ import type {ViewTransitionState} from './ReactFiberViewTransitionComponent'; import { alwaysThrottleRetries, enableCreateEventHandleAPI, + enableEventAPIActivityFix, enableHiddenSubtreeInsertionEffectCleanup, enableProfilerTimer, enableProfilerCommitHooks, @@ -482,7 +483,6 @@ function commitBeforeMutationEffectsOnFiber( if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) { // Check to see if the focused element was inside of a hidden (Suspense) subtree. // TODO: Move this out of the hot path using a dedicated effect tag. - // TODO: This should consider Offscreen in general and not just SuspenseComponent. if ( finishedWork.tag === SuspenseComponent && isSuspenseBoundaryBeingHidden(current, finishedWork) && @@ -492,6 +492,21 @@ function commitBeforeMutationEffectsOnFiber( shouldFireAfterActiveInstanceBlur = true; beforeActiveInstanceBlur(finishedWork); } + + // Check if an OffscreenComponent (Activity) is being hidden with focus inside. + if (enableEventAPIActivityFix) { + if ( + finishedWork.tag === OffscreenComponent && + current !== null && + current.memoizedState === null && // was visible + finishedWork.memoizedState !== null && // now hidden + // $FlowFixMe[incompatible-call] found when upgrading Flow + doesFiberContain(finishedWork, focusedInstanceHandle) + ) { + shouldFireAfterActiveInstanceBlur = true; + beforeActiveInstanceBlur(finishedWork); + } + } } } diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 587e4e6a1da..510a80813de 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -57,6 +57,9 @@ export const enableScopeAPI: boolean = false; // Experimental Create Event Handle API. export const enableCreateEventHandleAPI: boolean = false; +// Fix for Activity blur events in the Event Handle API. +export const enableEventAPIActivityFix: boolean = false; + // Support legacy Primer support on internal FB www export const enableLegacyFBSupport: boolean = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index e2674b70fee..abbf24c1282 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -43,6 +43,7 @@ export const enableAsyncDebugInfo: boolean = false; export const enableAsyncIterableChildren: boolean = false; export const enableCPUSuspense: boolean = true; export const enableCreateEventHandleAPI: boolean = false; +export const enableEventAPIActivityFix: boolean = false; export const enableMoveBefore: boolean = true; export const enableFizzExternalRuntime: boolean = true; export const enableHalt: boolean = true; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 672c3755108..2db67310e55 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -28,6 +28,7 @@ export const enableAsyncDebugInfo: boolean = false; export const enableAsyncIterableChildren: boolean = false; export const enableCPUSuspense: boolean = false; export const enableCreateEventHandleAPI: boolean = false; +export const enableEventAPIActivityFix: boolean = false; export const enableMoveBefore: boolean = true; export const enableFizzExternalRuntime: boolean = true; export const enableHalt: boolean = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 67c08d94367..74b6ce56321 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -26,6 +26,7 @@ export const disableCommentsAsDOMContainers: boolean = true; export const disableInputAttributeSyncing: boolean = false; export const enableScopeAPI: boolean = false; export const enableCreateEventHandleAPI: boolean = false; +export const enableEventAPIActivityFix: boolean = false; export const enableSuspenseCallback: boolean = false; export const enableTrustedTypesIntegration: boolean = false; export const disableTextareaChildren: boolean = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index a0ee01baa7d..60a79aa5a74 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -23,6 +23,7 @@ export const enableAsyncDebugInfo = false; export const enableAsyncIterableChildren = false; export const enableCPUSuspense = true; export const enableCreateEventHandleAPI = false; +export const enableEventAPIActivityFix = false; export const enableMoveBefore = false; export const enableFizzExternalRuntime = true; export const enableHalt = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 058fd752d6e..b2fad1b4a0c 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -26,6 +26,7 @@ export const disableCommentsAsDOMContainers: boolean = true; export const disableInputAttributeSyncing: boolean = false; export const enableScopeAPI: boolean = true; export const enableCreateEventHandleAPI: boolean = false; +export const enableEventAPIActivityFix: boolean = false; export const enableSuspenseCallback: boolean = true; export const disableLegacyContext: boolean = false; export const disableLegacyContextForFunctionComponents: boolean = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 1d174609cb9..cfce5decaeb 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -39,6 +39,7 @@ export const enableFragmentRefsScrollIntoView: boolean = __VARIANT__; export const enableAsyncDebugInfo: boolean = __VARIANT__; export const enableInternalInstanceMap: boolean = __VARIANT__; +export const enableEventAPIActivityFix: boolean = __VARIANT__; // TODO: These flags are hard-coded to the default values used in open source. // Update the tests so that they pass in either mode, then set these diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 0752f266afc..1e6033467e0 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -18,6 +18,7 @@ export const { alwaysThrottleRetries, disableLegacyContextForFunctionComponents, disableSchedulerTimeoutInWorkLoop, + enableEventAPIActivityFix, enableHiddenSubtreeInsertionEffectCleanup, enableInfiniteRenderLoopDetection, enableNoCloningMemoCache,