From fc43644eba08256464edb125adce2027cdbdca30 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 14 Nov 2019 11:55:50 +0000 Subject: [PATCH] [react-interactions] Follow up active element blur logic (#17364) --- .../src/client/ReactDOMHostConfig.js | 48 ++++++------ .../src/client/ReactInputSelection.js | 2 +- .../events/src/dom/Focus.js | 73 ++++++++++--------- .../__tests__/FocusWithin-test.internal.js | 42 ++++++----- 4 files changed, 84 insertions(+), 81 deletions(-) diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 2d4d9ba109..9d72ced2d9 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -105,7 +105,7 @@ export type TimeoutHandle = TimeoutID; export type NoTimeout = -1; type SelectionInformation = {| - blurredActiveElement: null | HTMLElement, + activeElementDetached: null | HTMLElement, focusedElem: null | HTMLElement, selectionRange: mixed, |}; @@ -212,10 +212,10 @@ export function prepareForCommit(containerInfo: Container): void { export function resetAfterCommit(containerInfo: Container): void { restoreSelection(selectionInformation); if (enableFlareAPI) { - const blurredActiveElement = (selectionInformation: any) - .blurredActiveElement; - if (blurredActiveElement !== null) { - dispatchActiveElementBlur(blurredActiveElement); + const activeElementDetached = (selectionInformation: any) + .activeElementDetached; + if (activeElementDetached !== null) { + dispatchDetachedBlur(activeElementDetached); } } selectionInformation = null; @@ -465,17 +465,14 @@ export function insertInContainerBefore( } } -function dispatchFlareDetachedBlurEvent( - elementDetached: boolean, - targetInstance: null | Object, - target: Element | Document, -): void { - // Simlulate the custom event to the React Flare responder system. +function dispatchBeforeDetachedBlur(target: HTMLElement): void { + const targetInstance = getClosestInstanceFromNode(target); + ((selectionInformation: any): SelectionInformation).activeElementDetached = target; + dispatchEventForResponderEventSystem( - 'blur', + 'beforeblur', targetInstance, ({ - elementDetached, target, timeStamp: Date.now(), }: any), @@ -484,16 +481,19 @@ function dispatchFlareDetachedBlurEvent( ); } -function dispatchBeforeActiveElementBlur(element: HTMLElement): void { - const targtInstance = getClosestInstanceFromNode(element); - ((selectionInformation: any): SelectionInformation).blurredActiveElement = element; - dispatchFlareDetachedBlurEvent(false, targtInstance, element); -} - -function dispatchActiveElementBlur( - node: Instance | TextInstance | SuspenseInstance, -): void { - dispatchFlareDetachedBlurEvent(true, null, ((node: any): HTMLElement)); +function dispatchDetachedBlur(target: HTMLElement): void { + const targetInstance = getClosestInstanceFromNode(target); + dispatchEventForResponderEventSystem( + 'blur', + targetInstance, + ({ + isTargetAttached: false, + target, + timeStamp: Date.now(), + }: any), + target, + RESPONDER_EVENT_SYSTEM | IS_PASSIVE, + ); } // This is a specific event for the React Flare @@ -508,7 +508,7 @@ export function beforeRemoveInstance( selectionInformation && instance === selectionInformation.focusedElem ) { - dispatchBeforeActiveElementBlur(((instance: any): HTMLElement)); + dispatchBeforeDetachedBlur(((instance: any): HTMLElement)); } } diff --git a/packages/react-dom/src/client/ReactInputSelection.js b/packages/react-dom/src/client/ReactInputSelection.js index cf413367f7..ec785d0346 100644 --- a/packages/react-dom/src/client/ReactInputSelection.js +++ b/packages/react-dom/src/client/ReactInputSelection.js @@ -101,7 +101,7 @@ export function getSelectionInformation() { const focusedElem = getActiveElementDeep(); return { // Used by Flare - blurredActiveElement: null, + activeElementDetached: null, focusedElem: focusedElem, selectionRange: hasSelectionCapabilities(focusedElem) ? getSelection(focusedElem) diff --git a/packages/react-interactions/events/src/dom/Focus.js b/packages/react-interactions/events/src/dom/Focus.js index 9886011c66..8d344b36de 100644 --- a/packages/react-interactions/events/src/dom/Focus.js +++ b/packages/react-interactions/events/src/dom/Focus.js @@ -22,6 +22,7 @@ import {DiscreteEvent} from 'shared/ReactTypes'; */ type FocusEvent = {| + isTargetAttached: boolean, target: Element | Document, type: FocusEventType | FocusWithinEventType, pointerType: PointerType, @@ -30,6 +31,7 @@ type FocusEvent = {| |}; type FocusState = { + detachedTarget: null | Element | Document, focusTarget: null | Element | Document, isFocused: boolean, isFocusVisible: boolean, @@ -50,11 +52,10 @@ type FocusEventType = 'focus' | 'blur' | 'focuschange' | 'focusvisiblechange'; type FocusWithinProps = { disabled?: boolean, onFocusWithin?: (e: FocusEvent) => void, + onBeforeBlurWithin?: (e: FocusEvent) => void, onBlurWithin?: (e: FocusEvent) => void, onFocusWithinChange?: boolean => void, onFocusWithinVisibleChange?: boolean => void, - onBeforeFocusedElementDetached?: (e: FocusEvent) => void, - onFocusedElementDetached?: (e: FocusEvent) => void, }; type FocusWithinEventType = @@ -62,8 +63,7 @@ type FocusWithinEventType = | 'focuswithinchange' | 'blurwithin' | 'focuswithin' - | 'focusedelementdetached' - | 'beforefocusedelementdetached'; + | 'beforeblurwithin'; /** * Shared between Focus and FocusWithin @@ -76,7 +76,7 @@ const isMac = ? /^Mac/.test(window.navigator.platform) : false; -const targetEventTypes = ['focus', 'blur']; +const targetEventTypes = ['focus', 'blur', 'beforeblur']; const hasPointerEvents = typeof window !== 'undefined' && window.PointerEvent != null; @@ -102,8 +102,10 @@ function createFocusEvent( type: FocusEventType | FocusWithinEventType, target: Element | Document, pointerType: PointerType, + isTargetAttached: boolean, ): FocusEvent { return { + isTargetAttached, target, type, pointerType, @@ -218,6 +220,7 @@ function dispatchFocusEvents( 'focus', target, pointerType, + true, ); context.dispatchEvent(syntheticEvent, onFocus, DiscreteEvent); } @@ -241,6 +244,7 @@ function dispatchBlurEvents( 'blur', target, pointerType, + true, ); context.dispatchEvent(syntheticEvent, onBlur, DiscreteEvent); } @@ -265,6 +269,7 @@ function dispatchFocusWithinEvents( 'focuswithin', target, pointerType, + true, ); context.dispatchEvent(syntheticEvent, onFocusWithin, DiscreteEvent); } @@ -279,12 +284,14 @@ function dispatchBlurWithinEvents( const pointerType = state.pointerType; const target = ((state.focusTarget: any): Element | Document) || event.target; const onBlurWithin = (props.onBlurWithin: any); + const isTargetAttached = state.detachedTarget === null; if (isFunction(onBlurWithin)) { const syntheticEvent = createFocusEvent( context, 'blurwithin', target, pointerType, + isTargetAttached, ); context.dispatchEvent(syntheticEvent, onBlurWithin, DiscreteEvent); } @@ -328,6 +335,7 @@ const focusResponderImpl = { rootEventTypes, getInitialState(): FocusState { return { + detachedTarget: null, focusTarget: null, isEmulatingMouseEvents: false, isFocused: false, @@ -466,6 +474,7 @@ const focusWithinResponderImpl = { rootEventTypes, getInitialState(): FocusState { return { + detachedTarget: null, focusTarget: null, isEmulatingMouseEvents: false, isFocused: false, @@ -509,23 +518,6 @@ const focusWithinResponderImpl = { break; } case 'blur': { - if ((nativeEvent: any).elementDetached === false) { - const onBeforeFocusedElementDetached = (props.onBeforeFocusedElementDetached: any); - if (isFunction(onBeforeFocusedElementDetached)) { - const syntheticEvent = createFocusEvent( - context, - 'beforefocusedelementdetached', - event.target, - state.pointerType, - ); - context.dispatchEvent( - syntheticEvent, - onBeforeFocusedElementDetached, - DiscreteEvent, - ); - } - return; - } if ( state.isFocused && !context.isTargetWithinResponder(relatedTarget) @@ -536,6 +528,24 @@ const focusWithinResponderImpl = { } break; } + case 'beforeblur': { + const onBeforeBlurWithin = (props.onBeforeBlurWithin: any); + if (isFunction(onBeforeBlurWithin)) { + const syntheticEvent = createFocusEvent( + context, + 'beforeblurwithin', + event.target, + state.pointerType, + true, + ); + state.detachedTarget = event.target; + context.dispatchEvent( + syntheticEvent, + onBeforeBlurWithin, + DiscreteEvent, + ); + } + } } }, onRootEvent( @@ -544,20 +554,11 @@ const focusWithinResponderImpl = { props: FocusWithinProps, state: FocusState, ): void { - if ((event.nativeEvent: any).elementDetached === true) { - const onFocusedElementDetached = (props.onFocusedElementDetached: any); - if (isFunction(onFocusedElementDetached)) { - const syntheticEvent = createFocusEvent( - context, - 'focusedelementdetached', - event.target, - state.pointerType, - ); - context.dispatchEvent( - syntheticEvent, - onFocusedElementDetached, - DiscreteEvent, - ); + if (event.type === 'blur') { + const detachedTarget = state.detachedTarget; + if (detachedTarget !== null && detachedTarget === event.target) { + dispatchBlurWithinEvents(context, event, props, state); + state.detachedTarget = null; } return; } 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 388bce20f2..f5167db52e 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 @@ -262,16 +262,12 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { }); }); - describe('onBeforeFocusedElementDetached/onFocusedElementDetached', () => { - let onBeforeFocusedElementDetached, - onFocusedElementDetached, - ref, - innerRef, - innerRef2; + describe('onBeforeBlurWithin', () => { + let onBeforeBlurWithin, onBlurWithin, ref, innerRef, innerRef2; beforeEach(() => { - onBeforeFocusedElementDetached = jest.fn(); - onFocusedElementDetached = jest.fn(); + onBeforeBlurWithin = jest.fn(); + onBlurWithin = jest.fn(); ref = React.createRef(); innerRef = React.createRef(); innerRef2 = React.createRef(); @@ -280,8 +276,8 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { it('is called after a focused element is unmounted', () => { const Component = ({show}) => { const listener = useFocusWithin({ - onBeforeFocusedElementDetached, - onFocusedElementDetached, + onBeforeBlurWithin, + onBlurWithin, }); return (
@@ -297,18 +293,21 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { const target = createEventTarget(inner); target.keydown({key: 'Tab'}); target.focus(); - expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(0); - expect(onFocusedElementDetached).toHaveBeenCalledTimes(0); + expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0); + expect(onBlurWithin).toHaveBeenCalledTimes(0); ReactDOM.render(, container); - expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(1); - expect(onFocusedElementDetached).toHaveBeenCalledTimes(1); + expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1); + expect(onBlurWithin).toHaveBeenCalledTimes(1); + expect(onBlurWithin).toHaveBeenCalledWith( + expect.objectContaining({isTargetAttached: false}), + ); }); it('is called after a nested focused element is unmounted', () => { const Component = ({show}) => { const listener = useFocusWithin({ - onBeforeFocusedElementDetached, - onFocusedElementDetached, + onBeforeBlurWithin, + onBlurWithin, }); return (
@@ -328,11 +327,14 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { const target = createEventTarget(inner); target.keydown({key: 'Tab'}); target.focus(); - expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(0); - expect(onFocusedElementDetached).toHaveBeenCalledTimes(0); + expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0); + expect(onBlurWithin).toHaveBeenCalledTimes(0); ReactDOM.render(, container); - expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(1); - expect(onFocusedElementDetached).toHaveBeenCalledTimes(1); + expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1); + expect(onBlurWithin).toHaveBeenCalledTimes(1); + expect(onBlurWithin).toHaveBeenCalledWith( + expect.objectContaining({isTargetAttached: false}), + ); }); });