From e701632ad4112038c4dd8e7302b90c0bb3fc4f2b Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 7 Nov 2019 10:27:02 +0000 Subject: [PATCH] [react-interactions] Change unmount blur logic to a dedicated event (#17291) --- .../src/client/ReactDOMHostConfig.js | 14 ++++--- .../events/src/dom/Focus.js | 29 ++++++++++++-- .../__tests__/FocusWithin-test.internal.js | 40 ++++++++++++------- 3 files changed, 60 insertions(+), 23 deletions(-) diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index e0ee9f4e1d..5cda5e1ec4 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -452,7 +452,11 @@ export function insertInContainerBefore( } } -function handleSimulateChildBlur( +// This is a specific event for the React Flare +// event system, so event responders can act +// accordingly to a DOM node being unmounted that +// previously had active document focus. +function dispatchDetachedVisibleNodeEvent( child: Instance | TextInstance | SuspenseInstance, ): void { if ( @@ -463,13 +467,11 @@ function handleSimulateChildBlur( const targetFiber = getClosestInstanceFromNode(child); // Simlulate a blur event to the React Flare responder system. dispatchEventForResponderEventSystem( - 'blur', + 'detachedvisiblenode', targetFiber, ({ - relatedTarget: null, target: child, timeStamp: Date.now(), - type: 'blur', }: any), ((child: any): Document | Element), RESPONDER_EVENT_SYSTEM | IS_PASSIVE, @@ -481,7 +483,7 @@ export function removeChild( parentInstance: Instance, child: Instance | TextInstance | SuspenseInstance, ): void { - handleSimulateChildBlur(child); + dispatchDetachedVisibleNodeEvent(child); parentInstance.removeChild(child); } @@ -492,7 +494,7 @@ export function removeChildFromContainer( if (container.nodeType === COMMENT_NODE) { (container.parentNode: any).removeChild(child); } else { - handleSimulateChildBlur(child); + dispatchDetachedVisibleNodeEvent(child); container.removeChild(child); } } diff --git a/packages/react-interactions/events/src/dom/Focus.js b/packages/react-interactions/events/src/dom/Focus.js index 5084ce1e5f..1c7d261fc4 100644 --- a/packages/react-interactions/events/src/dom/Focus.js +++ b/packages/react-interactions/events/src/dom/Focus.js @@ -45,7 +45,12 @@ type FocusProps = { onFocusVisibleChange: boolean => void, }; -type FocusEventType = 'focus' | 'blur' | 'focuschange' | 'focusvisiblechange'; +type FocusEventType = + | 'focus' + | 'blur' + | 'focuschange' + | 'focusvisiblechange' + | 'detachedvisiblenode'; type FocusWithinProps = { disabled?: boolean, @@ -53,13 +58,15 @@ type FocusWithinProps = { onBlurWithin?: (e: FocusEvent) => void, onFocusWithinChange?: boolean => void, onFocusWithinVisibleChange?: boolean => void, + onDetachedVisibleNode?: (e: FocusEvent) => void, }; type FocusWithinEventType = | 'focuswithinvisiblechange' | 'focuswithinchange' | 'blurwithin' - | 'focuswithin'; + | 'focuswithin' + | 'detachedvisiblenode'; /** * Shared between Focus and FocusWithin @@ -72,7 +79,7 @@ const isMac = ? /^Mac/.test(window.navigator.platform) : false; -const targetEventTypes = ['focus', 'blur']; +const targetEventTypes = ['focus', 'blur', 'detachedvisiblenode']; const hasPointerEvents = typeof window !== 'undefined' && window.PointerEvent != null; @@ -507,6 +514,22 @@ const focusWithinResponderImpl = { } break; } + case 'detachedvisiblenode': { + const onDetachedVisibleNode = (props.onDetachedVisibleNode: any); + if (isFunction(onDetachedVisibleNode)) { + const syntheticEvent = createFocusEvent( + context, + 'detachedvisiblenode', + event.target, + state.pointerType, + ); + context.dispatchEvent( + syntheticEvent, + onDetachedVisibleNode, + DiscreteEvent, + ); + } + } } }, onRootEvent( 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 12a2d7f03a..d90a95b841 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 @@ -141,16 +141,6 @@ 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', () => { @@ -270,17 +260,39 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false); }); + }); + + describe('onDetachedVisibleNode', () => { + let onDetachedVisibleNode, ref, innerRef, innerRef2; + + const Component = ({show}) => { + const listener = useFocusWithin({ + onDetachedVisibleNode, + }); + return ( +
+ {show && } +
+
+ ); + }; + + beforeEach(() => { + onDetachedVisibleNode = jest.fn(); + ref = React.createRef(); + innerRef = React.createRef(); + innerRef2 = React.createRef(); + ReactDOM.render(, container); + }); 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); + expect(onDetachedVisibleNode).toHaveBeenCalledTimes(0); ReactDOM.render(, container); - expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2); - expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false); + expect(onDetachedVisibleNode).toHaveBeenCalledTimes(1); }); });