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..f3c83a7ffcb14 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,53 @@ export function insertInContainerBefore( } } +function dispatchCustomFlareEvent( + type: string, + targetInstance: null | Object, + target: Element | Document, +): void { + // Simlulate the custom event to the React Flare responder system. + dispatchEventForResponderEventSystem( + type, + targetInstance, + ({ + 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; + dispatchCustomFlareEvent('beforeactiveelementblur', targtInstance, element); +} + +function dispatchActiveElementBlur( + node: Instance | TextInstance | SuspenseInstance, +): void { + dispatchCustomFlareEvent( + 'activeelementblur', + 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 +519,6 @@ export function removeChild( parentInstance: Instance, child: Instance | TextInstance | SuspenseInstance, ): void { - dispatchDetachedVisibleNodeEvent(child); parentInstance.removeChild(child); } @@ -494,7 +529,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..cc0da6e953029 100644 --- a/packages/react-interactions/events/src/dom/Focus.js +++ b/packages/react-interactions/events/src/dom/Focus.js @@ -50,7 +50,8 @@ type FocusEventType = | 'blur' | 'focuschange' | 'focusvisiblechange' - | 'detachedvisiblenode'; + | 'beforeactiveelementblur' + | 'activeelementblur'; type FocusWithinProps = { disabled?: boolean, @@ -58,7 +59,8 @@ type FocusWithinProps = { onBlurWithin?: (e: FocusEvent) => void, onFocusWithinChange?: boolean => void, onFocusWithinVisibleChange?: boolean => void, - onDetachedVisibleNode?: (e: FocusEvent) => void, + onBeforeActiveElementBlur?: (e: FocusEvent) => void, + onActiveElementBlur?: (e: FocusEvent) => void, }; type FocusWithinEventType = @@ -66,7 +68,8 @@ type FocusWithinEventType = | 'focuswithinchange' | 'blurwithin' | 'focuswithin' - | 'detachedvisiblenode'; + | 'beforeactiveelementblur' + | 'activeelementblur'; /** * Shared between Focus and FocusWithin @@ -79,14 +82,29 @@ const isMac = ? /^Mac/.test(window.navigator.platform) : false; -const targetEventTypes = ['focus', 'blur', 'detachedvisiblenode']; +const targetEventTypes = ['focus', 'blur', 'beforeactiveelementblur']; 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', + 'activeelementblur', + ] + : [ + 'keydown', + 'keyup', + 'mousedown', + 'touchmove', + 'touchstart', + 'touchend', + 'activeelementblur', + ]; function isFunction(obj): boolean { return typeof obj === 'function'; @@ -514,18 +532,18 @@ const focusWithinResponderImpl = { } break; } - case 'detachedvisiblenode': { - const onDetachedVisibleNode = (props.onDetachedVisibleNode: any); - if (isFunction(onDetachedVisibleNode)) { + case 'beforeactiveelementblur': { + const onBeforeActiveElementBlur = (props.onBeforeActiveElementBlur: any); + if (isFunction(onBeforeActiveElementBlur)) { const syntheticEvent = createFocusEvent( context, - 'detachedvisiblenode', + 'beforeactiveelementblur', event.target, state.pointerType, ); context.dispatchEvent( syntheticEvent, - onDetachedVisibleNode, + onBeforeActiveElementBlur, DiscreteEvent, ); } @@ -538,6 +556,23 @@ const focusWithinResponderImpl = { props: FocusWithinProps, state: FocusState, ): void { + if (event.type === 'activeelementblur') { + const onActiveElementBlur = (props.onActiveElementBlur: any); + if (isFunction(onActiveElementBlur)) { + const syntheticEvent = createFocusEvent( + context, + 'activeelementblur', + event.target, + state.pointerType, + ); + context.dispatchEvent( + syntheticEvent, + onActiveElementBlur, + 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..70aac0f36bad4 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 ( -