diff --git a/packages/react-dom/src/events/SyntheticEvent.js b/packages/react-dom/src/events/SyntheticEvent.js index 717960b8dd..649cab082e 100644 --- a/packages/react-dom/src/events/SyntheticEvent.js +++ b/packages/react-dom/src/events/SyntheticEvent.js @@ -15,11 +15,131 @@ type EventInterfaceType = { [propName: string]: 0 | ((event: {[propName: string]: mixed}) => mixed), }; +function functionThatReturnsTrue() { + return true; +} + +function functionThatReturnsFalse() { + return false; +} + +// This is intentionally a factory so that we have different returned constructors. +// If we had a single constructor, it would be megamorphic and engines would deopt. +function createSyntheticEvent(Interface: EventInterfaceType) { + /** + * Synthetic events are dispatched by event plugins, typically in response to a + * top-level event delegation handler. + * + * These systems should generally use pooling to reduce the frequency of garbage + * collection. The system should check `isPersistent` to determine whether the + * event should be released into the pool after being dispatched. Users that + * need a persisted event should invoke `persist`. + * + * Synthetic events (and subclasses) implement the DOM Level 3 Events API by + * normalizing browser quirks. Subclasses do not necessarily have to implement a + * DOM interface; custom application-specific events can also subclass this. + */ + function SyntheticBaseEvent( + reactName: string | null, + reactEventType: string, + targetInst: Fiber, + nativeEvent: {[propName: string]: mixed}, + nativeEventTarget: null | EventTarget, + ) { + this._reactName = reactName; + this._targetInst = targetInst; + this.type = reactEventType; + this.nativeEvent = nativeEvent; + this.target = nativeEventTarget; + this.currentTarget = null; + + for (const propName in Interface) { + if (!Interface.hasOwnProperty(propName)) { + continue; + } + const normalize = Interface[propName]; + if (normalize) { + this[propName] = normalize(nativeEvent); + } else { + this[propName] = nativeEvent[propName]; + } + } + + const defaultPrevented = + nativeEvent.defaultPrevented != null + ? nativeEvent.defaultPrevented + : nativeEvent.returnValue === false; + if (defaultPrevented) { + this.isDefaultPrevented = functionThatReturnsTrue; + } else { + this.isDefaultPrevented = functionThatReturnsFalse; + } + this.isPropagationStopped = functionThatReturnsFalse; + return this; + } + + Object.assign(SyntheticBaseEvent.prototype, { + preventDefault: function() { + this.defaultPrevented = true; + const event = this.nativeEvent; + if (!event) { + return; + } + + if (event.preventDefault) { + event.preventDefault(); + // $FlowFixMe - flow is not aware of `unknown` in IE + } else if (typeof event.returnValue !== 'unknown') { + event.returnValue = false; + } + this.isDefaultPrevented = functionThatReturnsTrue; + }, + + stopPropagation: function() { + const event = this.nativeEvent; + if (!event) { + return; + } + + if (event.stopPropagation) { + event.stopPropagation(); + // $FlowFixMe - flow is not aware of `unknown` in IE + } else if (typeof event.cancelBubble !== 'unknown') { + // The ChangeEventPlugin registers a "propertychange" event for + // IE. This event does not support bubbling or cancelling, and + // any references to cancelBubble throw "Member not found". A + // typeof check of "unknown" circumvents this issue (and is also + // IE specific). + event.cancelBubble = true; + } + + this.isPropagationStopped = functionThatReturnsTrue; + }, + + /** + * We release all dispatched `SyntheticEvent`s after each event loop, adding + * them back into the pool. This allows a way to hold onto a reference that + * won't be added back into the pool. + */ + persist: function() { + // Modern event system doesn't use pooling. + }, + + /** + * Checks if this event should be released back into the pool. + * + * @return {boolean} True if this should not be released, false otherwise. + */ + isPersistent: functionThatReturnsTrue, + }); + return SyntheticBaseEvent; +} + /** * @interface Event * @see http://www.w3.org/TR/DOM-Level-3-Events/ */ -const EventInterface: EventInterfaceType = { +const EventInterface = { eventPhase: 0, bubbles: 0, cancelable: 0, @@ -29,128 +149,14 @@ const EventInterface: EventInterfaceType = { defaultPrevented: 0, isTrusted: 0, }; +export const SyntheticEvent = createSyntheticEvent(EventInterface); -function functionThatReturnsTrue() { - return true; -} - -function functionThatReturnsFalse() { - return false; -} - -/** - * Synthetic events are dispatched by event plugins, typically in response to a - * top-level event delegation handler. - * - * These systems should generally use pooling to reduce the frequency of garbage - * collection. The system should check `isPersistent` to determine whether the - * event should be released into the pool after being dispatched. Users that - * need a persisted event should invoke `persist`. - * - * Synthetic events (and subclasses) implement the DOM Level 3 Events API by - * normalizing browser quirks. Subclasses do not necessarily have to implement a - * DOM interface; custom application-specific events can also subclass this. - */ -export function SyntheticEvent( - reactName: string | null, - reactEventType: string, - targetInst: Fiber, - nativeEvent: {[propName: string]: mixed}, - nativeEventTarget: null | EventTarget, - Interface: EventInterfaceType = EventInterface, -) { - this._reactName = reactName; - this._targetInst = targetInst; - this.type = reactEventType; - this.nativeEvent = nativeEvent; - this.target = nativeEventTarget; - this.currentTarget = null; - - for (const propName in Interface) { - if (!Interface.hasOwnProperty(propName)) { - continue; - } - const normalize = Interface[propName]; - if (normalize) { - this[propName] = normalize(nativeEvent); - } else { - this[propName] = nativeEvent[propName]; - } - } - - const defaultPrevented = - nativeEvent.defaultPrevented != null - ? nativeEvent.defaultPrevented - : nativeEvent.returnValue === false; - if (defaultPrevented) { - this.isDefaultPrevented = functionThatReturnsTrue; - } else { - this.isDefaultPrevented = functionThatReturnsFalse; - } - this.isPropagationStopped = functionThatReturnsFalse; - return this; -} - -Object.assign(SyntheticEvent.prototype, { - preventDefault: function() { - this.defaultPrevented = true; - const event = this.nativeEvent; - if (!event) { - return; - } - - if (event.preventDefault) { - event.preventDefault(); - // $FlowFixMe - flow is not aware of `unknown` in IE - } else if (typeof event.returnValue !== 'unknown') { - event.returnValue = false; - } - this.isDefaultPrevented = functionThatReturnsTrue; - }, - - stopPropagation: function() { - const event = this.nativeEvent; - if (!event) { - return; - } - - if (event.stopPropagation) { - event.stopPropagation(); - // $FlowFixMe - flow is not aware of `unknown` in IE - } else if (typeof event.cancelBubble !== 'unknown') { - // The ChangeEventPlugin registers a "propertychange" event for - // IE. This event does not support bubbling or cancelling, and - // any references to cancelBubble throw "Member not found". A - // typeof check of "unknown" circumvents this issue (and is also - // IE specific). - event.cancelBubble = true; - } - - this.isPropagationStopped = functionThatReturnsTrue; - }, - - /** - * We release all dispatched `SyntheticEvent`s after each event loop, adding - * them back into the pool. This allows a way to hold onto a reference that - * won't be added back into the pool. - */ - persist: function() { - // Modern event system doesn't use pooling. - }, - - /** - * Checks if this event should be released back into the pool. - * - * @return {boolean} True if this should not be released, false otherwise. - */ - isPersistent: functionThatReturnsTrue, -}); - -export const UIEventInterface: EventInterfaceType = { +const UIEventInterface: EventInterfaceType = { ...EventInterface, view: 0, detail: 0, }; +export const SyntheticUIEvent = createSyntheticEvent(UIEventInterface); let lastMovementX; let lastMovementY; @@ -173,7 +179,7 @@ function updateMouseMovementPolyfillState(event) { * @interface MouseEvent * @see http://www.w3.org/TR/DOM-Level-3-Events/ */ -export const MouseEventInterface: EventInterfaceType = { +const MouseEventInterface: EventInterfaceType = { ...UIEventInterface, screenX: 0, screenY: 0, @@ -213,42 +219,48 @@ export const MouseEventInterface: EventInterfaceType = { return lastMovementY; }, }; +export const SyntheticMouseEvent = createSyntheticEvent(MouseEventInterface); /** * @interface DragEvent * @see http://www.w3.org/TR/DOM-Level-3-Events/ */ -export const DragEventInterface: EventInterfaceType = { +const DragEventInterface: EventInterfaceType = { ...MouseEventInterface, dataTransfer: 0, }; +export const SyntheticDragEvent = createSyntheticEvent(DragEventInterface); /** * @interface FocusEvent * @see http://www.w3.org/TR/DOM-Level-3-Events/ */ -export const FocusEventInterface: EventInterfaceType = { +const FocusEventInterface: EventInterfaceType = { ...UIEventInterface, relatedTarget: 0, }; +export const SyntheticFocusEvent = createSyntheticEvent(FocusEventInterface); /** * @interface Event * @see http://www.w3.org/TR/css3-animations/#AnimationEvent-interface * @see https://developer.mozilla.org/en-US/docs/Web/API/AnimationEvent */ -export const AnimationEventInterface: EventInterfaceType = { +const AnimationEventInterface: EventInterfaceType = { ...EventInterface, animationName: 0, elapsedTime: 0, pseudoElement: 0, }; +export const SyntheticAnimationEvent = createSyntheticEvent( + AnimationEventInterface, +); /** * @interface Event * @see http://www.w3.org/TR/clipboard-apis/ */ -export const ClipboardEventInterface: EventInterfaceType = { +const ClipboardEventInterface: EventInterfaceType = { ...EventInterface, clipboardData: function(event) { return 'clipboardData' in event @@ -256,15 +268,21 @@ export const ClipboardEventInterface: EventInterfaceType = { : window.clipboardData; }, }; +export const SyntheticClipboardEvent = createSyntheticEvent( + ClipboardEventInterface, +); /** * @interface Event * @see http://www.w3.org/TR/DOM-Level-3-Events/#events-compositionevents */ -export const CompositionEventInterface: EventInterfaceType = { +const CompositionEventInterface: EventInterfaceType = { ...EventInterface, data: 0, }; +export const SyntheticCompositionEvent = createSyntheticEvent( + CompositionEventInterface, +); /** * @interface Event @@ -272,7 +290,7 @@ export const CompositionEventInterface: EventInterfaceType = { * /#events-inputevents */ // Happens to share the same list for now. -export const InputEventInterface: EventInterfaceType = CompositionEventInterface; +export const SyntheticInputEvent = SyntheticCompositionEvent; /** * Normalization of deprecated HTML5 `key` values @@ -402,7 +420,7 @@ function getEventModifierState(nativeEvent) { * @interface KeyboardEvent * @see http://www.w3.org/TR/DOM-Level-3-Events/ */ -export const KeyboardEventInterface = { +const KeyboardEventInterface = { ...UIEventInterface, key: getEventKey, code: 0, @@ -451,12 +469,15 @@ export const KeyboardEventInterface = { return 0; }, }; +export const SyntheticKeyboardEvent = createSyntheticEvent( + KeyboardEventInterface, +); /** * @interface PointerEvent * @see http://www.w3.org/TR/pointerevents/ */ -export const PointerEventInterface = { +const PointerEventInterface = { ...MouseEventInterface, pointerId: 0, width: 0, @@ -469,12 +490,15 @@ export const PointerEventInterface = { pointerType: 0, isPrimary: 0, }; +export const SyntheticPointerEvent = createSyntheticEvent( + PointerEventInterface, +); /** * @interface TouchEvent * @see http://www.w3.org/TR/touch-events/ */ -export const TouchEventInterface = { +const TouchEventInterface = { ...UIEventInterface, touches: 0, targetTouches: 0, @@ -485,24 +509,28 @@ export const TouchEventInterface = { shiftKey: 0, getModifierState: getEventModifierState, }; +export const SyntheticTouchEvent = createSyntheticEvent(TouchEventInterface); /** * @interface Event * @see http://www.w3.org/TR/2009/WD-css3-transitions-20090320/#transition-events- * @see https://developer.mozilla.org/en-US/docs/Web/API/TransitionEvent */ -export const TransitionEventInterface = { +const TransitionEventInterface = { ...EventInterface, propertyName: 0, elapsedTime: 0, pseudoElement: 0, }; +export const SyntheticTransitionEvent = createSyntheticEvent( + TransitionEventInterface, +); /** * @interface WheelEvent * @see http://www.w3.org/TR/DOM-Level-3-Events/ */ -export const WheelEventInterface = { +const WheelEventInterface = { ...MouseEventInterface, deltaX(event) { return 'deltaX' in event @@ -531,3 +559,4 @@ export const WheelEventInterface = { // ~40 pixels, for DOM_DELTA_SCREEN (2) it is 87.5% of viewport size. deltaMode: 0, }; +export const SyntheticWheelEvent = createSyntheticEvent(WheelEventInterface); diff --git a/packages/react-dom/src/events/plugins/BeforeInputEventPlugin.js b/packages/react-dom/src/events/plugins/BeforeInputEventPlugin.js index 3cd1825fd6..1935d30170 100644 --- a/packages/react-dom/src/events/plugins/BeforeInputEventPlugin.js +++ b/packages/react-dom/src/events/plugins/BeforeInputEventPlugin.js @@ -22,9 +22,8 @@ import { reset as FallbackCompositionStateReset, } from '../FallbackCompositionState'; import { - CompositionEventInterface, - InputEventInterface, - SyntheticEvent, + SyntheticCompositionEvent, + SyntheticInputEvent, } from '../SyntheticEvent'; import {accumulateTwoPhaseListeners} from '../DOMPluginEventSystem'; @@ -227,13 +226,12 @@ function extractCompositionEvent( } } - const event = new SyntheticEvent( + const event = new SyntheticCompositionEvent( eventType, domEventName, null, nativeEvent, nativeEventTarget, - CompositionEventInterface, ); accumulateTwoPhaseListeners(targetInst, dispatchQueue, event); @@ -396,13 +394,12 @@ function extractBeforeInputEvent( return null; } - const event = new SyntheticEvent( + const event = new SyntheticInputEvent( 'onBeforeInput', 'beforeinput', null, nativeEvent, nativeEventTarget, - InputEventInterface, ); accumulateTwoPhaseListeners(targetInst, dispatchQueue, event); event.data = chars; diff --git a/packages/react-dom/src/events/plugins/EnterLeaveEventPlugin.js b/packages/react-dom/src/events/plugins/EnterLeaveEventPlugin.js index 196d99c711..be5d94da80 100644 --- a/packages/react-dom/src/events/plugins/EnterLeaveEventPlugin.js +++ b/packages/react-dom/src/events/plugins/EnterLeaveEventPlugin.js @@ -14,11 +14,7 @@ import type {EventSystemFlags} from '../EventSystemFlags'; import {registerDirectEvent} from '../EventRegistry'; import {IS_REPLAYED} from 'react-dom/src/events/EventSystemFlags'; -import { - SyntheticEvent, - MouseEventInterface, - PointerEventInterface, -} from '../SyntheticEvent'; +import {SyntheticMouseEvent, SyntheticPointerEvent} from '../SyntheticEvent'; import { getClosestInstanceFromNode, getNodeFromInstance, @@ -123,12 +119,12 @@ function extractEvents( return; } - let eventInterface = MouseEventInterface; + let SyntheticEventCtor = SyntheticMouseEvent; let leaveEventType = 'onMouseLeave'; let enterEventType = 'onMouseEnter'; let eventTypePrefix = 'mouse'; if (domEventName === 'pointerout' || domEventName === 'pointerover') { - eventInterface = PointerEventInterface; + SyntheticEventCtor = SyntheticPointerEvent; leaveEventType = 'onPointerLeave'; enterEventType = 'onPointerEnter'; eventTypePrefix = 'pointer'; @@ -137,13 +133,12 @@ function extractEvents( const fromNode = from == null ? win : getNodeFromInstance(from); const toNode = to == null ? win : getNodeFromInstance(to); - const leave = new SyntheticEvent( + const leave = new SyntheticEventCtor( leaveEventType, eventTypePrefix + 'leave', from, nativeEvent, nativeEventTarget, - eventInterface, ); leave.target = fromNode; leave.relatedTarget = toNode; @@ -154,13 +149,12 @@ function extractEvents( // the first ancestor. Next time, we will ignore the event. const nativeTargetInst = getClosestInstanceFromNode((nativeEventTarget: any)); if (nativeTargetInst === targetInst) { - const enterEvent: KnownReactSyntheticEvent = new SyntheticEvent( + const enterEvent: KnownReactSyntheticEvent = new SyntheticEventCtor( enterEventType, eventTypePrefix + 'enter', to, nativeEvent, nativeEventTarget, - eventInterface, ); enterEvent.target = toNode; enterEvent.relatedTarget = fromNode; diff --git a/packages/react-dom/src/events/plugins/SimpleEventPlugin.js b/packages/react-dom/src/events/plugins/SimpleEventPlugin.js index fa49b84e64..2e4416c6a7 100644 --- a/packages/react-dom/src/events/plugins/SimpleEventPlugin.js +++ b/packages/react-dom/src/events/plugins/SimpleEventPlugin.js @@ -15,17 +15,17 @@ import type {EventSystemFlags} from '../EventSystemFlags'; import { SyntheticEvent, - AnimationEventInterface, - ClipboardEventInterface, - FocusEventInterface, - KeyboardEventInterface, - MouseEventInterface, - PointerEventInterface, - DragEventInterface, - TouchEventInterface, - TransitionEventInterface, - UIEventInterface, - WheelEventInterface, + SyntheticKeyboardEvent, + SyntheticFocusEvent, + SyntheticMouseEvent, + SyntheticDragEvent, + SyntheticTouchEvent, + SyntheticAnimationEvent, + SyntheticTransitionEvent, + SyntheticUIEvent, + SyntheticWheelEvent, + SyntheticClipboardEvent, + SyntheticPointerEvent, } from '../../events/SyntheticEvent'; import { @@ -62,7 +62,7 @@ function extractEvents( if (reactName === undefined) { return; } - let EventInterface; + let SyntheticEventCtor = SyntheticEvent; let reactEventType = domEventName; switch (domEventName) { case 'keypress': @@ -75,19 +75,19 @@ function extractEvents( /* falls through */ case 'keydown': case 'keyup': - EventInterface = KeyboardEventInterface; + SyntheticEventCtor = SyntheticKeyboardEvent; break; case 'focusin': reactEventType = 'focus'; - EventInterface = FocusEventInterface; + SyntheticEventCtor = SyntheticFocusEvent; break; case 'focusout': reactEventType = 'blur'; - EventInterface = FocusEventInterface; + SyntheticEventCtor = SyntheticFocusEvent; break; case 'beforeblur': case 'afterblur': - EventInterface = FocusEventInterface; + SyntheticEventCtor = SyntheticFocusEvent; break; case 'click': // Firefox creates a click event on right mouse clicks. This removes the @@ -106,7 +106,7 @@ function extractEvents( case 'mouseout': case 'mouseover': case 'contextmenu': - EventInterface = MouseEventInterface; + SyntheticEventCtor = SyntheticMouseEvent; break; case 'drag': case 'dragend': @@ -116,32 +116,32 @@ function extractEvents( case 'dragover': case 'dragstart': case 'drop': - EventInterface = DragEventInterface; + SyntheticEventCtor = SyntheticDragEvent; break; case 'touchcancel': case 'touchend': case 'touchmove': case 'touchstart': - EventInterface = TouchEventInterface; + SyntheticEventCtor = SyntheticTouchEvent; break; case ANIMATION_END: case ANIMATION_ITERATION: case ANIMATION_START: - EventInterface = AnimationEventInterface; + SyntheticEventCtor = SyntheticAnimationEvent; break; case TRANSITION_END: - EventInterface = TransitionEventInterface; + SyntheticEventCtor = SyntheticTransitionEvent; break; case 'scroll': - EventInterface = UIEventInterface; + SyntheticEventCtor = SyntheticUIEvent; break; case 'wheel': - EventInterface = WheelEventInterface; + SyntheticEventCtor = SyntheticWheelEvent; break; case 'copy': case 'cut': case 'paste': - EventInterface = ClipboardEventInterface; + SyntheticEventCtor = SyntheticClipboardEvent; break; case 'gotpointercapture': case 'lostpointercapture': @@ -151,19 +151,18 @@ function extractEvents( case 'pointerout': case 'pointerover': case 'pointerup': - EventInterface = PointerEventInterface; + SyntheticEventCtor = SyntheticPointerEvent; break; default: // Unknown event. This is used by createEventHandle. break; } - const event = new SyntheticEvent( + const event = new SyntheticEventCtor( reactName, reactEventType, null, nativeEvent, nativeEventTarget, - EventInterface, ); const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;