From 509889119360ed83ca6ef3f83bcf01e5aa7dcd81 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 23 Jul 2019 23:46:44 +0100 Subject: [PATCH] [Flare] Redesign core event system (#16163) --- packages/events/ReactGenericBatching.js | 6 +- packages/react-art/src/ReactARTHostConfig.js | 28 +- .../react-debug-tools/src/ReactDebugHooks.js | 4 +- .../react-dom/src/client/ReactDOMComponent.js | 5 + .../src/client/ReactDOMHostConfig.js | 102 +- .../src/events/DOMEventResponderSystem.js | 618 +++--- .../DOMEventResponderSystem-test.internal.js | 462 ++--- .../src/server/ReactPartialRenderer.js | 32 +- .../src/server/ReactPartialRendererHooks.js | 2 +- .../shared/ReactControlledValuePropTypes.js | 4 +- packages/react-events/README.md | 63 +- packages/react-events/docs/Press.md | 2 +- packages/react-events/focus-scope.js | 4 +- packages/react-events/src/dom/Drag.js | 124 +- packages/react-events/src/dom/Focus.js | 147 +- packages/react-events/src/dom/FocusScope.js | 28 +- packages/react-events/src/dom/Hover.js | 96 +- packages/react-events/src/dom/Input.js | 42 +- packages/react-events/src/dom/Press.js | 214 +-- packages/react-events/src/dom/Scroll.js | 52 +- packages/react-events/src/dom/Swipe.js | 64 +- .../src/dom/__tests__/Drag-test.internal.js | 91 +- .../src/dom/__tests__/Focus-test.internal.js | 178 +- .../dom/__tests__/FocusScope-test.internal.js | 88 +- .../src/dom/__tests__/Hover-test.internal.js | 326 ++-- .../src/dom/__tests__/Input-test.internal.js | 442 +++-- .../src/dom/__tests__/Press-test.internal.js | 1679 ++++++++++------- .../src/dom/__tests__/Scroll-test.internal.js | 60 +- packages/react-events/src/rn/Press.js | 158 +- .../src/ReactFabricEventResponderSystem.js | 599 +++--- .../src/ReactFabricHostConfig.js | 52 +- .../src/ReactNativeHostConfig.js | 38 +- .../src/ReactNativeTypes.js | 8 +- .../src/createReactNoop.js | 30 +- packages/react-reconciler/src/ReactFiber.js | 47 +- .../src/ReactFiberBeginWork.js | 59 +- .../src/ReactFiberCommitWork.js | 43 +- .../src/ReactFiberCompleteWork.js | 223 ++- .../react-reconciler/src/ReactFiberEvents.js | 143 +- .../react-reconciler/src/ReactFiberHooks.js | 47 +- .../src/ReactFiberHostContext.js | 22 +- .../src/ReactFiberNewContext.js | 3 +- .../src/ReactFiberUnwindWork.js | 16 +- .../ReactFiberEvents-test.internal.js | 775 -------- .../ReactFiberHostContext-test.internal.js | 3 - .../src/forks/ReactFiberHostConfig.custom.js | 8 +- .../src/ReactShallowRenderer.js | 2 +- .../src/ReactTestHostConfig.js | 37 +- packages/react/src/React.js | 8 +- packages/react/src/ReactHooks.js | 21 +- packages/shared/ReactDOMTypes.js | 17 +- packages/shared/ReactSymbols.js | 6 +- packages/shared/ReactTypes.js | 32 +- packages/shared/ReactWorkTags.js | 6 +- packages/shared/createEventComponent.js | 30 - packages/shared/createEventResponder.js | 42 + packages/shared/getComponentName.js | 9 - packages/shared/isValidElementType.js | 6 +- scripts/error-codes/codes.json | 4 +- 59 files changed, 3418 insertions(+), 4039 deletions(-) delete mode 100644 packages/react-reconciler/src/__tests__/ReactFiberEvents-test.internal.js delete mode 100644 packages/shared/createEventComponent.js create mode 100644 packages/shared/createEventResponder.js diff --git a/packages/events/ReactGenericBatching.js b/packages/events/ReactGenericBatching.js index b65b40006edb..e92f15b7af95 100644 --- a/packages/events/ReactGenericBatching.js +++ b/packages/events/ReactGenericBatching.js @@ -59,15 +59,15 @@ export function batchedUpdates(fn, bookkeeping) { } } -export function batchedEventUpdates(fn, bookkeeping) { +export function batchedEventUpdates(fn, a, b) { if (isInsideEventHandler) { // If we are currently inside another batch, we need to wait until it // fully completes before restoring state. - return fn(bookkeeping); + return fn(a, b); } isInsideEventHandler = true; try { - return batchedEventUpdatesImpl(fn, bookkeeping); + return batchedEventUpdatesImpl(fn, a, b); } finally { isInsideEventHandler = false; finishEventHandler(); diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index 11df9690f7fd..231ee89205be 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -10,7 +10,10 @@ import Mode from 'art/modes/current'; import invariant from 'shared/invariant'; import {TYPES, EVENT_TYPES, childrenAsString} from './ReactARTInternals'; -import type {ReactEventComponentInstance} from 'shared/ReactTypes'; +import type { + ReactEventResponder, + ReactEventResponderInstance, +} from 'shared/ReactTypes'; const pooledTransform = new Transform(); @@ -329,10 +332,6 @@ export function getChildHostContext() { return NO_CONTEXT; } -export function getChildHostContextForEventComponent() { - return NO_CONTEXT; -} - export const scheduleTimeout = setTimeout; export const cancelTimeout = clearTimeout; export const noTimeout = -1; @@ -427,20 +426,19 @@ export function unhideTextInstance(textInstance, text): void { // Noop } -export function mountEventComponent( - eventComponentInstance: ReactEventComponentInstance, -) { - throw new Error('Not yet implemented.'); -} - -export function updateEventComponent( - eventComponentInstance: ReactEventComponentInstance, +export function mountResponderInstance( + responder: ReactEventResponder, + responderInstance: ReactEventResponderInstance, + props: Object, + state: Object, + instance: Object, + rootContainerInstance: Object, ) { throw new Error('Not yet implemented.'); } -export function unmountEventComponent( - eventComponentInstance: ReactEventComponentInstance, +export function unmountResponderInstance( + responderInstance: ReactEventResponderInstance, ): void { throw new Error('Not yet implemented.'); } diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 2e029d382e19..b49813d70850 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -215,7 +215,7 @@ function useMemo( return value; } -function useEvent() { +function useListener() { throw new Error('TODO: not yet implemented'); } @@ -231,7 +231,7 @@ const Dispatcher: DispatcherType = { useReducer, useRef, useState, - useEvent, + useListener, }; // Inspect diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js index af580be510be..55db92a52f3c 100644 --- a/packages/react-dom/src/client/ReactDOMComponent.js +++ b/packages/react-dom/src/client/ReactDOMComponent.js @@ -98,6 +98,7 @@ const AUTOFOCUS = 'autoFocus'; const CHILDREN = 'children'; const STYLE = 'style'; const HTML = '__html'; +const RESPONDERS = 'responders'; const {html: HTML_NAMESPACE} = Namespaces; @@ -340,6 +341,7 @@ function setInitialDOMProperties( setTextContent(domElement, '' + nextProp); } } else if ( + (enableFlareAPI && propKey === RESPONDERS) || propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING ) { @@ -696,6 +698,7 @@ export function diffProperties( } else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) { // Noop. This is handled by the clear text mechanism. } else if ( + (enableFlareAPI && propKey === RESPONDERS) || propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING ) { @@ -787,6 +790,7 @@ export function diffProperties( (updatePayload = updatePayload || []).push(propKey, '' + nextProp); } } else if ( + (enableFlareAPI && propKey === RESPONDERS) || propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING ) { @@ -1041,6 +1045,7 @@ export function diffHydratedProperties( if (suppressHydrationWarning) { // Don't bother comparing. We're ignoring all these warnings. } else if ( + (enableFlareAPI && propKey === RESPONDERS) || propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING || // Controlled attributes are not validated diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 901b2413b955..48425c1c8d93 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -44,11 +44,11 @@ import dangerousStyleValue from '../shared/dangerousStyleValue'; import type {DOMContainer} from './ReactDOM'; import type { ReactDOMEventResponder, - ReactDOMEventComponentInstance, + ReactDOMEventResponderInstance, ReactDOMFundamentalComponentInstance, } from 'shared/ReactDOMTypes'; import { - addRootEventTypesForComponentInstance, + addRootEventTypesForResponderInstance, mountEventResponder, unmountEventResponder, } from '../events/DOMEventResponderSystem'; @@ -90,9 +90,6 @@ export type PublicInstance = Element | Text; type HostContextDev = { namespace: string, ancestorInfo: mixed, - eventData: null | {| - isEventComponent?: boolean, - |}, }; type HostContextProd = string; export type HostContext = HostContextDev | HostContextProd; @@ -106,7 +103,6 @@ import { enableFlareAPI, enableFundamentalAPI, } from 'shared/ReactFeatureFlags'; -import warning from 'shared/warning'; let SUPPRESS_HYDRATION_WARNING; if (__DEV__) { @@ -164,7 +160,7 @@ export function getRootHostContext( if (__DEV__) { const validatedTag = type.toLowerCase(); const ancestorInfo = updatedAncestorInfo(null, validatedTag); - return {namespace, ancestorInfo, eventData: null}; + return {namespace, ancestorInfo}; } return namespace; } @@ -181,26 +177,12 @@ export function getChildHostContext( parentHostContextDev.ancestorInfo, type, ); - return {namespace, ancestorInfo, eventData: null}; + return {namespace, ancestorInfo}; } const parentNamespace = ((parentHostContext: any): HostContextProd); return getChildNamespace(parentNamespace, type); } -export function getChildHostContextForEventComponent( - parentHostContext: HostContext, -): HostContext { - if (__DEV__) { - const parentHostContextDev = ((parentHostContext: any): HostContextDev); - const {namespace, ancestorInfo} = parentHostContextDev; - const eventData = { - isEventComponent: true, - }; - return {namespace, ancestorInfo, eventData}; - } - return parentHostContext; -} - export function getPublicInstance(instance: Instance): * { return instance; } @@ -332,17 +314,6 @@ export function createTextInstance( if (__DEV__) { const hostContextDev = ((hostContext: any): HostContextDev); validateDOMNesting(null, text, hostContextDev.ancestorInfo); - if (enableFlareAPI) { - const eventData = hostContextDev.eventData; - if (eventData !== null) { - warning( - !eventData.isEventComponent, - 'validateDOMNesting: React event components cannot have text DOM nodes as children. ' + - 'Wrap the child text "%s" in an element.', - text, - ); - } - } } const textNode: TextInstance = createTextNode(text, rootContainerInstance); precacheFiberNode(internalInstanceHandle, textNode); @@ -844,44 +815,43 @@ export function didNotFindHydratableSuspenseInstance( } } -export function mountEventComponent( - eventComponentInstance: ReactDOMEventComponentInstance, -): void { - if (enableFlareAPI) { - const rootContainerInstance = ((eventComponentInstance.rootInstance: any): Container); - const doc = rootContainerInstance.ownerDocument; - const documentBody = doc.body || doc; - const responder = eventComponentInstance.responder; - const { - rootEventTypes, - targetEventTypes, - } = ((responder: any): ReactDOMEventResponder); - if (targetEventTypes !== undefined) { - listenToEventResponderEventTypes(targetEventTypes, documentBody); - } - if (rootEventTypes !== undefined) { - addRootEventTypesForComponentInstance( - eventComponentInstance, - rootEventTypes, - ); - listenToEventResponderEventTypes(rootEventTypes, documentBody); - } - mountEventResponder(eventComponentInstance); - } -} - -export function updateEventComponent( - eventComponentInstance: ReactDOMEventComponentInstance, -): void { - // NO-OP, why might use this in the future +export function mountResponderInstance( + responder: ReactDOMEventResponder, + responderInstance: ReactDOMEventResponderInstance, + responderProps: Object, + responderState: Object, + instance: Instance, + rootContainerInstance: Container, +): ReactDOMEventResponderInstance { + // Listen to events + const doc = rootContainerInstance.ownerDocument; + const documentBody = doc.body || doc; + const { + rootEventTypes, + targetEventTypes, + } = ((responder: any): ReactDOMEventResponder); + if (targetEventTypes !== null) { + listenToEventResponderEventTypes(targetEventTypes, documentBody); + } + if (rootEventTypes !== null) { + addRootEventTypesForResponderInstance(responderInstance, rootEventTypes); + listenToEventResponderEventTypes(rootEventTypes, documentBody); + } + mountEventResponder( + responder, + responderInstance, + responderProps, + responderState, + ); + return responderInstance; } -export function unmountEventComponent( - eventComponentInstance: ReactDOMEventComponentInstance, +export function unmountResponderInstance( + responderInstance: ReactDOMEventResponderInstance, ): void { if (enableFlareAPI) { // TODO stop listening to targetEventTypes - unmountEventResponder(eventComponentInstance); + unmountEventResponder(responderInstance); } } diff --git a/packages/react-dom/src/events/DOMEventResponderSystem.js b/packages/react-dom/src/events/DOMEventResponderSystem.js index a318f90ab2ce..5c8885ecb93b 100644 --- a/packages/react-dom/src/events/DOMEventResponderSystem.js +++ b/packages/react-dom/src/events/DOMEventResponderSystem.js @@ -13,14 +13,15 @@ import { } from 'events/EventSystemFlags'; import type {AnyNativeEvent} from 'events/PluginModuleType'; import { - EventComponent, HostComponent, FunctionComponent, + MemoComponent, + ForwardRef, } from 'shared/ReactWorkTags'; import type {EventPriority} from 'shared/ReactTypes'; import type { ReactDOMEventResponder, - ReactDOMEventComponentInstance, + ReactDOMEventResponderInstance, ReactDOMResponderContext, ReactDOMResponderEvent, } from 'shared/ReactDOMTypes'; @@ -65,17 +66,11 @@ export function setListenToResponderEventTypes( listenToResponderEventTypesImpl = _listenToResponderEventTypesImpl; } -type EventObjectType = $Shape; - -type EventQueue = { - events: Array, - eventPriority: EventPriority, -}; - -type PartialEventObject = { - target: Element | Document, - type: string, -}; +type EventQueueItem = {| + listeners: Array<(val: any) => void>, + value: any, +|}; +type EventQueue = Array; type ResponderTimeout = {| id: TimeoutID, @@ -83,114 +78,59 @@ type ResponderTimeout = {| |}; type ResponderTimer = {| - isHook: boolean, - instance: ReactDOMEventComponentInstance, + instance: ReactDOMEventResponderInstance, func: () => void, id: number, timeStamp: number, |}; const activeTimeouts: Map = new Map(); -const rootEventTypesToEventComponentInstances: Map< +const rootEventTypesToEventResponderInstances: Map< DOMTopLevelEventType | string, - Set, + Set, > = new Map(); -const ownershipChangeListeners: Set = new Set(); -const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; -const eventListeners: - | WeakMap - | Map< - $Shape, - ($Shape) => void, - > = new PossiblyWeakMap(); +const ownershipChangeListeners: Set = new Set(); let globalOwner = null; -let continueLocalPropagation = false; let currentTimeStamp = 0; let currentTimers = new Map(); -let currentInstance: null | ReactDOMEventComponentInstance = null; +let currentInstance: null | ReactDOMEventResponderInstance = null; let currentEventQueue: null | EventQueue = null; +let currentEventQueuePriority: EventPriority = ContinuousEvent; let currentTimerIDCounter = 0; let currentDocument: null | Document = null; -let currentlyInHook = false; const eventResponderContext: ReactDOMResponderContext = { dispatchEvent( - possibleEventObject: Object, - listener: ($Shape) => void, + eventProp: string, + eventValue: any, eventPriority: EventPriority, ): void { validateResponderContext(); - const {target, type, timeStamp} = possibleEventObject; - - if (target == null || type == null || timeStamp == null) { - throw new Error( - 'context.dispatchEvent: "target", "timeStamp", and "type" fields on event object are required.', + validateEventValue(eventValue); + if (eventPriority < currentEventQueuePriority) { + currentEventQueuePriority = eventPriority; + } + const responderInstance = ((currentInstance: any): ReactDOMEventResponderInstance); + const target = responderInstance.fiber; + const responder = responderInstance.responder; + const listeners = collectListeners(eventProp, responder, target); + if (listeners.length !== 0) { + ((currentEventQueue: any): EventQueue).push( + createEventQueueItem(eventValue, listeners), ); } - const showWarning = name => { - if (__DEV__) { - warning( - false, - '%s is not available on event objects created from event responder modules (React Flare). ' + - 'Try wrapping in a conditional, i.e. `if (event.type !== "press") { event.%s }`', - name, - name, - ); - } - }; - possibleEventObject.preventDefault = () => { - if (__DEV__) { - showWarning('preventDefault()'); - } - }; - possibleEventObject.stopPropagation = () => { - if (__DEV__) { - showWarning('stopPropagation()'); - } - }; - possibleEventObject.isDefaultPrevented = () => { - if (__DEV__) { - showWarning('isDefaultPrevented()'); - } - }; - possibleEventObject.isPropagationStopped = () => { - if (__DEV__) { - showWarning('isPropagationStopped()'); - } - }; - // $FlowFixMe: we don't need value, Flow thinks we do - Object.defineProperty(possibleEventObject, 'nativeEvent', { - get() { - if (__DEV__) { - showWarning('nativeEvent'); - } - }, - }); - - const eventObject = ((possibleEventObject: any): $Shape< - PartialEventObject, - >); - const eventQueue = ((currentEventQueue: any): EventQueue); - eventQueue.eventPriority = eventPriority; - eventListeners.set(eventObject, listener); - eventQueue.events.push(eventObject); }, - isTargetWithinEventComponent(target: Element | Document): boolean { + isTargetWithinResponder(target: Element | Document): boolean { validateResponderContext(); if (target != null) { let fiber = getClosestInstanceFromNode(target); - const currentFiber = ((currentInstance: any): ReactDOMEventComponentInstance) - .currentFiber; + const responderFiber = ((currentInstance: any): ReactDOMEventResponderInstance) + .fiber; while (fiber !== null) { - const stateNode = fiber.stateNode; - if (fiber.tag === EventComponent && stateNode !== null) { - // Switch to the current fiber tree - fiber = stateNode.currentFiber; - } - if (fiber === currentFiber || stateNode === currentInstance) { + if (fiber === responderFiber || fiber.alternate === responderFiber) { return true; } fiber = fiber.return; @@ -198,28 +138,21 @@ const eventResponderContext: ReactDOMResponderContext = { } return false; }, - isTargetWithinEventResponderScope(target: Element | Document): boolean { + isTargetWithinResponderScope(target: Element | Document): boolean { validateResponderContext(); - const componentInstance = ((currentInstance: any): ReactDOMEventComponentInstance); + const componentInstance = ((currentInstance: any): ReactDOMEventResponderInstance); const responder = componentInstance.responder; if (target != null) { let fiber = getClosestInstanceFromNode(target); - const currentFiber = ((currentInstance: any): ReactDOMEventComponentInstance) - .currentFiber; + const responderFiber = ((currentInstance: any): ReactDOMEventResponderInstance) + .fiber; + while (fiber !== null) { - const stateNode = fiber.stateNode; - if (fiber.tag === EventComponent && stateNode !== null) { - // Switch to the current fiber tree - fiber = stateNode.currentFiber; - } - if (fiber === currentFiber || stateNode === currentInstance) { + if (fiber === responderFiber || fiber.alternate === responderFiber) { return true; } - if ( - fiber.tag === EventComponent && - (stateNode === null || stateNode.responder === responder) - ) { + if (doesFiberHaveResponder(fiber, responder)) { return false; } fiber = fiber.return; @@ -251,25 +184,25 @@ const eventResponderContext: ReactDOMResponderContext = { listenToResponderEventTypesImpl(rootEventTypes, activeDocument); for (let i = 0; i < rootEventTypes.length; i++) { const rootEventType = rootEventTypes[i]; - const eventComponentInstance = ((currentInstance: any): ReactDOMEventComponentInstance); - registerRootEventType(rootEventType, eventComponentInstance); + const eventResponderInstance = ((currentInstance: any): ReactDOMEventResponderInstance); + registerRootEventType(rootEventType, eventResponderInstance); } }, removeRootEventTypes(rootEventTypes: Array): void { validateResponderContext(); for (let i = 0; i < rootEventTypes.length; i++) { const rootEventType = rootEventTypes[i]; - let rootEventComponents = rootEventTypesToEventComponentInstances.get( + let rootEventComponents = rootEventTypesToEventResponderInstances.get( rootEventType, ); - let rootEventTypesSet = ((currentInstance: any): ReactDOMEventComponentInstance) + let rootEventTypesSet = ((currentInstance: any): ReactDOMEventResponderInstance) .rootEventTypes; if (rootEventTypesSet !== null) { rootEventTypesSet.delete(rootEventType); } if (rootEventComponents !== undefined) { rootEventComponents.delete( - ((currentInstance: any): ReactDOMEventComponentInstance), + ((currentInstance: any): ReactDOMEventResponderInstance), ); } } @@ -289,8 +222,8 @@ const eventResponderContext: ReactDOMResponderContext = { }, releaseOwnership(): boolean { validateResponderContext(); - return releaseOwnershipForEventComponentInstance( - ((currentInstance: any): ReactDOMEventComponentInstance), + return releaseOwnershipForEventResponderInstance( + ((currentInstance: any): ReactDOMEventResponderInstance), ); }, setTimeout(func: () => void, delay): number { @@ -313,8 +246,7 @@ const eventResponderContext: ReactDOMResponderContext = { currentTimers.set(delay, timeout); } timeout.timers.set(timerId, { - isHook: currentlyInHook, - instance: ((currentInstance: any): ReactDOMEventComponentInstance), + instance: ((currentInstance: any): ReactDOMEventResponderInstance), func, id: timerId, timeStamp: currentTimeStamp, @@ -334,11 +266,22 @@ const eventResponderContext: ReactDOMResponderContext = { } } }, - getFocusableElementsInScope(): Array { + getFocusableElementsInScope(deep: boolean): Array { validateResponderContext(); const focusableElements = []; - const eventComponentInstance = ((currentInstance: any): ReactDOMEventComponentInstance); - const child = ((eventComponentInstance.currentFiber: any): Fiber).child; + const eventResponderInstance = ((currentInstance: any): ReactDOMEventResponderInstance); + const currentResponder = eventResponderInstance.responder; + let focusScopeFiber = eventResponderInstance.fiber; + if (deep) { + let deepNode = focusScopeFiber.return; + while (deepNode !== null) { + if (doesFiberHaveResponder(deepNode, currentResponder)) { + focusScopeFiber = deepNode; + } + deepNode = deepNode.return; + } + } + const child = focusScopeFiber.child; if (child !== null) { collectFocusableElements(child, focusableElements); @@ -354,22 +297,11 @@ const eventResponderContext: ReactDOMResponderContext = { isTargetWithinHostComponent( target: Element | Document, elementType: string, - deep: boolean, ): boolean { validateResponderContext(); let fiber = getClosestInstanceFromNode(target); - const currentResponder = ((currentInstance: any): ReactDOMEventComponentInstance) - .responder; while (fiber !== null) { - const stateNode = fiber.stateNode; - if ( - !deep && - (fiber.tag === EventComponent && - (stateNode === null || stateNode.responder === currentResponder)) - ) { - return false; - } if (fiber.tag === HostComponent && fiber.type === elementType) { return true; } @@ -377,16 +309,60 @@ const eventResponderContext: ReactDOMResponderContext = { } return false; }, - continueLocalPropagation() { - validateResponderContext(); - continueLocalPropagation = true; - }, - isRespondingToHook() { - return currentlyInHook; - }, enqueueStateRestore, }; +function validateEventValue(eventValue: any): void { + if (typeof eventValue === 'object' && eventValue !== null) { + const {target, type, timeStamp} = eventValue; + + if (target == null || type == null || timeStamp == null) { + throw new Error( + 'context.dispatchEvent: "target", "timeStamp", and "type" fields on event object are required.', + ); + } + const showWarning = name => { + if (__DEV__) { + warning( + false, + '%s is not available on event objects created from event responder modules (React Flare). ' + + 'Try wrapping in a conditional, i.e. `if (event.type !== "press") { event.%s }`', + name, + name, + ); + } + }; + eventValue.preventDefault = () => { + if (__DEV__) { + showWarning('preventDefault()'); + } + }; + eventValue.stopPropagation = () => { + if (__DEV__) { + showWarning('stopPropagation()'); + } + }; + eventValue.isDefaultPrevented = () => { + if (__DEV__) { + showWarning('isDefaultPrevented()'); + } + }; + eventValue.isPropagationStopped = () => { + if (__DEV__) { + showWarning('isPropagationStopped()'); + } + }; + // $FlowFixMe: we don't need value, Flow thinks we do + Object.defineProperty(eventValue, 'nativeEvent', { + get() { + if (__DEV__) { + showWarning('nativeEvent'); + } + }, + }); + } +} + function collectFocusableElements( node: Fiber, focusableElements: Array, @@ -414,14 +390,40 @@ function collectFocusableElements( } } +function createEventQueueItem( + value: any, + listeners: Array<(val: any) => void>, +): EventQueueItem { + return { + value, + listeners, + }; +} + +function doesFiberHaveResponder( + fiber: Fiber, + responder: ReactDOMEventResponder, +): boolean { + if (fiber.tag === HostComponent) { + const dependencies = fiber.dependencies; + if (dependencies !== null) { + const respondersMap = dependencies.responders; + if (respondersMap !== null && respondersMap.has(responder)) { + return true; + } + } + } + return false; +} + function getActiveDocument(): Document { return ((currentDocument: any): Document); } -function releaseOwnershipForEventComponentInstance( - eventComponentInstance: ReactDOMEventComponentInstance, +function releaseOwnershipForEventResponderInstance( + eventResponderInstance: ReactDOMEventResponderInstance, ): boolean { - if (globalOwner === eventComponentInstance) { + if (globalOwner === eventResponderInstance) { globalOwner = null; triggerOwnershipListeners(); return true; @@ -461,13 +463,13 @@ function processTimers( delay: number, ): void { const timersArr = Array.from(timers.values()); - currentEventQueue = createEventQueue(); + currentEventQueuePriority = ContinuousEvent; try { for (let i = 0; i < timersArr.length; i++) { - const {isHook, instance, func, id, timeStamp} = timersArr[i]; + const {instance, func, id, timeStamp} = timersArr[i]; currentInstance = instance; + currentEventQueue = []; currentTimeStamp = timeStamp + delay; - currentlyInHook = isHook; try { func(); } finally { @@ -517,39 +519,83 @@ function createDOMResponderEvent( }; } -function createEventQueue(): EventQueue { - return { - events: [], - eventPriority: ContinuousEvent, - }; -} - -function processEvent(event: $Shape): void { - const type = event.type; - const listener = ((eventListeners.get(event): any): ( - $Shape, - ) => void); - invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event); +function collectListeners( + eventProp: string, + eventResponder: ReactDOMEventResponder, + target: Fiber, +): Array<(any) => void> { + const eventListeners = []; + let node = target.return; + nodeTraversal: while (node !== null) { + switch (node.tag) { + case HostComponent: { + const dependencies = node.dependencies; + + if (dependencies !== null) { + const respondersMap = dependencies.responders; + + if (respondersMap !== null && respondersMap.has(eventResponder)) { + break nodeTraversal; + } + } + break; + } + case FunctionComponent: + case MemoComponent: + case ForwardRef: { + const dependencies = node.dependencies; + + if (dependencies !== null) { + const listeners = dependencies.listeners; + + if (listeners !== null) { + for ( + let s = 0, listenersLength = listeners.length; + s < listenersLength; + s++ + ) { + const listener = listeners[s]; + const {responder, props} = listener; + const listenerFunc = props[eventProp]; + + if ( + responder === eventResponder && + typeof listenerFunc === 'function' + ) { + eventListeners.push(listenerFunc); + } + } + } + } + } + } + node = node.return; + } + return eventListeners; } -function processEvents(events: Array): void { - for (let i = 0, length = events.length; i < length; i++) { - processEvent(events[i]); +function processEvents(eventQueue: EventQueue): void { + for (let i = 0, length = eventQueue.length; i < length; i++) { + const {value, listeners} = eventQueue[i]; + for (let s = 0, length2 = listeners.length; s < length2; s++) { + const listener = listeners[s]; + const type = + typeof value === 'object' && value !== null ? value.type : ''; + invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, value); + } } } function processEventQueue(): void { - const {events, eventPriority} = ((currentEventQueue: any): EventQueue); - - if (events.length === 0) { + const eventQueue = ((currentEventQueue: any): EventQueue); + if (eventQueue.length === 0) { return; } - - switch (eventPriority) { + switch (currentEventQueuePriority) { case DiscreteEvent: { flushDiscreteUpdatesIfNeeded(currentTimeStamp); discreteUpdates(() => { - batchedEventUpdates(processEvents, events); + batchedEventUpdates(processEvents, eventQueue); }); break; } @@ -557,15 +603,15 @@ function processEventQueue(): void { if (enableUserBlockingEvents) { runWithPriority( UserBlockingPriority, - batchedEventUpdates.bind(null, processEvents, events), + batchedEventUpdates.bind(null, processEvents, eventQueue), ); } else { - batchedEventUpdates(processEvents, events); + batchedEventUpdates(processEvents, eventQueue); } break; } case ContinuousEvent: { - batchedEventUpdates(processEvents, events); + batchedEventUpdates(processEvents, eventQueue); break; } } @@ -583,36 +629,22 @@ function responderEventTypesContainType( return false; } -function validateEventTargetTypesForResponder( +function validateResponderTargetEventTypes( eventType: string, responder: ReactDOMEventResponder, ): boolean { - const targetEventTypes = responder.targetEventTypes; + const {targetEventTypes} = responder; // Validate the target event type exists on the responder - if (targetEventTypes !== undefined) { + if (targetEventTypes !== null) { return responderEventTypesContainType(targetEventTypes, eventType); } return false; } -function handleTargetEventResponderInstance( - responderEvent: ReactDOMResponderEvent, - eventComponentInstance: ReactDOMEventComponentInstance, - responder: ReactDOMEventResponder, -): void { - const {isHook, props, state} = eventComponentInstance; - const onEvent = responder.onEvent; - if (onEvent !== undefined) { - currentInstance = eventComponentInstance; - currentlyInHook = isHook; - onEvent(responderEvent, eventResponderContext, props, state); - } -} - function validateOwnership( - eventComponentInstance: ReactDOMEventComponentInstance, + responderInstance: ReactDOMEventResponderInstance, ): boolean { - return globalOwner === null || globalOwner === eventComponentInstance; + return globalOwner === null || globalOwner === responderInstance; } function traverseAndHandleEventResponderInstances( @@ -628,8 +660,8 @@ function traverseAndHandleEventResponderInstances( const eventType = isPassive ? topLevelType : topLevelType + '_active'; // Trigger event responders in this order: - // - Bubble target phase - // - Root phase + // - Bubble target responder phase + // - Root responder phase const responderEvent = createDOMResponderEvent( topLevelType, @@ -638,75 +670,31 @@ function traverseAndHandleEventResponderInstances( isPassiveEvent, isPassiveSupported, ); - const responderTargets = new Map(); - const allowLocalPropagation = new Set(); - - // Bubbled event phases have the notion of local propagation. - // This means that the propgation chain can be stopped part of the the way - // through processing event component instances. + const visitedResponders = new Set(); let node = targetFiber; - let currentTarget = nativeEventTarget; while (node !== null) { - const {dependencies, stateNode, tag} = node; - if (tag === HostComponent) { - currentTarget = stateNode; - } else if (tag === EventComponent) { - const eventComponentInstance = stateNode; - if (validateOwnership(eventComponentInstance)) { - const responder = eventComponentInstance.responder; - let responderTarget = responderTargets.get(responder); - let skipCurrentNode = false; - - if (responderTarget === undefined) { - if (validateEventTargetTypesForResponder(eventType, responder)) { - responderTarget = currentTarget; - responderTargets.set(responder, currentTarget); - } else { - skipCurrentNode = true; - } - } else if (allowLocalPropagation.has(responder)) { - // TODO: remove continueLocalPropagation - allowLocalPropagation.delete(responder); - } else { - skipCurrentNode = true; - } - if (!skipCurrentNode) { - responderEvent.responderTarget = ((responderTarget: any): - | Document - | Element); - // Switch to the current fiber tree - node = eventComponentInstance.currentFiber; - handleTargetEventResponderInstance( - responderEvent, - eventComponentInstance, - responder, - ); - // TODO: remove continueLocalPropagation - if (continueLocalPropagation) { - continueLocalPropagation = false; - allowLocalPropagation.add(responder); - } - } - } - } else if (tag === FunctionComponent && dependencies !== null) { - const events = dependencies.events; - if (events !== null) { - for (let i = 0; i < events.length; i++) { - const eventComponentInstance = events[i]; - if (validateOwnership(eventComponentInstance)) { - const responder = eventComponentInstance.responder; - const responderTarget = responderTargets.get(responder); - if (responderTarget !== undefined) { - responderEvent.responderTarget = responderTarget; - handleTargetEventResponderInstance( - responderEvent, - eventComponentInstance, - responder, - ); - // TODO: remove continueLocalPropagation - if (continueLocalPropagation) { - continueLocalPropagation = false; - allowLocalPropagation.add(responder); + const {dependencies, tag} = node; + if (tag === HostComponent && dependencies !== null) { + const respondersMap = dependencies.responders; + if (respondersMap !== null) { + const responderInstances = Array.from(respondersMap.values()); + for (let i = 0, length = responderInstances.length; i < length; i++) { + const responderInstance = responderInstances[i]; + + if (validateOwnership(responderInstance)) { + const {props, responder, state, target} = responderInstance; + if ( + !visitedResponders.has(responder) && + validateResponderTargetEventTypes(eventType, responder) + ) { + const onEvent = responder.onEvent; + visitedResponders.add(responder); + if (onEvent !== null) { + currentInstance = responderInstance; + responderEvent.responderTarget = ((target: any): + | Element + | Document); + onEvent(responderEvent, eventResponderContext, props, state); } } } @@ -716,24 +704,22 @@ function traverseAndHandleEventResponderInstances( node = node.return; } // Root phase - const rootEventInstances = rootEventTypesToEventComponentInstances.get( + const rootEventResponderInstances = rootEventTypesToEventResponderInstances.get( eventType, ); - if (rootEventInstances !== undefined) { - const rootEventComponentInstances = Array.from(rootEventInstances); + if (rootEventResponderInstances !== undefined) { + const responderInstances = Array.from(rootEventResponderInstances); - for (let i = 0; i < rootEventComponentInstances.length; i++) { - const rootEventComponentInstance = rootEventComponentInstances[i]; - if (!validateOwnership(rootEventComponentInstance)) { + for (let i = 0; i < responderInstances.length; i++) { + const responderInstance = responderInstances[i]; + if (!validateOwnership(responderInstance)) { continue; } - const {isHook, props, responder, state} = rootEventComponentInstance; + const {props, responder, state, target} = responderInstance; const onRootEvent = responder.onRootEvent; - if (onRootEvent !== undefined) { - currentInstance = rootEventComponentInstance; - currentlyInHook = isHook; - const responderTarget = responderTargets.get(responder); - responderEvent.responderTarget = responderTarget || null; + if (onRootEvent !== null) { + currentInstance = responderInstance; + responderEvent.responderTarget = ((target: any): Element | Document); onRootEvent(responderEvent, eventResponderContext, props, state); } } @@ -743,40 +729,43 @@ function traverseAndHandleEventResponderInstances( function triggerOwnershipListeners(): void { const listeningInstances = Array.from(ownershipChangeListeners); const previousInstance = currentInstance; - const previouslyInHook = currentlyInHook; - currentEventQueue = createEventQueue(); + const previousEventQueuePriority = currentEventQueuePriority; + const previousEventQueue = currentEventQueue; try { for (let i = 0; i < listeningInstances.length; i++) { const instance = listeningInstances[i]; - const {isHook, props, responder, state} = instance; + const {props, responder, state} = instance; currentInstance = instance; - currentlyInHook = isHook; + currentEventQueuePriority = ContinuousEvent; + currentEventQueue = []; const onOwnershipChange = ((responder: any): ReactDOMEventResponder) .onOwnershipChange; - if (onOwnershipChange !== undefined) { + if (onOwnershipChange !== null) { onOwnershipChange(eventResponderContext, props, state); } } processEventQueue(); } finally { currentInstance = previousInstance; - currentlyInHook = previouslyInHook; + currentEventQueue = previousEventQueue; + currentEventQueuePriority = previousEventQueuePriority; } } export function mountEventResponder( - eventComponentInstance: ReactDOMEventComponentInstance, + responder: ReactDOMEventResponder, + responderInstance: ReactDOMEventResponderInstance, + props: Object, + state: Object, ) { - const responder = ((eventComponentInstance.responder: any): ReactDOMEventResponder); - if (responder.onOwnershipChange !== undefined) { - ownershipChangeListeners.add(eventComponentInstance); + if (responder.onOwnershipChange !== null) { + ownershipChangeListeners.add(responderInstance); } const onMount = responder.onMount; - if (onMount !== undefined) { - let {isHook, props, state} = eventComponentInstance; - currentEventQueue = createEventQueue(); - currentInstance = eventComponentInstance; - currentlyInHook = isHook; + if (onMount !== null) { + currentEventQueuePriority = ContinuousEvent; + currentInstance = responderInstance; + currentEventQueue = []; try { onMount(eventResponderContext, props, state); processEventQueue(); @@ -789,15 +778,15 @@ export function mountEventResponder( } export function unmountEventResponder( - eventComponentInstance: ReactDOMEventComponentInstance, + responderInstance: ReactDOMEventResponderInstance, ): void { - const responder = ((eventComponentInstance.responder: any): ReactDOMEventResponder); + const responder = ((responderInstance.responder: any): ReactDOMEventResponder); const onUnmount = responder.onUnmount; - if (onUnmount !== undefined) { - let {isHook, props, state} = eventComponentInstance; - currentEventQueue = createEventQueue(); - currentInstance = eventComponentInstance; - currentlyInHook = isHook; + if (onUnmount !== null) { + let {props, state} = responderInstance; + currentEventQueue = []; + currentEventQueuePriority = ContinuousEvent; + currentInstance = responderInstance; try { onUnmount(eventResponderContext, props, state); processEventQueue(); @@ -807,27 +796,21 @@ export function unmountEventResponder( currentTimers = null; } } - try { - currentEventQueue = createEventQueue(); - releaseOwnershipForEventComponentInstance(eventComponentInstance); - processEventQueue(); - } finally { - currentEventQueue = null; - } - if (responder.onOwnershipChange !== undefined) { - ownershipChangeListeners.delete(eventComponentInstance); + releaseOwnershipForEventResponderInstance(responderInstance); + if (responder.onOwnershipChange !== null) { + ownershipChangeListeners.delete(responderInstance); } - const rootEventTypesSet = eventComponentInstance.rootEventTypes; + const rootEventTypesSet = responderInstance.rootEventTypes; if (rootEventTypesSet !== null) { const rootEventTypes = Array.from(rootEventTypesSet); for (let i = 0; i < rootEventTypes.length; i++) { const topLevelEventType = rootEventTypes[i]; - let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get( + let rootEventResponderInstances = rootEventTypesToEventResponderInstances.get( topLevelEventType, ); - if (rootEventComponentInstances !== undefined) { - rootEventComponentInstances.delete(eventComponentInstance); + if (rootEventResponderInstances !== undefined) { + rootEventResponderInstances.delete(responderInstance); } } } @@ -835,7 +818,7 @@ export function unmountEventResponder( function validateResponderContext(): void { invariant( - currentEventQueue && currentInstance, + currentInstance !== null, 'An event responder context was used outside of an event cycle. ' + 'Use context.setTimeout() to use asynchronous responder context outside of event cycle .', ); @@ -854,9 +837,10 @@ export function dispatchEventForResponderEventSystem( const previousTimers = currentTimers; const previousTimeStamp = currentTimeStamp; const previousDocument = currentDocument; - const previouslyInHook = currentlyInHook; + const previousEventQueuePriority = currentEventQueuePriority; currentTimers = null; - currentEventQueue = createEventQueue(); + currentEventQueue = []; + currentEventQueuePriority = ContinuousEvent; // nodeType 9 is DOCUMENT_NODE currentDocument = (nativeEventTarget: any).nodeType === 9 @@ -879,38 +863,38 @@ export function dispatchEventForResponderEventSystem( currentEventQueue = previousEventQueue; currentTimeStamp = previousTimeStamp; currentDocument = previousDocument; - currentlyInHook = previouslyInHook; + currentEventQueuePriority = previousEventQueuePriority; } } } -export function addRootEventTypesForComponentInstance( - eventComponentInstance: ReactDOMEventComponentInstance, +export function addRootEventTypesForResponderInstance( + responderInstance: ReactDOMEventResponderInstance, rootEventTypes: Array, ): void { for (let i = 0; i < rootEventTypes.length; i++) { const rootEventType = rootEventTypes[i]; - registerRootEventType(rootEventType, eventComponentInstance); + registerRootEventType(rootEventType, responderInstance); } } function registerRootEventType( rootEventType: string, - eventComponentInstance: ReactDOMEventComponentInstance, + eventResponderInstance: ReactDOMEventResponderInstance, ): void { - let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get( + let rootEventResponderInstances = rootEventTypesToEventResponderInstances.get( rootEventType, ); - if (rootEventComponentInstances === undefined) { - rootEventComponentInstances = new Set(); - rootEventTypesToEventComponentInstances.set( + if (rootEventResponderInstances === undefined) { + rootEventResponderInstances = new Set(); + rootEventTypesToEventResponderInstances.set( rootEventType, - rootEventComponentInstances, + rootEventResponderInstances, ); } - let rootEventTypesSet = eventComponentInstance.rootEventTypes; + let rootEventTypesSet = eventResponderInstance.rootEventTypes; if (rootEventTypesSet === null) { - rootEventTypesSet = eventComponentInstance.rootEventTypes = new Set(); + rootEventTypesSet = eventResponderInstance.rootEventTypes = new Set(); } invariant( !rootEventTypesSet.has(rootEventType), @@ -920,5 +904,5 @@ function registerRootEventType( rootEventType, ); rootEventTypesSet.add(rootEventType); - rootEventComponentInstances.add(eventComponentInstance); + rootEventResponderInstances.add(eventResponderInstance); } 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 f566c164d6c6..9ea8b8ad72a8 100644 --- a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js @@ -17,29 +17,26 @@ let ReactDOM; // now it's an enum but is that what we want? Hard coding this for now. const DiscreteEvent = 0; -function createReactEventComponent({ - targetEventTypes, - rootEventTypes, - getInitialState, +function createEventResponder({ onEvent, onRootEvent, + rootEventTypes, + targetEventTypes, onMount, onUnmount, onOwnershipChange, + getInitialState, }) { - const testEventResponder = { - displayName: 'TestEventComponent', + return React.unstable_createResponder('TestEventComponent', { targetEventTypes, rootEventTypes, - getInitialState, onEvent, onRootEvent, onMount, onUnmount, onOwnershipChange, - }; - - return React.unstable_createEvent(testEventResponder); + getInitialState, + }); } const createEvent = (type, data) => { @@ -83,12 +80,12 @@ describe('DOMEventResponderSystem', () => { container = null; }); - it('the event responder event listeners should fire on click event', () => { + it('the event responders should fire on click event', () => { let eventResponderFiredCount = 0; let eventLog = []; const buttonRef = React.createRef(); - const ClickEventComponent = createReactEventComponent({ + const TestResponder = createEventResponder({ targetEventTypes: ['click'], onEvent: (event, context, props) => { eventResponderFiredCount++; @@ -102,9 +99,9 @@ describe('DOMEventResponderSystem', () => { }); const Test = () => ( - - - + ); ReactDOM.render(, container); @@ -137,7 +134,7 @@ describe('DOMEventResponderSystem', () => { expect(eventResponderFiredCount).toBe(2); }); - it('the event responder event listeners should fire on click event (passive events forced)', () => { + it('the event responders 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; @@ -145,7 +142,7 @@ describe('DOMEventResponderSystem', () => { let eventLog = []; const buttonRef = React.createRef(); - const ClickEventComponent = createReactEventComponent({ + const TestResponder = createEventResponder({ targetEventTypes: ['click'], onEvent: (event, context, props) => { eventLog.push({ @@ -158,9 +155,9 @@ describe('DOMEventResponderSystem', () => { }); const Test = () => ( - - - + ); ReactDOM.render(, container); @@ -179,15 +176,14 @@ describe('DOMEventResponderSystem', () => { ]); }); - it('nested event responders and their event listeners should fire multiple times', () => { + it('nested event responders should not fire multiple times', () => { let eventResponderFiredCount = 0; let eventLog = []; const buttonRef = React.createRef(); - const ClickEventComponent = createReactEventComponent({ + const TestResponder = createEventResponder({ targetEventTypes: ['click'], onEvent: (event, context, props) => { - context.continueLocalPropagation(); eventResponderFiredCount++; eventLog.push({ name: event.type, @@ -198,12 +194,12 @@ describe('DOMEventResponderSystem', () => { }, }); - const Test = () => ( - - - - - + let Test = () => ( + ); ReactDOM.render(, container); @@ -211,8 +207,8 @@ describe('DOMEventResponderSystem', () => { // Clicking the button should trigger the event responder onEvent() let buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); - expect(eventResponderFiredCount).toBe(2); - expect(eventLog.length).toBe(2); + expect(eventResponderFiredCount).toBe(1); + expect(eventLog.length).toBe(1); // JSDOM does not support passive events, so this will be false expect(eventLog).toEqual([ { @@ -221,6 +217,27 @@ describe('DOMEventResponderSystem', () => { passiveSupported: false, phase: 'bubble', }, + ]); + + eventLog = []; + + Test = () => ( +
}> + +
+ ); + + ReactDOM.render(, container); + + // Clicking the button should trigger the event responder onEvent() + buttonElement = buttonRef.current; + dispatchClickEvent(buttonElement); + expect(eventResponderFiredCount).toBe(2); + expect(eventLog.length).toBe(1); + + expect(eventLog).toEqual([ { name: 'click', passive: false, @@ -230,30 +247,30 @@ describe('DOMEventResponderSystem', () => { ]); }); - it('nested event responders and their event listeners should fire in the correct order', () => { + it('nested event responders should fire in the correct order', () => { let eventLog = []; const buttonRef = React.createRef(); - const ClickEventComponentA = createReactEventComponent({ + const TestResponderA = createEventResponder({ targetEventTypes: ['click'], onEvent: (event, context, props) => { eventLog.push(`A [bubble]`); }, }); - const ClickEventComponentB = createReactEventComponent({ + const TestResponderB = createEventResponder({ targetEventTypes: ['click'], onEvent: (event, context, props) => { eventLog.push(`B [bubble]`); }, }); - const Test = () => ( - - - - - + let Test = () => ( + ); ReactDOM.render(, container); @@ -262,33 +279,22 @@ describe('DOMEventResponderSystem', () => { let buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); - expect(eventLog).toEqual(['B [bubble]', 'A [bubble]']); - }); + expect(eventLog).toEqual(['A [bubble]', 'B [bubble]']); - it('nested event responders should fire in the correct order with continueLocalPropagation', () => { - let eventLog = []; - const buttonRef = React.createRef(); - - const ClickEventComponent = createReactEventComponent({ - targetEventTypes: ['click'], - onEvent: (event, context, props) => { - context.continueLocalPropagation(); - eventLog.push(`${props.name} [bubble]`); - }, - }); + eventLog = []; - const Test = () => ( - - - - - + Test = () => ( +
}> + +
); ReactDOM.render(, container); // Clicking the button should trigger the event responder onEvent() - let buttonElement = buttonRef.current; + buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(eventLog).toEqual(['B [bubble]', 'A [bubble]']); @@ -298,7 +304,7 @@ describe('DOMEventResponderSystem', () => { let eventLog = []; const buttonRef = React.createRef(); - const ClickEventComponent = createReactEventComponent({ + const TestResponder = createEventResponder({ targetEventTypes: ['click'], onEvent: (event, context, props) => { eventLog.push(`${props.name} [bubble]`); @@ -306,11 +312,11 @@ describe('DOMEventResponderSystem', () => { }); const Test = () => ( - - - - - +
}> + +
); ReactDOM.render(, container); @@ -326,22 +332,16 @@ describe('DOMEventResponderSystem', () => { let eventLog = []; const buttonRef = React.createRef(); - const ClickEventComponent = createReactEventComponent({ + const TestResponder = createEventResponder({ targetEventTypes: ['click'], onEvent: (event, context, props) => { - if (props.onMagicClick) { - const syntheticEvent = { - target: event.target, - type: 'magicclick', - phase: 'bubble', - timeStamp: context.getTimeStamp(), - }; - context.dispatchEvent( - syntheticEvent, - props.onMagicClick, - DiscreteEvent, - ); - } + const syntheticEvent = { + target: event.target, + type: 'magicclick', + phase: 'bubble', + timeStamp: context.getTimeStamp(), + }; + context.dispatchEvent('onMagicClick', syntheticEvent, DiscreteEvent); }, }); @@ -349,11 +349,17 @@ describe('DOMEventResponderSystem', () => { eventLog.push('magic event fired', e.type, e.phase); } - const Test = () => ( - - - - ); + const Test = () => { + React.unstable_useListener(TestResponder, { + onMagicClick: handleMagicEvent, + }); + + return ( + + ); + }; ReactDOM.render(, container); @@ -375,40 +381,32 @@ describe('DOMEventResponderSystem', () => { phase, timeStamp: context.getTimeStamp(), }; - context.dispatchEvent(pressEvent, props.onPress, DiscreteEvent); + context.dispatchEvent('onPress', pressEvent, DiscreteEvent); context.setTimeout(() => { - if (props.onLongPress) { - const longPressEvent = { - target: event.target, - type: 'longpress', - phase, - timeStamp: context.getTimeStamp(), - }; - context.dispatchEvent( - longPressEvent, - props.onLongPress, - DiscreteEvent, - ); - } - - if (props.onLongPressChange) { - const longPressChangeEvent = { - target: event.target, - type: 'longpresschange', - phase, - timeStamp: context.getTimeStamp(), - }; - context.dispatchEvent( - longPressChangeEvent, - props.onLongPressChange, - DiscreteEvent, - ); - } + const longPressEvent = { + target: event.target, + type: 'longpress', + phase, + timeStamp: context.getTimeStamp(), + }; + context.dispatchEvent('onLongPress', longPressEvent, DiscreteEvent); + + const longPressChangeEvent = { + target: event.target, + type: 'longpresschange', + phase, + timeStamp: context.getTimeStamp(), + }; + context.dispatchEvent( + 'onLongPressChange', + longPressChangeEvent, + DiscreteEvent, + ); }, 500); } - const LongPressEventComponent = createReactEventComponent({ + const TestResponder = createEventResponder({ targetEventTypes: ['click'], onEvent: (event, context, props) => { handleEvent(event, context, props, 'bubble'); @@ -419,14 +417,19 @@ describe('DOMEventResponderSystem', () => { eventLog.push(msg); } - const Test = () => ( - log('press ' + e.phase)} - onLongPress={e => log('longpress ' + e.phase)} - onLongPressChange={e => log('longpresschange ' + e.phase)}> - - - ); + const Test = () => { + React.unstable_useListener(TestResponder, { + onPress: e => log('press ' + e.phase), + onLongPress: e => log('longpress ' + e.phase), + onLongPressChange: e => log('longpresschange ' + e.phase), + }); + + return ( + + ); + }; ReactDOM.render(, container); @@ -445,48 +448,68 @@ describe('DOMEventResponderSystem', () => { it('the event responder onMount() function should fire', () => { let onMountFired = 0; - const EventComponent = createReactEventComponent({ + const TestResponder = createEventResponder({ targetEventTypes: [], onMount: () => { onMountFired++; }, }); - const Test = () => ( - - - + ); ReactDOM.render(, container); @@ -579,50 +594,13 @@ describe('DOMEventResponderSystem', () => { ]); }); - it('isTargetWithinEventResponderScope works', () => { - const buttonRef = React.createRef(); - const divRef = React.createRef(); - const log = []; - - const EventComponent = createReactEventComponent({ - targetEventTypes: ['pointerout'], - onEvent: (event, context) => { - context.continueLocalPropagation(); - const isWithin = context.isTargetWithinEventResponderScope( - event.nativeEvent.relatedTarget, - ); - log.push(isWithin); - }, - allowMultipleHostChildren: true, - }); - - const Test = () => ( - -
- - - - - ); - ReactDOM.render(, container); - - buttonRef.current.dispatchEvent( - createEvent('pointerout', {relatedTarget: divRef.current}), - ); - divRef.current.dispatchEvent( - createEvent('pointerout', {relatedTarget: buttonRef.current}), - ); - - expect(log).toEqual([false, true, false]); - }); - it('the event responder target listeners should correctly fire for only their events', () => { let clickEventComponent1Fired = 0; let clickEventComponent2Fired = 0; let eventLog = []; const buttonRef = React.createRef(); - const ClickEventComponent1 = createReactEventComponent({ + const TestResponderA = createEventResponder({ targetEventTypes: ['click_active'], onEvent: event => { clickEventComponent1Fired++; @@ -634,7 +612,7 @@ describe('DOMEventResponderSystem', () => { }, }); - const ClickEventComponent2 = createReactEventComponent({ + const TestResponderB = createEventResponder({ targetEventTypes: ['click'], onEvent: event => { clickEventComponent2Fired++; @@ -647,11 +625,11 @@ describe('DOMEventResponderSystem', () => { }); const Test = () => ( - - - - - +
}> + +
); ReactDOM.render(, container); @@ -681,7 +659,7 @@ describe('DOMEventResponderSystem', () => { let clickEventComponent2Fired = 0; let eventLog = []; - const ClickEventComponent1 = createReactEventComponent({ + const TestResponderA = createEventResponder({ rootEventTypes: ['click_active'], onRootEvent: event => { clickEventComponent1Fired++; @@ -693,7 +671,7 @@ describe('DOMEventResponderSystem', () => { }, }); - const ClickEventComponent2 = createReactEventComponent({ + const TestResponderB = createEventResponder({ rootEventTypes: ['click'], onRootEvent: event => { clickEventComponent2Fired++; @@ -706,11 +684,9 @@ describe('DOMEventResponderSystem', () => { }); const Test = () => ( - - - - - +
}> + +
); ReactDOM.render(, container); @@ -737,7 +713,7 @@ describe('DOMEventResponderSystem', () => { }); it('the event responder system should warn on accessing invalid properties', () => { - const ClickEventComponent = createReactEventComponent({ + const TestResponder = createEventResponder({ rootEventTypes: ['click'], onRootEvent: (event, context, props) => { const syntheticEvent = { @@ -745,16 +721,18 @@ describe('DOMEventResponderSystem', () => { type: 'click', timeStamp: context.getTimeStamp(), }; - context.dispatchEvent(syntheticEvent, props.onClick, DiscreteEvent); + context.dispatchEvent('onClick', syntheticEvent, DiscreteEvent); }, }); let handler; - const Test = () => ( - - - - ); + const Test = () => { + React.unstable_useListener(TestResponder, { + onClick: handler, + }); + + return ; + }; expect(() => { handler = event => { event.preventDefault(); @@ -818,79 +796,45 @@ describe('DOMEventResponderSystem', () => { expect(container.innerHTML).toBe(''); }); - it('should work with event component hooks', () => { + it('should work with event listener hooks', () => { const buttonRef = React.createRef(); const eventLogs = []; - const EventComponent = createReactEventComponent({ + const TestResponder = createEventResponder({ targetEventTypes: ['foo'], onEvent: (event, context, props) => { - if (props.onFoo) { - const fooEvent = { - target: event.target, - type: 'foo', - timeStamp: context.getTimeStamp(), - }; - context.dispatchEvent(fooEvent, props.onFoo, DiscreteEvent); - } - eventLogs.push(context.isRespondingToHook() ? '[hook]' : '[component]'); + const fooEvent = { + target: event.target, + type: 'foo', + timeStamp: context.getTimeStamp(), + }; + context.dispatchEvent('onFoo', fooEvent, DiscreteEvent); }, }); const Test = () => { - React.unstable_useEvent(EventComponent, { + React.unstable_useListener(TestResponder, { onFoo: e => eventLogs.push('hook'), }); - return ( - eventLogs.push('prop')}> -
); @@ -92,13 +90,11 @@ describe('FocusScope event responder', () => { const button2Ref = React.createRef(); const SimpleFocusScope = () => ( -
- - -
); @@ -125,17 +121,15 @@ describe('FocusScope event responder', () => { const button4Ref = React.createRef(); const SimpleFocusScope = () => ( -
- - -
+ +