diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index 4a525677bd214..dbd86245516ab 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -465,3 +465,7 @@ export function unmountFundamentalComponent(fundamentalInstance) { export function getInstanceFromNode(node) { throw new Error('Not yet implemented.'); } + +export function beforeRemoveInstance(instance) { + // noop +} diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 5cda5e1ec48ef..2d4d9ba109d3e 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -104,6 +104,12 @@ export type ChildSet = void; // Unused export type TimeoutHandle = TimeoutID; export type NoTimeout = -1; +type SelectionInformation = {| + blurredActiveElement: null | HTMLElement, + focusedElem: null | HTMLElement, + selectionRange: mixed, +|}; + import { enableSuspenseServerRenderer, enableFlareAPI, @@ -127,7 +133,7 @@ const SUSPENSE_FALLBACK_START_DATA = '$!'; const STYLE = 'style'; let eventsEnabled: ?boolean = null; -let selectionInformation: ?mixed = null; +let selectionInformation: null | SelectionInformation = null; function shouldAutoFocusHostComponent(type: string, props: Props): boolean { switch (type) { @@ -205,6 +211,13 @@ 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); + } + } selectionInformation = null; ReactBrowserEventEmitterSetEnabled(eventsEnabled); eventsEnabled = null; @@ -452,30 +465,50 @@ export function insertInContainerBefore( } } +function dispatchFlareDetachedBlurEvent( + elementDetached: boolean, + targetInstance: null | Object, + target: Element | Document, +): void { + // Simlulate the custom event to the React Flare responder system. + dispatchEventForResponderEventSystem( + 'blur', + targetInstance, + ({ + elementDetached, + target, + timeStamp: Date.now(), + }: any), + target, + RESPONDER_EVENT_SYSTEM | IS_PASSIVE, + ); +} + +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)); +} + // 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, +export function beforeRemoveInstance( + instance: Instance | TextInstance | SuspenseInstance, ): void { if ( enableFlareAPI && selectionInformation && - child === selectionInformation.focusedElem + instance === selectionInformation.focusedElem ) { - const targetFiber = getClosestInstanceFromNode(child); - // Simlulate a blur event to the React Flare responder system. - dispatchEventForResponderEventSystem( - 'detachedvisiblenode', - targetFiber, - ({ - target: child, - timeStamp: Date.now(), - }: any), - ((child: any): Document | Element), - RESPONDER_EVENT_SYSTEM | IS_PASSIVE, - ); + dispatchBeforeActiveElementBlur(((instance: any): HTMLElement)); } } @@ -483,7 +516,6 @@ export function removeChild( parentInstance: Instance, child: Instance | TextInstance | SuspenseInstance, ): void { - dispatchDetachedVisibleNodeEvent(child); parentInstance.removeChild(child); } @@ -494,7 +526,6 @@ export function removeChildFromContainer( if (container.nodeType === COMMENT_NODE) { (container.parentNode: any).removeChild(child); } else { - dispatchDetachedVisibleNodeEvent(child); container.removeChild(child); } } diff --git a/packages/react-dom/src/client/ReactInputSelection.js b/packages/react-dom/src/client/ReactInputSelection.js index f2d0987608b3e..cf413367f7a67 100644 --- a/packages/react-dom/src/client/ReactInputSelection.js +++ b/packages/react-dom/src/client/ReactInputSelection.js @@ -100,6 +100,8 @@ export function hasSelectionCapabilities(elem) { export function getSelectionInformation() { const focusedElem = getActiveElementDeep(); return { + // Used by Flare + blurredActiveElement: 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 1c7d261fc4686..9886011c66bf1 100644 --- a/packages/react-interactions/events/src/dom/Focus.js +++ b/packages/react-interactions/events/src/dom/Focus.js @@ -45,12 +45,7 @@ type FocusProps = { onFocusVisibleChange: boolean => void, }; -type FocusEventType = - | 'focus' - | 'blur' - | 'focuschange' - | 'focusvisiblechange' - | 'detachedvisiblenode'; +type FocusEventType = 'focus' | 'blur' | 'focuschange' | 'focusvisiblechange'; type FocusWithinProps = { disabled?: boolean, @@ -58,7 +53,8 @@ type FocusWithinProps = { onBlurWithin?: (e: FocusEvent) => void, onFocusWithinChange?: boolean => void, onFocusWithinVisibleChange?: boolean => void, - onDetachedVisibleNode?: (e: FocusEvent) => void, + onBeforeFocusedElementDetached?: (e: FocusEvent) => void, + onFocusedElementDetached?: (e: FocusEvent) => void, }; type FocusWithinEventType = @@ -66,7 +62,8 @@ type FocusWithinEventType = | 'focuswithinchange' | 'blurwithin' | 'focuswithin' - | 'detachedvisiblenode'; + | 'focusedelementdetached' + | 'beforefocusedelementdetached'; /** * Shared between Focus and FocusWithin @@ -79,14 +76,22 @@ const isMac = ? /^Mac/.test(window.navigator.platform) : false; -const targetEventTypes = ['focus', 'blur', 'detachedvisiblenode']; +const targetEventTypes = ['focus', 'blur']; const hasPointerEvents = typeof window !== 'undefined' && window.PointerEvent != null; const rootEventTypes = hasPointerEvents - ? ['keydown', 'keyup', 'pointermove', 'pointerdown', 'pointerup'] - : ['keydown', 'keyup', 'mousedown', 'touchmove', 'touchstart', 'touchend']; + ? ['keydown', 'keyup', 'pointermove', 'pointerdown', 'pointerup', 'blur'] + : [ + 'keydown', + 'keyup', + 'mousedown', + 'touchmove', + 'touchstart', + 'touchend', + 'blur', + ]; function isFunction(obj): boolean { return typeof obj === 'function'; @@ -504,6 +509,23 @@ 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) @@ -514,22 +536,6 @@ 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( @@ -538,6 +544,23 @@ 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, + ); + } + return; + } handleRootEvent(event, context, state, isFocusVisible => { if (state.isFocused && state.isFocusVisible !== isFocusVisible) { state.isFocusVisible = isFocusVisible; 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 d90a95b8413d8..388bce20f2b27 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,37 +262,77 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { }); }); - describe('onDetachedVisibleNode', () => { - let onDetachedVisibleNode, ref, innerRef, innerRef2; - - const Component = ({show}) => { - const listener = useFocusWithin({ - onDetachedVisibleNode, - }); - return ( -