From 671aacc8e384c9ef150e40c295bb8cce02652fd5 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 24 Jun 2020 11:56:10 +0100 Subject: [PATCH] Modern Event System: use focusin/focusout for onFocus/onBlur Fix test now we use capture phase Cleanup Address feedback Revise --- packages/dom-event-testing-library/domEvents.js | 8 ++++++++ packages/dom-event-testing-library/index.js | 2 ++ .../react-dom/src/events/DOMEventProperties.js | 4 ++-- .../src/events/DOMModernPluginEventSystem.js | 10 +++------- .../react-dom/src/events/DOMTopLevelEventTypes.js | 5 +++-- .../react-dom/src/events/ReactDOMEventReplaying.js | 14 +++++++------- .../DOMModernPluginEventSystem-test.internal.js | 6 +++--- .../events/plugins/ModernBeforeInputEventPlugin.js | 10 +++++----- .../src/events/plugins/ModernChangeEventPlugin.js | 14 +++++++------- .../src/events/plugins/ModernSelectEventPlugin.js | 12 ++++++------ .../src/events/plugins/ModernSimpleEventPlugin.js | 4 ++-- 11 files changed, 48 insertions(+), 41 deletions(-) diff --git a/packages/dom-event-testing-library/domEvents.js b/packages/dom-event-testing-library/domEvents.js index 8c9aff78d3dc1..3ed66046ff522 100644 --- a/packages/dom-event-testing-library/domEvents.js +++ b/packages/dom-event-testing-library/domEvents.js @@ -234,6 +234,10 @@ export function blur({relatedTarget} = {}) { return new FocusEvent('blur', {relatedTarget}); } +export function focusout({relatedTarget} = {}) { + return new FocusEvent('focusout', {relatedTarget, bubbles: true}); +} + export function click(payload) { return createMouseEvent('click', { button: buttonType.primary, @@ -259,6 +263,10 @@ export function focus({relatedTarget} = {}) { return new FocusEvent('focus', {relatedTarget}); } +export function focusin({relatedTarget} = {}) { + return new FocusEvent('focusin', {relatedTarget, bubbles: true}); +} + export function scroll() { return createEvent('scroll'); } diff --git a/packages/dom-event-testing-library/index.js b/packages/dom-event-testing-library/index.js index 0b5ac81a60eaf..d2d257d3dfab8 100644 --- a/packages/dom-event-testing-library/index.js +++ b/packages/dom-event-testing-library/index.js @@ -22,12 +22,14 @@ const createEventTarget = node => ({ */ blur(payload) { node.dispatchEvent(domEvents.blur(payload)); + node.dispatchEvent(domEvents.focusout(payload)); }, click(payload) { node.dispatchEvent(domEvents.click(payload)); }, focus(payload) { node.dispatchEvent(domEvents.focus(payload)); + node.dispatchEvent(domEvents.focusin(payload)); node.focus(); }, keydown(payload) { diff --git a/packages/react-dom/src/events/DOMEventProperties.js b/packages/react-dom/src/events/DOMEventProperties.js index 4ccf4a6359760..c840d903602f6 100644 --- a/packages/react-dom/src/events/DOMEventProperties.js +++ b/packages/react-dom/src/events/DOMEventProperties.js @@ -41,7 +41,6 @@ const eventPriorities = new Map(); // prettier-ignore const discreteEventPairsForSimpleEventPlugin = [ - DOMTopLevelEventTypes.TOP_BLUR, 'blur', DOMTopLevelEventTypes.TOP_CANCEL, 'cancel', DOMTopLevelEventTypes.TOP_CLICK, 'click', DOMTopLevelEventTypes.TOP_CLOSE, 'close', @@ -53,7 +52,8 @@ const discreteEventPairsForSimpleEventPlugin = [ DOMTopLevelEventTypes.TOP_DRAG_END, 'dragEnd', DOMTopLevelEventTypes.TOP_DRAG_START, 'dragStart', DOMTopLevelEventTypes.TOP_DROP, 'drop', - DOMTopLevelEventTypes.TOP_FOCUS, 'focus', + DOMTopLevelEventTypes.TOP_FOCUS_IN, 'focus', + DOMTopLevelEventTypes.TOP_FOCUS_OUT, 'blur', DOMTopLevelEventTypes.TOP_INPUT, 'input', DOMTopLevelEventTypes.TOP_INVALID, 'invalid', DOMTopLevelEventTypes.TOP_KEY_DOWN, 'keyDown', diff --git a/packages/react-dom/src/events/DOMModernPluginEventSystem.js b/packages/react-dom/src/events/DOMModernPluginEventSystem.js index f4a17afb14638..4e8486950904f 100644 --- a/packages/react-dom/src/events/DOMModernPluginEventSystem.js +++ b/packages/react-dom/src/events/DOMModernPluginEventSystem.js @@ -43,12 +43,10 @@ import { import getEventTarget from './getEventTarget'; import { - TOP_FOCUS, TOP_LOAD, TOP_ABORT, TOP_CANCEL, TOP_INVALID, - TOP_BLUR, TOP_SCROLL, TOP_CLOSE, TOP_RESET, @@ -179,8 +177,6 @@ function extractEvents( } export const capturePhaseEvents: Set = new Set([ - TOP_FOCUS, - TOP_BLUR, TOP_SCROLL, TOP_LOAD, TOP_ABORT, @@ -381,7 +377,7 @@ function addTrappedEventListener( targetContainer: EventTarget, topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, - capturePhase: boolean, + capture: boolean, isDeferredListenerForLegacyFBSupport?: boolean, passive?: boolean, priority?: EventPriority, @@ -427,12 +423,12 @@ function addTrappedEventListener( targetContainer, rawEventName, unsubscribeListener, - capturePhase, + capture, ); } }; } - if (capturePhase) { + if (capture) { if (enableCreateEventHandleAPI && passive !== undefined) { unsubscribeListener = addEventCaptureListenerWithPassiveFlag( targetContainer, diff --git a/packages/react-dom/src/events/DOMTopLevelEventTypes.js b/packages/react-dom/src/events/DOMTopLevelEventTypes.js index 86fcb713a4917..140deafc0be06 100644 --- a/packages/react-dom/src/events/DOMTopLevelEventTypes.js +++ b/packages/react-dom/src/events/DOMTopLevelEventTypes.js @@ -32,7 +32,6 @@ export const TOP_ANIMATION_ITERATION = unsafeCastStringToDOMTopLevelType( export const TOP_ANIMATION_START = unsafeCastStringToDOMTopLevelType( getVendorPrefixedEventName('animationstart'), ); -export const TOP_BLUR = unsafeCastStringToDOMTopLevelType('blur'); export const TOP_CAN_PLAY = unsafeCastStringToDOMTopLevelType('canplay'); export const TOP_CAN_PLAY_THROUGH = unsafeCastStringToDOMTopLevelType( 'canplaythrough', @@ -72,7 +71,6 @@ export const TOP_EMPTIED = unsafeCastStringToDOMTopLevelType('emptied'); export const TOP_ENCRYPTED = unsafeCastStringToDOMTopLevelType('encrypted'); export const TOP_ENDED = unsafeCastStringToDOMTopLevelType('ended'); export const TOP_ERROR = unsafeCastStringToDOMTopLevelType('error'); -export const TOP_FOCUS = unsafeCastStringToDOMTopLevelType('focus'); export const TOP_GOT_POINTER_CAPTURE = unsafeCastStringToDOMTopLevelType( 'gotpointercapture', ); @@ -152,6 +150,9 @@ export const TOP_WHEEL = unsafeCastStringToDOMTopLevelType('wheel'); export const TOP_AFTER_BLUR = unsafeCastStringToDOMTopLevelType('afterblur'); export const TOP_BEFORE_BLUR = unsafeCastStringToDOMTopLevelType('beforeblur'); +export const TOP_FOCUS_IN = unsafeCastStringToDOMTopLevelType('focusin'); +export const TOP_FOCUS_OUT = unsafeCastStringToDOMTopLevelType('focusout'); + // List of events that need to be individually attached to media elements. // Note that events in this list will *not* be listened to at the top level // unless they're explicitly listed in `ReactBrowserEventEmitter.listenTo`. diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js index 91eb3f5e06363..78e869de27739 100644 --- a/packages/react-dom/src/events/ReactDOMEventReplaying.js +++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js @@ -131,8 +131,8 @@ import { TOP_POINTER_OUT, TOP_GOT_POINTER_CAPTURE, TOP_LOST_POINTER_CAPTURE, - TOP_FOCUS, - TOP_BLUR, + TOP_FOCUS_IN, + TOP_FOCUS_OUT, } from './DOMTopLevelEventTypes'; import {IS_REPLAYED, PLUGIN_EVENT_SYSTEM} from './EventSystemFlags'; import { @@ -216,10 +216,10 @@ const discreteReplayableEvents = [ ]; const continuousReplayableEvents = [ - TOP_FOCUS, - TOP_BLUR, TOP_DRAG_ENTER, TOP_DRAG_LEAVE, + TOP_FOCUS_IN, + TOP_FOCUS_OUT, TOP_MOUSE_OVER, TOP_MOUSE_OUT, TOP_POINTER_OVER, @@ -362,8 +362,8 @@ export function clearIfContinuousEvent( nativeEvent: AnyNativeEvent, ): void { switch (topLevelType) { - case TOP_FOCUS: - case TOP_BLUR: + case TOP_FOCUS_IN: + case TOP_FOCUS_OUT: queuedFocus = null; break; case TOP_DRAG_ENTER: @@ -443,7 +443,7 @@ export function queueIfContinuousEvent( // moved from outside the window (no target) onto the target once it hydrates. // Instead of mutating we could clone the event. switch (topLevelType) { - case TOP_FOCUS: { + case TOP_FOCUS_IN: { const focusEvent = ((nativeEvent: any): FocusEvent); queuedFocus = accumulateOrCreateContinuousQueuedReplayableEvent( queuedFocus, diff --git a/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js index 9e5b09e8b0785..a3b8eb9eb2fcb 100644 --- a/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js @@ -855,9 +855,9 @@ describe('DOMModernPluginEventSystem', () => { expect(onFocus).toHaveBeenCalledTimes(3); expect(onFocusCapture).toHaveBeenCalledTimes(3); expect(log[2]).toEqual(['capture', buttonElement]); - expect(log[3]).toEqual(['bubble', buttonElement]); - expect(log[4]).toEqual(['capture', divElement]); - expect(log[5]).toEqual(['bubble', divElement]); + expect(log[3]).toEqual(['capture', divElement]); + expect(log[4]).toEqual(['bubble', divElement]); + expect(log[5]).toEqual(['bubble', buttonElement]); }); it('handle propagation of focus events between portals', () => { diff --git a/packages/react-dom/src/events/plugins/ModernBeforeInputEventPlugin.js b/packages/react-dom/src/events/plugins/ModernBeforeInputEventPlugin.js index bf9e9cd806161..43afab2aa84a0 100644 --- a/packages/react-dom/src/events/plugins/ModernBeforeInputEventPlugin.js +++ b/packages/react-dom/src/events/plugins/ModernBeforeInputEventPlugin.js @@ -11,7 +11,7 @@ import {canUseDOM} from 'shared/ExecutionEnvironment'; import {registerTwoPhaseEvent} from '../EventRegistry'; import { - TOP_BLUR, + TOP_FOCUS_OUT, TOP_COMPOSITION_START, TOP_COMPOSITION_END, TOP_COMPOSITION_UPDATE, @@ -66,24 +66,24 @@ function registerEvents() { TOP_PASTE, ]); registerTwoPhaseEvent('onCompositionEnd', [ - TOP_BLUR, TOP_COMPOSITION_END, + TOP_FOCUS_OUT, TOP_KEY_DOWN, TOP_KEY_PRESS, TOP_KEY_UP, TOP_MOUSE_DOWN, ]); registerTwoPhaseEvent('onCompositionStart', [ - TOP_BLUR, TOP_COMPOSITION_START, + TOP_FOCUS_OUT, TOP_KEY_DOWN, TOP_KEY_PRESS, TOP_KEY_UP, TOP_MOUSE_DOWN, ]); registerTwoPhaseEvent('onCompositionUpdate', [ - TOP_BLUR, TOP_COMPOSITION_UPDATE, + TOP_FOCUS_OUT, TOP_KEY_DOWN, TOP_KEY_PRESS, TOP_KEY_UP, @@ -154,7 +154,7 @@ function isFallbackCompositionEnd(topLevelType, nativeEvent) { return nativeEvent.keyCode !== START_KEYCODE; case TOP_KEY_PRESS: case TOP_MOUSE_DOWN: - case TOP_BLUR: + case TOP_FOCUS_OUT: // Events are not possible without cancelling IME. return true; default: diff --git a/packages/react-dom/src/events/plugins/ModernChangeEventPlugin.js b/packages/react-dom/src/events/plugins/ModernChangeEventPlugin.js index 70459b175d825..32415816e5ee2 100644 --- a/packages/react-dom/src/events/plugins/ModernChangeEventPlugin.js +++ b/packages/react-dom/src/events/plugins/ModernChangeEventPlugin.js @@ -17,10 +17,10 @@ import isTextInputElement from '../isTextInputElement'; import {canUseDOM} from 'shared/ExecutionEnvironment'; import { - TOP_BLUR, + TOP_FOCUS_OUT, TOP_CHANGE, TOP_CLICK, - TOP_FOCUS, + TOP_FOCUS_IN, TOP_INPUT, TOP_KEY_DOWN, TOP_KEY_UP, @@ -42,10 +42,10 @@ import { function registerEvents() { registerTwoPhaseEvent('onChange', [ - TOP_BLUR, TOP_CHANGE, TOP_CLICK, - TOP_FOCUS, + TOP_FOCUS_IN, + TOP_FOCUS_OUT, TOP_INPUT, TOP_KEY_DOWN, TOP_KEY_UP, @@ -172,7 +172,7 @@ function handlePropertyChange(nativeEvent) { } function handleEventsForInputEventPolyfill(topLevelType, target, targetInst) { - if (topLevelType === TOP_FOCUS) { + if (topLevelType === TOP_FOCUS_IN) { // In IE9, propertychange fires for most input events but is buggy and // doesn't fire when text is deleted, but conveniently, selectionchange // appears to fire in all of the remaining cases so we catch those and @@ -185,7 +185,7 @@ function handleEventsForInputEventPolyfill(topLevelType, target, targetInst) { // missed a blur event somehow. stopWatchingForValueChange(); startWatchingForValueChange(target, targetInst); - } else if (topLevelType === TOP_BLUR) { + } else if (topLevelType === TOP_FOCUS_OUT) { stopWatchingForValueChange(); } } @@ -304,7 +304,7 @@ function extractEvents( } // When blurring, set the value attribute for number inputs - if (topLevelType === TOP_BLUR) { + if (topLevelType === TOP_FOCUS_OUT) { handleControlledInputBlur(((targetNode: any): HTMLInputElement)); } } diff --git a/packages/react-dom/src/events/plugins/ModernSelectEventPlugin.js b/packages/react-dom/src/events/plugins/ModernSelectEventPlugin.js index dc6cacce37792..08555eb80d052 100644 --- a/packages/react-dom/src/events/plugins/ModernSelectEventPlugin.js +++ b/packages/react-dom/src/events/plugins/ModernSelectEventPlugin.js @@ -12,10 +12,10 @@ import shallowEqual from 'shared/shallowEqual'; import {registerTwoPhaseEvent} from '../EventRegistry'; import { - TOP_BLUR, + TOP_FOCUS_OUT, TOP_CONTEXT_MENU, TOP_DRAG_END, - TOP_FOCUS, + TOP_FOCUS_IN, TOP_KEY_DOWN, TOP_KEY_UP, TOP_MOUSE_DOWN, @@ -35,10 +35,10 @@ const skipSelectionChangeEvent = canUseDOM && 'documentMode' in document && document.documentMode <= 11; const rootTargetDependencies = [ - TOP_BLUR, + TOP_FOCUS_OUT, TOP_CONTEXT_MENU, TOP_DRAG_END, - TOP_FOCUS, + TOP_FOCUS_IN, TOP_KEY_DOWN, TOP_KEY_UP, TOP_MOUSE_DOWN, @@ -186,7 +186,7 @@ function extractEvents( switch (topLevelType) { // Track the input node that has focus. - case TOP_FOCUS: + case TOP_FOCUS_IN: if ( isTextInputElement(targetNode) || targetNode.contentEditable === 'true' @@ -196,7 +196,7 @@ function extractEvents( lastSelection = null; } break; - case TOP_BLUR: + case TOP_FOCUS_OUT: activeElement = null; activeElementInst = null; lastSelection = null; diff --git a/packages/react-dom/src/events/plugins/ModernSimpleEventPlugin.js b/packages/react-dom/src/events/plugins/ModernSimpleEventPlugin.js index be6731082c107..98593c472704c 100644 --- a/packages/react-dom/src/events/plugins/ModernSimpleEventPlugin.js +++ b/packages/react-dom/src/events/plugins/ModernSimpleEventPlugin.js @@ -70,8 +70,8 @@ function extractEvents( case DOMTopLevelEventTypes.TOP_KEY_UP: EventConstructor = SyntheticKeyboardEvent; break; - case DOMTopLevelEventTypes.TOP_BLUR: - case DOMTopLevelEventTypes.TOP_FOCUS: + case DOMTopLevelEventTypes.TOP_FOCUS_IN: + case DOMTopLevelEventTypes.TOP_FOCUS_OUT: case DOMTopLevelEventTypes.TOP_BEFORE_BLUR: case DOMTopLevelEventTypes.TOP_AFTER_BLUR: EventConstructor = SyntheticFocusEvent;