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,