diff --git a/packages/events/EventTypes.js b/packages/events/EventTypes.js index 169b34aa07713..ab5c51db640a0 100644 --- a/packages/events/EventTypes.js +++ b/packages/events/EventTypes.js @@ -10,34 +10,40 @@ import type {AnyNativeEvent} from 'events/PluginModuleType'; import type {ReactEventResponderEventType} from 'shared/ReactTypes'; -export type EventResponderContext = { - event: AnyNativeEvent, - eventTarget: Element | Document, - eventType: string, - isPassive: () => boolean, - isPassiveSupported: () => boolean, - dispatchEvent: ( - eventObject: E, - { - capture?: boolean, - discrete?: boolean, - stopPropagation?: boolean, - }, +export type ResponderEvent = { + nativeEvent: AnyNativeEvent, + target: Element | Document, + type: string, + passive: boolean, + passiveSupported: boolean, +}; + +export type ResponderDispatchEventOptions = { + capture?: boolean, + discrete?: boolean, + stopPropagation?: boolean, +}; + +export type ResponderContext = { + dispatchEvent: ( + eventObject: Object, + otpions: ResponderDispatchEventOptions, ) => void, isTargetWithinElement: ( childTarget: Element | Document, parentTarget: Element | Document, ) => boolean, - isTargetOwned: (Element | Document) => boolean, isTargetWithinEventComponent: (Element | Document) => boolean, isPositionWithinTouchHitTarget: (x: number, y: number) => boolean, addRootEventTypes: ( + document: Document, rootEventTypes: Array, ) => void, removeRootEventTypes: ( rootEventTypes: Array, ) => void, - requestOwnership: (target: Element | Document | null) => boolean, - releaseOwnership: (target: Element | Document | null) => boolean, - withAsyncDispatching: (func: () => void) => void, + hasOwnership: () => boolean, + requestOwnership: () => boolean, + releaseOwnership: () => boolean, + setTimeout: (func: () => void, timeout: number) => TimeoutID, }; diff --git a/packages/react-dom/src/events/DOMEventResponderSystem.js b/packages/react-dom/src/events/DOMEventResponderSystem.js index 45e6c95e463b6..7630d8d7b427f 100644 --- a/packages/react-dom/src/events/DOMEventResponderSystem.js +++ b/packages/react-dom/src/events/DOMEventResponderSystem.js @@ -6,6 +6,11 @@ * @flow */ +import type { + ResponderContext, + ResponderEvent, + ResponderDispatchEventOptions, +} from 'events/EventTypes'; import { type EventSystemFlags, IS_PASSIVE, @@ -20,13 +25,13 @@ import type { import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes'; import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; -import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; - +import warning from 'shared/warning'; import {enableEventAPI} from 'shared/ReactFeatureFlags'; import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils'; -import warning from 'shared/warning'; -let listenToResponderEventTypesImpl; +import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; + +export let listenToResponderEventTypesImpl; export function setListenToResponderEventTypes( _listenToResponderEventTypesImpl: Function, @@ -34,31 +39,215 @@ export function setListenToResponderEventTypes( listenToResponderEventTypesImpl = _listenToResponderEventTypesImpl; } -const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set; +type EventQueue = { + bubble: null | Array<$Shape>, + capture: null | Array<$Shape>, + discrete: boolean, +}; + +type PartialEventObject = { + listener: ($Shape) => void, + target: Element | Document, + type: string, +}; + +let currentOwner = null; +let currentFiber: Fiber; +let currentResponder: ReactEventResponder; +let currentEventQueue: EventQueue; + +const eventResponderContext: ResponderContext = { + dispatchEvent( + possibleEventObject: Object, + {capture, discrete, stopPropagation}: ResponderDispatchEventOptions, + ): void { + const eventQueue = currentEventQueue; + const {listener, target, type} = possibleEventObject; + + if (listener == null || target == null || type == null) { + throw new Error( + 'context.dispatchEvent: "listener", "target" and "type" fields on event object are required.', + ); + } + if (__DEV__) { + possibleEventObject.preventDefault = () => { + // Update this warning when we have a story around dealing with preventDefault + warning( + false, + 'preventDefault() is no longer available on event objects created from event responder modules.', + ); + }; + possibleEventObject.stopPropagation = () => { + // Update this warning when we have a story around dealing with stopPropgation + warning( + false, + 'stopPropagation() is no longer available on event objects created from event responder modules.', + ); + }; + } + const eventObject = ((possibleEventObject: any): $Shape< + PartialEventObject, + >); + let events; + + if (capture) { + events = eventQueue.capture; + if (events === null) { + events = eventQueue.capture = []; + } + } else { + events = eventQueue.bubble; + if (events === null) { + events = eventQueue.bubble = []; + } + } + if (discrete) { + eventQueue.discrete = true; + } + events.push(eventObject); + + if (stopPropagation) { + eventsWithStopPropagation.add(eventObject); + } + }, + isPositionWithinTouchHitTarget(x: number, y: number): boolean { + return false; + }, + isTargetWithinEventComponent(target: Element | Document): boolean { + const eventFiber = currentFiber; + + if (target != null) { + let fiber = getClosestInstanceFromNode(target); + while (fiber !== null) { + if (fiber === eventFiber || fiber === eventFiber.alternate) { + return true; + } + fiber = fiber.return; + } + } + return false; + }, + isTargetWithinElement( + childTarget: Element | Document, + parentTarget: Element | Document, + ): boolean { + const childFiber = getClosestInstanceFromNode(childTarget); + const parentFiber = getClosestInstanceFromNode(parentTarget); + + let node = childFiber; + while (node !== null) { + if (node === parentFiber) { + return true; + } + node = node.return; + } + return false; + }, + addRootEventTypes( + doc: Document, + rootEventTypes: Array, + ): void { + listenToResponderEventTypesImpl(rootEventTypes, doc); + const eventComponent = currentFiber; + for (let i = 0; i < rootEventTypes.length; i++) { + const rootEventType = rootEventTypes[i]; + const topLevelEventType = + typeof rootEventType === 'string' ? rootEventType : rootEventType.name; + let rootEventComponents = rootEventTypesToEventComponents.get( + topLevelEventType, + ); + if (rootEventComponents === undefined) { + rootEventComponents = new Set(); + rootEventTypesToEventComponents.set( + topLevelEventType, + rootEventComponents, + ); + } + rootEventComponents.add(eventComponent); + } + }, + removeRootEventTypes( + rootEventTypes: Array, + ): void { + const eventComponent = currentFiber; + for (let i = 0; i < rootEventTypes.length; i++) { + const rootEventType = rootEventTypes[i]; + const topLevelEventType = + typeof rootEventType === 'string' ? rootEventType : rootEventType.name; + let rootEventComponents = rootEventTypesToEventComponents.get( + topLevelEventType, + ); + if (rootEventComponents !== undefined) { + rootEventComponents.delete(eventComponent); + } + } + }, + hasOwnership(): boolean { + return currentOwner === currentFiber; + }, + requestOwnership(): boolean { + if (currentOwner !== null) { + return false; + } + currentOwner = currentFiber; + return true; + }, + releaseOwnership(): boolean { + if (currentOwner !== currentFiber) { + return false; + } + currentOwner = null; + return false; + }, + setTimeout(func: () => void, delay): TimeoutID { + const contextResponder = currentResponder; + const contextFiber = currentFiber; + return setTimeout(() => { + const previousEventQueue = currentEventQueue; + const previousFiber = currentFiber; + const previousResponder = currentResponder; + currentEventQueue = createEventQueue(); + currentResponder = contextResponder; + currentFiber = contextFiber; + try { + func(); + batchedUpdates(processEventQueue, currentEventQueue); + } finally { + currentFiber = previousFiber; + currentEventQueue = previousEventQueue; + currentResponder = previousResponder; + } + }, delay); + }, +}; const rootEventTypesToEventComponents: Map< DOMTopLevelEventType | string, Set, > = new Map(); +const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set; +const eventsWithStopPropagation: + | WeakSet + | Set<$Shape> = new PossiblyWeakSet(); const targetEventTypeCached: Map< Array, Set, > = new Map(); -const targetOwnership: Map = new Map(); -const eventsWithStopPropagation: - | WeakSet - | Set<$Shape> = new PossiblyWeakSet(); -type PartialEventObject = { - listener: ($Shape) => void, - target: Element | Document, - type: string, -}; -type EventQueue = { - bubble: null | Array<$Shape>, - capture: null | Array<$Shape>, - discrete: boolean, -}; +function createResponderEvent( + topLevelType: string, + nativeEvent: AnyNativeEvent, + nativeEventTarget: Element | Document, + eventSystemFlags: EventSystemFlags, +): ResponderEvent { + return { + nativeEvent: nativeEvent, + target: nativeEventTarget, + type: topLevelType, + passive: (eventSystemFlags & IS_PASSIVE) !== 0, + passiveSupported: (eventSystemFlags & PASSIVE_NOT_SUPPORTED) === 0, + }; +} function createEventQueue(): EventQueue { return { @@ -100,8 +289,8 @@ function processEvents( } } -function processEventQueue(eventQueue: EventQueue): void { - const {bubble, capture, discrete} = eventQueue; +export function processEventQueue(): void { + const {bubble, capture, discrete} = currentEventQueue; if (discrete) { interactiveUpdates(() => { @@ -112,218 +301,6 @@ function processEventQueue(eventQueue: EventQueue): void { } } -// TODO add context methods for dispatching events -function DOMEventResponderContext( - topLevelType: DOMTopLevelEventType, - nativeEvent: AnyNativeEvent, - nativeEventTarget: EventTarget, - eventSystemFlags: EventSystemFlags, -) { - this.event = nativeEvent; - this.eventTarget = nativeEventTarget; - this.eventType = topLevelType; - this._flags = eventSystemFlags; - this._fiber = null; - this._responder = null; - this._discreteEvents = null; - this._nonDiscreteEvents = null; - this._isBatching = true; - this._eventQueue = createEventQueue(); -} - -DOMEventResponderContext.prototype.isPassive = function(): boolean { - return (this._flags & IS_PASSIVE) !== 0; -}; - -DOMEventResponderContext.prototype.isPassiveSupported = function(): boolean { - return (this._flags & PASSIVE_NOT_SUPPORTED) === 0; -}; - -DOMEventResponderContext.prototype.dispatchEvent = function( - possibleEventObject: Object, - { - capture, - discrete, - stopPropagation, - }: { - capture?: boolean, - discrete?: boolean, - stopPropagation?: boolean, - }, -): void { - const eventQueue = this._eventQueue; - const {listener, target, type} = possibleEventObject; - - if (listener == null || target == null || type == null) { - throw new Error( - 'context.dispatchEvent: "listener", "target" and "type" fields on event object are required.', - ); - } - if (__DEV__) { - possibleEventObject.preventDefault = () => { - // Update this warning when we have a story around dealing with preventDefault - warning( - false, - 'preventDefault() is no longer available on event objects created from event responder modules.', - ); - }; - possibleEventObject.stopPropagation = () => { - // Update this warning when we have a story around dealing with stopPropgation - warning( - false, - 'stopPropagation() is no longer available on event objects created from event responder modules.', - ); - }; - } - const eventObject = ((possibleEventObject: any): $Shape); - let events; - - if (capture) { - events = eventQueue.capture; - if (events === null) { - events = eventQueue.capture = []; - } - } else { - events = eventQueue.bubble; - if (events === null) { - events = eventQueue.bubble = []; - } - } - if (discrete) { - eventQueue.discrete = true; - } - events.push(eventObject); - - if (stopPropagation) { - eventsWithStopPropagation.add(eventObject); - } -}; - -DOMEventResponderContext.prototype.isTargetWithinEventComponent = function( - target: AnyNativeEvent, -): boolean { - const eventFiber = this._fiber; - - if (target != null) { - let fiber = getClosestInstanceFromNode(target); - while (fiber !== null) { - if (fiber === eventFiber || fiber === eventFiber.alternate) { - return true; - } - fiber = fiber.return; - } - } - return false; -}; - -DOMEventResponderContext.prototype.isTargetWithinElement = function( - childTarget: EventTarget, - parentTarget: EventTarget, -): boolean { - const childFiber = getClosestInstanceFromNode(childTarget); - const parentFiber = getClosestInstanceFromNode(parentTarget); - - let currentFiber = childFiber; - while (currentFiber !== null) { - if (currentFiber === parentFiber) { - return true; - } - currentFiber = currentFiber.return; - } - return false; -}; - -DOMEventResponderContext.prototype.addRootEventTypes = function( - rootEventTypes: Array, -) { - const element = this.eventTarget.ownerDocument; - listenToResponderEventTypesImpl(rootEventTypes, element); - const eventComponent = this._fiber; - for (let i = 0; i < rootEventTypes.length; i++) { - const rootEventType = rootEventTypes[i]; - const topLevelEventType = - typeof rootEventType === 'string' ? rootEventType : rootEventType.name; - let rootEventComponents = rootEventTypesToEventComponents.get( - topLevelEventType, - ); - if (rootEventComponents === undefined) { - rootEventComponents = new Set(); - rootEventTypesToEventComponents.set( - topLevelEventType, - rootEventComponents, - ); - } - rootEventComponents.add(eventComponent); - } -}; - -DOMEventResponderContext.prototype.removeRootEventTypes = function( - rootEventTypes: Array, -): void { - const eventComponent = this._fiber; - for (let i = 0; i < rootEventTypes.length; i++) { - const rootEventType = rootEventTypes[i]; - const topLevelEventType = - typeof rootEventType === 'string' ? rootEventType : rootEventType.name; - let rootEventComponents = rootEventTypesToEventComponents.get( - topLevelEventType, - ); - if (rootEventComponents !== undefined) { - rootEventComponents.delete(eventComponent); - } - } -}; - -DOMEventResponderContext.prototype.isPositionWithinTouchHitTarget = function() { - // TODO -}; - -DOMEventResponderContext.prototype.isTargetOwned = function( - targetElement: Element | Node, -): boolean { - const targetDoc = targetElement.ownerDocument; - return targetOwnership.has(targetDoc); -}; - -DOMEventResponderContext.prototype.requestOwnership = function( - targetElement: Element | Node, -): boolean { - const targetDoc = targetElement.ownerDocument; - if (targetOwnership.has(targetDoc)) { - return false; - } - targetOwnership.set(targetDoc, this._fiber); - return true; -}; - -DOMEventResponderContext.prototype.releaseOwnership = function( - targetElement: Element | Node, -): boolean { - const targetDoc = targetElement.ownerDocument; - if (!targetOwnership.has(targetDoc)) { - return false; - } - const owner = targetOwnership.get(targetDoc); - if (owner === this._fiber || owner === this._fiber.alternate) { - targetOwnership.delete(targetDoc); - return true; - } - return false; -}; - -DOMEventResponderContext.prototype.withAsyncDispatching = function( - func: () => void, -) { - const previousEventQueue = this._eventQueue; - this._eventQueue = createEventQueue(); - try { - func(); - batchedUpdates(processEventQueue, this._eventQueue); - } finally { - this._eventQueue = previousEventQueue; - } -}; - function getTargetEventTypes( eventTypes: Array, ): Set { @@ -345,7 +322,7 @@ function getTargetEventTypes( function handleTopLevelType( topLevelType: DOMTopLevelEventType, fiber: Fiber, - context: Object, + responderEvent: ResponderEvent, isRootLevelEvent: boolean, ): void { const responder: ReactEventResponder = fiber.type.responder; @@ -360,9 +337,10 @@ function handleTopLevelType( if (state === null && responder.createInitialState !== undefined) { state = fiber.stateNode.state = responder.createInitialState(props); } - context._fiber = fiber; - context._responder = responder; - responder.handleEvent(context, props, state); + currentFiber = fiber; + currentResponder = responder; + + responder.onEvent(responderEvent, eventResponderContext, props, state); } export function runResponderEventsInBatch( @@ -373,17 +351,18 @@ export function runResponderEventsInBatch( eventSystemFlags: EventSystemFlags, ): void { if (enableEventAPI) { - const context = new DOMEventResponderContext( - topLevelType, + currentEventQueue = createEventQueue(); + const responderEvent = createResponderEvent( + ((topLevelType: any): string), nativeEvent, - nativeEventTarget, + ((nativeEventTarget: any): Element | Document), eventSystemFlags, ); let node = targetFiber; // Traverse up the fiber tree till we find event component fibers. while (node !== null) { if (node.tag === EventComponent) { - handleTopLevelType(topLevelType, node, context, false); + handleTopLevelType(topLevelType, node, responderEvent, false); } node = node.return; } @@ -399,11 +378,11 @@ export function runResponderEventsInBatch( handleTopLevelType( topLevelType, rootEventComponentFiber, - context, + responderEvent, true, ); } } - processEventQueue(context._eventQueue); + processEventQueue(); } } diff --git a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js index af0111e4eb9c8..c304781341d66 100644 --- a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js @@ -13,10 +13,10 @@ let React; let ReactFeatureFlags; let ReactDOM; -function createReactEventComponent(targetEventTypes, handleEvent) { +function createReactEventComponent(targetEventTypes, onEvent) { const testEventResponder = { targetEventTypes, - handleEvent, + onEvent, }; return { @@ -53,19 +53,19 @@ describe('DOMEventResponderSystem', () => { container = null; }); - it('the event responder handleEvent() function should fire on click event', () => { + it('the event responder onEvent() function should fire on click event', () => { let eventResponderFiredCount = 0; let eventLog = []; const buttonRef = React.createRef(); const ClickEventComponent = createReactEventComponent( ['click'], - (context, props) => { + (event, context, props) => { eventResponderFiredCount++; eventLog.push({ - name: context.eventType, - passive: context.isPassive(), - passiveSupported: context.isPassiveSupported(), + name: event.type, + passive: event.passive, + passiveSupported: event.passiveSupported, }); }, ); @@ -79,7 +79,7 @@ describe('DOMEventResponderSystem', () => { ReactDOM.render(, container); expect(container.innerHTML).toBe(''); - // Clicking the button should trigger the event responder handleEvent() + // Clicking the button should trigger the event responder onEvent() let buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(eventResponderFiredCount).toBe(1); @@ -103,7 +103,7 @@ describe('DOMEventResponderSystem', () => { expect(eventResponderFiredCount).toBe(2); }); - it('the event responder handleEvent() function should fire on click event (passive events forced)', () => { + it('the event responder onEvent() function should fire on click event (passive events forced)', () => { // JSDOM does not support passive events, so this manually overrides the value to be true const checkPassiveEvents = require('react-dom/src/events/checkPassiveEvents'); checkPassiveEvents.passiveBrowserEventsSupported = true; @@ -113,11 +113,11 @@ describe('DOMEventResponderSystem', () => { const ClickEventComponent = createReactEventComponent( ['click'], - (context, props) => { + (event, context, props) => { eventLog.push({ - name: context.eventType, - passive: context.isPassive(), - passiveSupported: context.isPassiveSupported(), + name: event.type, + passive: event.passive, + passiveSupported: event.passiveSupported, }); }, ); @@ -130,7 +130,7 @@ describe('DOMEventResponderSystem', () => { ReactDOM.render(, container); - // Clicking the button should trigger the event responder handleEvent() + // Clicking the button should trigger the event responder onEvent() let buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(eventLog.length).toBe(1); @@ -141,19 +141,19 @@ describe('DOMEventResponderSystem', () => { }); }); - it('nested event responders and their handleEvent() function should fire multiple times', () => { + it('nested event responders and their onEvent() function should fire multiple times', () => { let eventResponderFiredCount = 0; let eventLog = []; const buttonRef = React.createRef(); const ClickEventComponent = createReactEventComponent( ['click'], - (context, props) => { + (event, context, props) => { eventResponderFiredCount++; eventLog.push({ - name: context.eventType, - passive: context.isPassive(), - passiveSupported: context.isPassiveSupported(), + name: event.type, + passive: event.passive, + passiveSupported: event.passiveSupported, }); }, ); @@ -168,7 +168,7 @@ describe('DOMEventResponderSystem', () => { ReactDOM.render(, container); - // Clicking the button should trigger the event responder handleEvent() + // Clicking the button should trigger the event responder onEvent() let buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(eventResponderFiredCount).toBe(2); @@ -186,7 +186,7 @@ describe('DOMEventResponderSystem', () => { }); }); - it('nested event responders and their handleEvent() should fire in the correct order', () => { + it('nested event responders and their onEvent() should fire in the correct order', () => { let eventLog = []; const buttonRef = React.createRef(); @@ -214,7 +214,7 @@ describe('DOMEventResponderSystem', () => { ReactDOM.render(, container); - // Clicking the button should trigger the event responder handleEvent() + // Clicking the button should trigger the event responder onEvent() let buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); @@ -227,14 +227,14 @@ describe('DOMEventResponderSystem', () => { const ClickEventComponent = createReactEventComponent( ['click'], - (context, props) => { + (event, context, props) => { if (props.onMagicClick) { - const event = { + const syntheticEvent = { listener: props.onMagicClick, - target: context.eventTarget, + target: event.target, type: 'magicclick', }; - context.dispatchEvent(event, {discrete: true}); + context.dispatchEvent(syntheticEvent, {discrete: true}); } }, ); @@ -251,7 +251,7 @@ describe('DOMEventResponderSystem', () => { ReactDOM.render(, container); - // Clicking the button should trigger the event responder handleEvent() + // Clicking the button should trigger the event responder onEvent() let buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); @@ -264,37 +264,33 @@ describe('DOMEventResponderSystem', () => { const LongPressEventComponent = createReactEventComponent( ['click'], - (context, props) => { + (event, context, props) => { const pressEvent = { listener: props.onPress, - target: context.eventTarget, + target: event.target, type: 'press', }; context.dispatchEvent(pressEvent, {discrete: true}); - setTimeout( - () => - context.withAsyncDispatching(() => { - if (props.onLongPress) { - const longPressEvent = { - listener: props.onLongPress, - target: context.eventTarget, - type: 'longpress', - }; - context.dispatchEvent(longPressEvent, {discrete: true}); - } - - if (props.onLongPressChange) { - const longPressChangeEvent = { - listener: props.onLongPressChange, - target: context.eventTarget, - type: 'longpresschange', - }; - context.dispatchEvent(longPressChangeEvent, {discrete: true}); - } - }), - 500, - ); + context.setTimeout(() => { + if (props.onLongPress) { + const longPressEvent = { + listener: props.onLongPress, + target: event.target, + type: 'longpress', + }; + context.dispatchEvent(longPressEvent, {discrete: true}); + } + + if (props.onLongPressChange) { + const longPressChangeEvent = { + listener: props.onLongPressChange, + target: event.target, + type: 'longpresschange', + }; + context.dispatchEvent(longPressChangeEvent, {discrete: true}); + } + }, 500); }, ); @@ -313,7 +309,7 @@ describe('DOMEventResponderSystem', () => { ReactDOM.render(, container); - // Clicking the button should trigger the event responder handleEvent() + // Clicking the button should trigger the event responder onEvent() let buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); jest.runAllTimers(); diff --git a/packages/react-events/src/Drag.js b/packages/react-events/src/Drag.js index ed11c4260c270..86cb120af2667 100644 --- a/packages/react-events/src/Drag.js +++ b/packages/react-events/src/Drag.js @@ -7,7 +7,7 @@ * @flow */ -import type {EventResponderContext} from 'events/EventTypes'; +import type {ResponderEvent, ResponderContext} from 'events/EventTypes'; import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols'; const targetEventTypes = ['pointerdown', 'pointercancel']; @@ -62,7 +62,7 @@ function createDragEvent( } function dispatchDragEvent( - context: EventResponderContext, + context: ResponderContext, name: DragEventType, listener: DragEvent => void, state: DragState, @@ -87,28 +87,31 @@ const DragResponder = { y: 0, }; }, - handleEvent( - context: EventResponderContext, + onEvent( + event: ResponderEvent, + context: ResponderContext, props: Object, state: DragState, ): void { - const {eventTarget, eventType, event} = context; + const {target, type, nativeEvent} = event; - switch (eventType) { + switch (type) { case 'touchstart': case 'mousedown': case 'pointerdown': { if (!state.isDragging) { if (props.onShouldClaimOwnership) { - context.releaseOwnership(state.dragTarget); + context.releaseOwnership(); } const obj = - eventType === 'touchstart' ? (event: any).changedTouches[0] : event; + type === 'touchstart' + ? (nativeEvent: any).changedTouches[0] + : nativeEvent; const x = (state.startX = (obj: any).screenX); const y = (state.startY = (obj: any).screenY); state.x = x; state.y = y; - state.dragTarget = eventTarget; + state.dragTarget = target; state.isPointerDown = true; if (props.onDragStart) { @@ -121,19 +124,21 @@ const DragResponder = { ); } - context.addRootEventTypes(rootEventTypes); + context.addRootEventTypes(target.ownerDocument, rootEventTypes); } break; } case 'touchmove': case 'mousemove': case 'pointermove': { - if (context.isPassive()) { + if (event.passive) { return; } if (state.isPointerDown) { const obj = - eventType === 'touchmove' ? (event: any).changedTouches[0] : event; + type === 'touchmove' + ? (nativeEvent: any).changedTouches[0] + : nativeEvent; const x = (obj: any).screenX; const y = (obj: any).screenY; state.x = x; @@ -145,7 +150,7 @@ const DragResponder = { props.onShouldClaimOwnership && props.onShouldClaimOwnership() ) { - shouldEnableDragging = context.requestOwnership(state.dragTarget); + shouldEnableDragging = context.requestOwnership(); } if (shouldEnableDragging) { state.isDragging = true; @@ -181,7 +186,7 @@ const DragResponder = { eventData, ); } - (event: any).preventDefault(); + (nativeEvent: any).preventDefault(); } } break; @@ -193,7 +198,7 @@ const DragResponder = { case 'pointerup': { if (state.isDragging) { if (props.onShouldClaimOwnership) { - context.releaseOwnership(state.dragTarget); + context.releaseOwnership(); } if (props.onDragEnd) { dispatchDragEvent(context, 'dragend', props.onDragEnd, state, true); diff --git a/packages/react-events/src/Focus.js b/packages/react-events/src/Focus.js index 0b26bde9757bf..30ff7102ba4cc 100644 --- a/packages/react-events/src/Focus.js +++ b/packages/react-events/src/Focus.js @@ -7,7 +7,7 @@ * @flow */ -import type {EventResponderContext} from 'events/EventTypes'; +import type {ResponderEvent, ResponderContext} from 'events/EventTypes'; import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols'; type FocusProps = { @@ -47,55 +47,45 @@ function createFocusEvent( } function dispatchFocusInEvents( - context: EventResponderContext, + event: ResponderEvent, + context: ResponderContext, props: FocusProps, ) { - const {event, eventTarget} = context; - if (context.isTargetWithinEventComponent((event: any).relatedTarget)) { + const {nativeEvent, target} = event; + if (context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget)) { return; } if (props.onFocus) { - const syntheticEvent = createFocusEvent( - 'focus', - eventTarget, - props.onFocus, - ); + const syntheticEvent = createFocusEvent('focus', target, props.onFocus); context.dispatchEvent(syntheticEvent, {discrete: true}); } if (props.onFocusChange) { const listener = () => { props.onFocusChange(true); }; - const syntheticEvent = createFocusEvent( - 'focuschange', - eventTarget, - listener, - ); + const syntheticEvent = createFocusEvent('focuschange', target, listener); context.dispatchEvent(syntheticEvent, {discrete: true}); } } function dispatchFocusOutEvents( - context: EventResponderContext, + event: ResponderEvent, + context: ResponderContext, props: FocusProps, ) { - const {event, eventTarget} = context; - if (context.isTargetWithinEventComponent((event: any).relatedTarget)) { + const {nativeEvent, target} = event; + if (context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget)) { return; } if (props.onBlur) { - const syntheticEvent = createFocusEvent('blur', eventTarget, props.onBlur); + const syntheticEvent = createFocusEvent('blur', target, props.onBlur); context.dispatchEvent(syntheticEvent, {discrete: true}); } if (props.onFocusChange) { const listener = () => { props.onFocusChange(false); }; - const syntheticEvent = createFocusEvent( - 'focuschange', - eventTarget, - listener, - ); + const syntheticEvent = createFocusEvent('focuschange', target, listener); context.dispatchEvent(syntheticEvent, {discrete: true}); } } @@ -107,24 +97,25 @@ const FocusResponder = { isFocused: false, }; }, - handleEvent( - context: EventResponderContext, + onEvent( + event: ResponderEvent, + context: ResponderContext, props: Object, state: FocusState, ): void { - const {eventTarget, eventType} = context; + const {type} = event; - switch (eventType) { + switch (type) { case 'focus': { - if (!state.isFocused && !context.isTargetOwned(eventTarget)) { - dispatchFocusInEvents(context, props); + if (!state.isFocused && !context.hasOwnership()) { + dispatchFocusInEvents(event, context, props); state.isFocused = true; } break; } case 'blur': { if (state.isFocused) { - dispatchFocusOutEvents(context, props); + dispatchFocusOutEvents(event, context, props); state.isFocused = false; } break; diff --git a/packages/react-events/src/Hover.js b/packages/react-events/src/Hover.js index ff42aac232e79..050a50bda0c31 100644 --- a/packages/react-events/src/Hover.js +++ b/packages/react-events/src/Hover.js @@ -7,7 +7,7 @@ * @flow */ -import type {EventResponderContext} from 'events/EventTypes'; +import type {ResponderEvent, ResponderContext} from 'events/EventTypes'; import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols'; type HoverProps = { @@ -61,17 +61,18 @@ function createHoverEvent( } function dispatchHoverStartEvents( - context: EventResponderContext, + event: ResponderEvent, + context: ResponderContext, props: HoverProps, ): void { - const {event, eventTarget} = context; - if (context.isTargetWithinEventComponent((event: any).relatedTarget)) { + const {nativeEvent, target} = event; + if (context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget)) { return; } if (props.onHoverStart) { const syntheticEvent = createHoverEvent( 'hoverstart', - eventTarget, + target, props.onHoverStart, ); context.dispatchEvent(syntheticEvent, {discrete: true}); @@ -80,27 +81,24 @@ function dispatchHoverStartEvents( const listener = () => { props.onHoverChange(true); }; - const syntheticEvent = createHoverEvent( - 'hoverchange', - eventTarget, - listener, - ); + const syntheticEvent = createHoverEvent('hoverchange', target, listener); context.dispatchEvent(syntheticEvent, {discrete: true}); } } function dispatchHoverEndEvents( - context: EventResponderContext, + event: ResponderEvent, + context: ResponderContext, props: HoverProps, ) { - const {event, eventTarget} = context; - if (context.isTargetWithinEventComponent((event: any).relatedTarget)) { + const {nativeEvent, target} = event; + if (context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget)) { return; } if (props.onHoverEnd) { const syntheticEvent = createHoverEvent( 'hoverend', - eventTarget, + target, props.onHoverEnd, ); context.dispatchEvent(syntheticEvent, {discrete: true}); @@ -109,11 +107,7 @@ function dispatchHoverEndEvents( const listener = () => { props.onHoverChange(false); }; - const syntheticEvent = createHoverEvent( - 'hoverchange', - eventTarget, - listener, - ); + const syntheticEvent = createHoverEvent('hoverchange', target, listener); context.dispatchEvent(syntheticEvent, {discrete: true}); } } @@ -127,14 +121,15 @@ const HoverResponder = { isTouched: false, }; }, - handleEvent( - context: EventResponderContext, + onEvent( + event: ResponderEvent, + context: ResponderContext, props: HoverProps, state: HoverState, ): void { - const {eventType, eventTarget, event} = context; + const {type, nativeEvent} = event; - switch (eventType) { + switch (type) { /** * Prevent hover events when touch is being used. */ @@ -147,25 +142,21 @@ const HoverResponder = { case 'pointerover': case 'mouseover': { - if ( - !state.isHovered && - !state.isTouched && - !context.isTargetOwned(eventTarget) - ) { - if ((event: any).pointerType === 'touch') { + if (!state.isHovered && !state.isTouched && !context.hasOwnership()) { + if ((nativeEvent: any).pointerType === 'touch') { state.isTouched = true; return; } if ( context.isPositionWithinTouchHitTarget( - (event: any).x, - (event: any).y, + (nativeEvent: any).x, + (nativeEvent: any).y, ) ) { state.isInHitSlop = true; return; } - dispatchHoverStartEvents(context, props); + dispatchHoverStartEvents(event, context, props); state.isHovered = true; } break; @@ -173,7 +164,7 @@ const HoverResponder = { case 'pointerout': case 'mouseout': { if (state.isHovered && !state.isTouched) { - dispatchHoverEndEvents(context, props); + dispatchHoverEndEvents(event, context, props); state.isHovered = false; } state.isInHitSlop = false; @@ -185,22 +176,22 @@ const HoverResponder = { if (state.isInHitSlop) { if ( !context.isPositionWithinTouchHitTarget( - (event: any).x, - (event: any).y, + (nativeEvent: any).x, + (nativeEvent: any).y, ) ) { - dispatchHoverStartEvents(context, props); + dispatchHoverStartEvents(event, context, props); state.isHovered = true; state.isInHitSlop = false; } } else if ( state.isHovered && context.isPositionWithinTouchHitTarget( - (event: any).x, - (event: any).y, + (nativeEvent: any).x, + (nativeEvent: any).y, ) ) { - dispatchHoverEndEvents(context, props); + dispatchHoverEndEvents(event, context, props); state.isHovered = false; state.isInHitSlop = true; } @@ -209,7 +200,7 @@ const HoverResponder = { } case 'pointercancel': { if (state.isHovered && !state.isTouched) { - dispatchHoverEndEvents(context, props); + dispatchHoverEndEvents(event, context, props); state.isHovered = false; state.isTouched = false; } diff --git a/packages/react-events/src/Press.js b/packages/react-events/src/Press.js index 69251a1fa3ef5..ab2750d6271b8 100644 --- a/packages/react-events/src/Press.js +++ b/packages/react-events/src/Press.js @@ -7,7 +7,7 @@ * @flow */ -import type {EventResponderContext} from 'events/EventTypes'; +import type {ResponderEvent, ResponderContext} from 'events/EventTypes'; import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols'; type PressProps = { @@ -86,7 +86,7 @@ function createPressEvent( } function dispatchEvent( - context: EventResponderContext, + context: ResponderContext, state: PressState, name: PressEventType, listener: (e: Object) => void, @@ -97,7 +97,7 @@ function dispatchEvent( } function dispatchPressChangeEvent( - context: EventResponderContext, + context: ResponderContext, props: PressProps, state: PressState, ): void { @@ -108,7 +108,7 @@ function dispatchPressChangeEvent( } function dispatchLongPressChangeEvent( - context: EventResponderContext, + context: ResponderContext, props: PressProps, state: PressState, ): void { @@ -119,7 +119,7 @@ function dispatchLongPressChangeEvent( } function dispatchPressStartEvents( - context: EventResponderContext, + context: ResponderContext, props: PressProps, state: PressState, ): void { @@ -138,34 +138,30 @@ function dispatchPressStartEvents( DEFAULT_LONG_PRESS_DELAY_MS, ); - state.longPressTimeout = setTimeout( - () => - context.withAsyncDispatching(() => { - state.isLongPressed = true; - state.longPressTimeout = null; - - if (props.onLongPress) { - const listener = e => { - props.onLongPress(e); - // TODO address this again at some point - // if (e.nativeEvent.defaultPrevented) { - // state.defaultPrevented = true; - // } - }; - dispatchEvent(context, state, 'longpress', listener); - } + state.longPressTimeout = context.setTimeout(() => { + state.isLongPressed = true; + state.longPressTimeout = null; + + if (props.onLongPress) { + const listener = e => { + props.onLongPress(e); + // TODO address this again at some point + // if (e.nativeEvent.defaultPrevented) { + // state.defaultPrevented = true; + // } + }; + dispatchEvent(context, state, 'longpress', listener); + } - if (props.onLongPressChange) { - dispatchLongPressChangeEvent(context, props, state); - } - }), - delayLongPress, - ); + if (props.onLongPressChange) { + dispatchLongPressChangeEvent(context, props, state); + } + }, delayLongPress); } } function dispatchPressEndEvents( - context: EventResponderContext, + context: ResponderContext, props: PressProps, state: PressState, ): void { @@ -206,6 +202,21 @@ function calculateDelayMS(delay: ?number, min = 0, fallback = 0) { return Math.max(min, maybeNumber != null ? maybeNumber : fallback); } +function unmountResponder( + context: ResponderContext, + props: PressProps, + state: PressState, +): void { + if (state.isPressed) { + state.isPressed = false; + dispatchPressEndEvents(context, props, state); + if (state.longPressTimeout !== null) { + clearTimeout(state.longPressTimeout); + state.longPressTimeout = null; + } + } +} + const PressResponder = { targetEventTypes, createInitialState(): PressState { @@ -219,14 +230,15 @@ const PressResponder = { shouldSkipMouseAfterTouch: false, }; }, - handleEvent( - context: EventResponderContext, + onEvent( + event: ResponderEvent, + context: ResponderContext, props: PressProps, state: PressState, ): void { - const {eventTarget, eventType, event} = context; + const {target, type, nativeEvent} = event; - switch (eventType) { + switch (type) { /** * Respond to pointer events and fall back to mouse. */ @@ -234,29 +246,29 @@ const PressResponder = { case 'mousedown': { if ( !state.isPressed && - !context.isTargetOwned(eventTarget) && + !context.hasOwnership() && !state.shouldSkipMouseAfterTouch ) { if ( - (event: any).pointerType === 'mouse' || - eventType === 'mousedown' + (nativeEvent: any).pointerType === 'mouse' || + type === 'mousedown' ) { if ( // Ignore right- and middle-clicks - event.button === 1 || - event.button === 2 || + nativeEvent.button === 1 || + nativeEvent.button === 2 || // Ignore pressing on hit slop area with mouse context.isPositionWithinTouchHitTarget( - (event: any).x, - (event: any).y, + (nativeEvent: any).x, + (nativeEvent: any).y, ) ) { return; } } - state.pressTarget = eventTarget; + state.pressTarget = target; dispatchPressStartEvents(context, props, state); - context.addRootEventTypes(rootEventTypes); + context.addRootEventTypes(target.ownerDocument, rootEventTypes); } break; } @@ -273,7 +285,7 @@ const PressResponder = { dispatchPressEndEvents(context, props, state); if (state.pressTarget !== null && props.onPress) { - if (context.isTargetWithinElement(eventTarget, state.pressTarget)) { + if (context.isTargetWithinElement(target, state.pressTarget)) { if ( !( wasLongPressed && @@ -303,16 +315,16 @@ const PressResponder = { * support for pointer events. */ case 'touchstart': { - if (!state.isPressed && !context.isTargetOwned(eventTarget)) { + if (!state.isPressed && !context.hasOwnership()) { // We bail out of polyfilling anchor tags, given the same heuristics // explained above in regards to needing to use click events. - if (isAnchorTagElement(eventTarget)) { + if (isAnchorTagElement(target)) { state.isAnchorTouched = true; return; } - state.pressTarget = eventTarget; + state.pressTarget = target; dispatchPressStartEvents(context, props, state); - context.addRootEventTypes(rootEventTypes); + context.addRootEventTypes(target.ownerDocument, rootEventTypes); } break; } @@ -326,17 +338,17 @@ const PressResponder = { dispatchPressEndEvents(context, props, state); - if (eventType !== 'touchcancel' && props.onPress) { + if (type !== 'touchcancel' && props.onPress) { // Find if the X/Y of the end touch is still that of the original target - const changedTouch = (event: any).changedTouches[0]; - const doc = (eventTarget: any).ownerDocument; - const target = doc.elementFromPoint( + const changedTouch = (nativeEvent: any).changedTouches[0]; + const doc = (target: any).ownerDocument; + const fromTarget = doc.elementFromPoint( changedTouch.screenX, changedTouch.screenY, ); if ( - target !== null && - context.isTargetWithinEventComponent(target) + fromTarget !== null && + context.isTargetWithinEventComponent(fromTarget) ) { if ( !( @@ -363,21 +375,21 @@ const PressResponder = { if ( !state.isPressed && !state.isLongPressed && - !context.isTargetOwned(eventTarget) && - isValidKeyPress((event: any).key) + !context.hasOwnership() && + isValidKeyPress((nativeEvent: any).key) ) { // Prevent spacebar press from scrolling the window - if ((event: any).key === ' ') { - (event: any).preventDefault(); + if ((nativeEvent: any).key === ' ') { + (nativeEvent: any).preventDefault(); } - state.pressTarget = eventTarget; + state.pressTarget = target; dispatchPressStartEvents(context, props, state); - context.addRootEventTypes(rootEventTypes); + context.addRootEventTypes(target.ownerDocument, rootEventTypes); } break; } case 'keyup': { - if (state.isPressed && isValidKeyPress((event: any).key)) { + if (state.isPressed && isValidKeyPress((nativeEvent: any).key)) { const wasLongPressed = state.isLongPressed; dispatchPressEndEvents(context, props, state); if (state.pressTarget !== null && props.onPress) { @@ -410,12 +422,24 @@ const PressResponder = { case 'click': { if (state.defaultPrevented) { - (event: any).preventDefault(); + (nativeEvent: any).preventDefault(); state.defaultPrevented = false; } } } }, + // TODO This method doesn't work as of yet + onUnmount(context: ResponderContext, props: PressProps, state: PressState) { + unmountResponder(context, props, state); + }, + // TODO This method doesn't work as of yet + onOwnershipChange( + context: ResponderContext, + props: PressProps, + state: PressState, + ) { + unmountResponder(context, props, state); + }, }; export default { diff --git a/packages/react-events/src/Swipe.js b/packages/react-events/src/Swipe.js index 85df2cca3f10e..ed211c939ef48 100644 --- a/packages/react-events/src/Swipe.js +++ b/packages/react-events/src/Swipe.js @@ -7,7 +7,7 @@ * @flow */ -import type {EventResponderContext} from 'events/EventTypes'; +import type {ResponderEvent, ResponderContext} from 'events/EventTypes'; import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols'; const targetEventTypes = ['pointerdown', 'pointercancel']; @@ -52,7 +52,7 @@ function createSwipeEvent( } function dispatchSwipeEvent( - context: EventResponderContext, + context: ResponderContext, name: SwipeEventType, listener: SwipeEvent => void, state: SwipeState, @@ -91,21 +91,22 @@ const SwipeResponder = { y: 0, }; }, - handleEvent( - context: EventResponderContext, + onEvent( + event: ResponderEvent, + context: ResponderContext, props: Object, state: SwipeState, ): void { - const {eventTarget, eventType, event} = context; + const {target, type, nativeEvent} = event; - switch (eventType) { + switch (type) { case 'touchstart': case 'mousedown': case 'pointerdown': { - if (!state.isSwiping && !context.isTargetOwned(eventTarget)) { + if (!state.isSwiping && !context.hasOwnership()) { let obj = event; - if (eventType === 'touchstart') { - obj = (event: any).targetTouches[0]; + if (type === 'touchstart') { + obj = (nativeEvent: any).targetTouches[0]; state.touchId = obj.identifier; } const x = (obj: any).screenX; @@ -114,7 +115,7 @@ const SwipeResponder = { let shouldEnableSwiping = true; if (props.onShouldClaimOwnership && props.onShouldClaimOwnership()) { - shouldEnableSwiping = context.requestOwnership(eventTarget); + shouldEnableSwiping = context.requestOwnership(); } if (shouldEnableSwiping) { state.isSwiping = true; @@ -122,8 +123,8 @@ const SwipeResponder = { state.startY = y; state.x = x; state.y = y; - state.swipeTarget = eventTarget; - context.addRootEventTypes(rootEventTypes); + state.swipeTarget = target; + context.addRootEventTypes(target.ownerDocument, rootEventTypes); } else { state.touchId = null; } @@ -133,13 +134,13 @@ const SwipeResponder = { case 'touchmove': case 'mousemove': case 'pointermove': { - if (context.isPassive()) { + if (event.passive) { return; } if (state.isSwiping) { let obj = null; - if (eventType === 'touchmove') { - const targetTouches = (event: any).targetTouches; + if (type === 'touchmove') { + const targetTouches = (nativeEvent: any).targetTouches; for (let i = 0; i < targetTouches.length; i++) { if (state.touchId === targetTouches[i].identifier) { obj = targetTouches[i]; @@ -147,7 +148,7 @@ const SwipeResponder = { } } } else { - obj = event; + obj = nativeEvent; } if (obj === null) { state.isSwiping = false; @@ -178,7 +179,7 @@ const SwipeResponder = { false, eventData, ); - (event: any).preventDefault(); + (nativeEvent: any).preventDefault(); } } break; @@ -193,7 +194,7 @@ const SwipeResponder = { return; } if (props.onShouldClaimOwnership) { - context.releaseOwnership(state.swipeTarget); + context.releaseOwnership(); } const direction = state.direction; const lastDirection = state.lastDirection; diff --git a/packages/react-events/src/__tests__/TouchHitTarget-test.internal.js b/packages/react-events/src/__tests__/TouchHitTarget-test.internal.js index e47f7f3cb4714..d30a6c92c2735 100644 --- a/packages/react-events/src/__tests__/TouchHitTarget-test.internal.js +++ b/packages/react-events/src/__tests__/TouchHitTarget-test.internal.js @@ -22,7 +22,7 @@ let TouchHitTarget; const noOpResponder = { targetEventTypes: [], - handleEvent() {}, + onEvent() {}, }; function createReactEventComponent() { diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 35b62021d574f..4448f129fbff1 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -624,6 +624,7 @@ export function createFiberFromEventComponent( fiber.elementType = eventComponent; fiber.type = eventComponent; fiber.stateNode = { + context: null, props: pendingProps, state: null, }; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 52ce27f9dfb10..0988ea8d1efbc 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -7,6 +7,8 @@ * @flow */ +import type {ResponderEvent, ResponderContext} from 'events/EventTypes'; + export type ReactNode = | React$Element | ReactPortal @@ -88,7 +90,18 @@ export type ReactEventResponderEventType = export type ReactEventResponder = { targetEventTypes: Array, createInitialState?: (props: Object) => Object, - handleEvent: (context: Object, props: Object, state: Object) => void, + onEvent: ( + event: ResponderEvent, + context: ResponderContext, + props: Object, + state: Object, + ) => void, + onUnmount: (context: ResponderContext, props: Object, state: Object) => void, + onOwnershipChange: ( + context: ResponderContext, + props: Object, + state: Object, + ) => void, }; export type ReactEventComponent = {|