From cb09dbe0ab0f42185c4892c045b0c32e04f6b9cb Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 4 Nov 2019 20:02:45 +0000 Subject: [PATCH] [react-interactions] Add handleSimulateChildBlur upon DOM node removal (#17225) * [react-interactions] Add handleSimulateChildBlur upon DOM node removal --- .../src/client/ReactDOMHostConfig.js | 32 +++++++++ .../__tests__/FocusWithin-test.internal.js | 72 ++++++++++++------- .../events/src/dom/testing-library/index.js | 1 + 3 files changed, 81 insertions(+), 24 deletions(-) diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 2e9224817acb9..e0ee9f4e1da1d 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -55,6 +55,7 @@ import { addRootEventTypesForResponderInstance, mountEventResponder, unmountEventResponder, + dispatchEventForResponderEventSystem, } from '../events/DOMEventResponderSystem'; import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying'; @@ -108,6 +109,10 @@ import { enableFlareAPI, enableFundamentalAPI, } from 'shared/ReactFeatureFlags'; +import { + RESPONDER_EVENT_SYSTEM, + IS_PASSIVE, +} from 'legacy-events/EventSystemFlags'; let SUPPRESS_HYDRATION_WARNING; if (__DEV__) { @@ -447,10 +452,36 @@ export function insertInContainerBefore( } } +function handleSimulateChildBlur( + child: Instance | TextInstance | SuspenseInstance, +): void { + if ( + enableFlareAPI && + selectionInformation && + child === selectionInformation.focusedElem + ) { + const targetFiber = getClosestInstanceFromNode(child); + // Simlulate a blur event to the React Flare responder system. + dispatchEventForResponderEventSystem( + 'blur', + targetFiber, + ({ + relatedTarget: null, + target: child, + timeStamp: Date.now(), + type: 'blur', + }: any), + ((child: any): Document | Element), + RESPONDER_EVENT_SYSTEM | IS_PASSIVE, + ); + } +} + export function removeChild( parentInstance: Instance, child: Instance | TextInstance | SuspenseInstance, ): void { + handleSimulateChildBlur(child); parentInstance.removeChild(child); } @@ -461,6 +492,7 @@ export function removeChildFromContainer( if (container.nodeType === COMMENT_NODE) { (container.parentNode: any).removeChild(child); } else { + handleSimulateChildBlur(child); container.removeChild(child); } } diff --git a/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js index d9808ce691a6e..12a2d7f03aec4 100644 --- a/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js @@ -77,23 +77,24 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { describe('onFocusWithinChange', () => { let onFocusWithinChange, ref, innerRef, innerRef2; + const Component = ({show}) => { + const listener = useFocusWithin({ + onFocusWithinChange, + }); + return ( +
+ {show && } +
+
+ ); + }; + beforeEach(() => { onFocusWithinChange = jest.fn(); ref = React.createRef(); innerRef = React.createRef(); innerRef2 = React.createRef(); - const Component = () => { - const listener = useFocusWithin({ - onFocusWithinChange, - }); - return ( -
-
-
-
- ); - }; - ReactDOM.render(, container); + ReactDOM.render(, container); }); it('is called after "blur" and "focus" events on focus target', () => { @@ -140,28 +141,39 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { expect(onFocusWithinChange).toHaveBeenCalledTimes(2); expect(onFocusWithinChange).toHaveBeenCalledWith(false); }); + + it('is called after a focused element is unmounted', () => { + const target = createEventTarget(innerRef.current); + target.focus(); + expect(onFocusWithinChange).toHaveBeenCalledTimes(1); + expect(onFocusWithinChange).toHaveBeenCalledWith(true); + ReactDOM.render(, container); + expect(onFocusWithinChange).toHaveBeenCalledTimes(2); + expect(onFocusWithinChange).toHaveBeenCalledWith(false); + }); }); describe('onFocusWithinVisibleChange', () => { let onFocusWithinVisibleChange, ref, innerRef, innerRef2; + const Component = ({show}) => { + const listener = useFocusWithin({ + onFocusWithinVisibleChange, + }); + return ( +
+ {show && } +
+
+ ); + }; + beforeEach(() => { onFocusWithinVisibleChange = jest.fn(); ref = React.createRef(); innerRef = React.createRef(); innerRef2 = React.createRef(); - const Component = () => { - const listener = useFocusWithin({ - onFocusWithinVisibleChange, - }); - return ( -
-
-
-
- ); - }; - ReactDOM.render(, container); + ReactDOM.render(, container); }); it('is called after "focus" and "blur" on focus target if keyboard was used', () => { @@ -258,6 +270,18 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false); }); + + it('is called after a focused element is unmounted', () => { + const inner = innerRef.current; + const target = createEventTarget(inner); + target.keydown({key: 'Tab'}); + target.focus(); + expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1); + expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true); + ReactDOM.render(, container); + expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2); + expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false); + }); }); it('expect displayName to show up for event component', () => { diff --git a/packages/react-interactions/events/src/dom/testing-library/index.js b/packages/react-interactions/events/src/dom/testing-library/index.js index d46f96a5058ce..f740a31040aa6 100644 --- a/packages/react-interactions/events/src/dom/testing-library/index.js +++ b/packages/react-interactions/events/src/dom/testing-library/index.js @@ -34,6 +34,7 @@ const createEventTarget = node => ({ }, focus(payload) { node.dispatchEvent(domEvents.focus(payload)); + node.focus(); }, scroll(payload) { node.dispatchEvent(domEvents.scroll(payload));