From a1759863a0a7ad1439a0139a2947e58918069227 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 19 Jun 2019 16:27:51 +0100 Subject: [PATCH 1/3] [Flare] Add useEvent hook implementation Validate hooks have decendent event components Few fixes and displayName changes Fix more responder bugs Update error codes --- .../react-debug-tools/src/ReactDebugHooks.js | 5 + .../src/events/DOMEventResponderSystem.js | 181 +++++++++++++----- .../DOMEventResponderSystem-test.internal.js | 74 +++++-- .../src/server/ReactPartialRendererHooks.js | 1 + packages/react-events/src/Drag.js | 4 +- packages/react-events/src/Focus.js | 4 +- packages/react-events/src/FocusScope.js | 7 +- packages/react-events/src/Hover.js | 4 +- packages/react-events/src/Press.js | 4 +- packages/react-events/src/Scroll.js | 7 +- packages/react-events/src/Swipe.js | 4 +- .../src/__tests__/Focus-test.internal.js | 2 +- .../src/__tests__/Hover-test.internal.js | 2 +- .../src/__tests__/Press-test.internal.js | 2 +- packages/react-reconciler/src/ReactFiber.js | 7 + .../src/ReactFiberBeginWork.js | 8 +- .../src/ReactFiberCompleteWork.js | 21 +- .../react-reconciler/src/ReactFiberEvents.js | 66 +++++++ .../react-reconciler/src/ReactFiberHooks.js | 36 +++- .../ReactFiberEvents-test-internal.js | 2 +- .../src/ReactShallowRenderer.js | 1 + packages/react/src/React.js | 7 +- packages/react/src/ReactHooks.js | 6 + packages/shared/ReactDOMTypes.js | 3 +- packages/shared/ReactTypes.js | 4 +- packages/shared/createEventComponent.js | 2 - packages/shared/getComponentName.js | 2 +- scripts/error-codes/codes.json | 3 +- 28 files changed, 375 insertions(+), 94 deletions(-) diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 12e3021d8afd..2e029d382e19 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -215,6 +215,10 @@ function useMemo( return value; } +function useEvent() { + throw new Error('TODO: not yet implemented'); +} + const Dispatcher: DispatcherType = { readContext, useCallback, @@ -227,6 +231,7 @@ const Dispatcher: DispatcherType = { useReducer, useRef, useState, + useEvent, }; // Inspect diff --git a/packages/react-dom/src/events/DOMEventResponderSystem.js b/packages/react-dom/src/events/DOMEventResponderSystem.js index ff6ab2737bcd..a8161078709a 100644 --- a/packages/react-dom/src/events/DOMEventResponderSystem.js +++ b/packages/react-dom/src/events/DOMEventResponderSystem.js @@ -16,6 +16,7 @@ import { EventComponent, EventTarget as EventTargetWorkTag, HostComponent, + FunctionComponent, } from 'shared/ReactWorkTags'; import type { ReactEventComponentInstance, @@ -117,6 +118,7 @@ let currentTimers = new Map(); let currentInstance: null | ReactEventComponentInstance = null; let currentEventQueue: null | EventQueue = null; let currentTimerIDCounter = 0; +let currentDocument: null | Document = null; const eventResponderContext: ReactDOMResponderContext = { dispatchEvent( @@ -217,15 +219,41 @@ const eventResponderContext: ReactDOMResponderContext = { } return false; }, - isTargetWithinEventComponent, + isTargetWithinEventComponent(target: Element | Document): boolean { + validateResponderContext(); + if (target != null) { + let fiber = getClosestInstanceFromNode(target); + const currentFiber = ((currentInstance: any): ReactEventComponentInstance) + .currentFiber; + + while (fiber !== null) { + if (fiber.tag === EventComponent) { + // Switch to the current fiber tree + fiber = fiber.stateNode.currentFiber; + } + if (fiber === currentFiber || fiber.stateNode === currentInstance) { + return true; + } + fiber = fiber.return; + } + } + return false; + }, isTargetWithinEventResponderScope(target: Element | Document): boolean { validateResponderContext(); - const responder = ((currentInstance: any): ReactEventComponentInstance) - .responder; + const componentInstance = ((currentInstance: any): ReactEventComponentInstance); + const responder = componentInstance.responder; + if (target != null) { let fiber = getClosestInstanceFromNode(target); + const currentFiber = ((currentInstance: any): ReactEventComponentInstance) + .currentFiber; while (fiber !== null) { - if (fiber.stateNode === currentInstance) { + if (fiber.tag === EventComponent) { + // Switch to the current fiber tree + fiber = fiber.stateNode.currentFiber; + } + if (fiber === currentFiber || fiber.stateNode === currentInstance) { return true; } if ( @@ -384,9 +412,15 @@ const eventResponderContext: ReactDOMResponderContext = { const target = event.target; let fiber = getClosestInstanceFromNode(target); let hostComponent = target; + const currentResponder = ((currentInstance: any): ReactEventComponentInstance) + .responder; while (fiber !== null) { - if (fiber.stateNode === currentInstance) { + const stateNode = fiber.stateNode; + if ( + fiber.tag === EventComponent && + (stateNode === null || stateNode.responder === currentResponder) + ) { break; } if (fiber.tag === HostComponent) { @@ -407,8 +441,16 @@ const eventResponderContext: ReactDOMResponderContext = { ): boolean { validateResponderContext(); let fiber = getClosestInstanceFromNode(target); + const currentResponder = ((currentInstance: any): ReactEventComponentInstance) + .responder; + while (fiber !== null) { - if (!deep && fiber.stateNode === currentInstance) { + const stateNode = fiber.stateNode; + if ( + !deep && + (fiber.tag === EventComponent && + (stateNode === null || stateNode.responder === currentResponder)) + ) { return false; } if (fiber.tag === HostComponent && fiber.type === elementType) { @@ -451,24 +493,8 @@ function collectFocusableElements( } } -function isTargetWithinEventComponent(target: Element | Document): boolean { - validateResponderContext(); - if (target != null) { - let fiber = getClosestInstanceFromNode(target); - while (fiber !== null) { - if (fiber.stateNode === currentInstance) { - return true; - } - fiber = fiber.return; - } - } - return false; -} - function getActiveDocument(): Document { - const eventComponentInstance = ((currentInstance: any): ReactEventComponentInstance); - const rootElement = ((eventComponentInstance.rootInstance: any): Element); - return rootElement.ownerDocument; + return ((currentDocument: any): Document); } function releaseOwnershipForEventComponentInstance( @@ -659,23 +685,60 @@ function getDOMTargetEventTypesSet( return cachedSet; } +function storeTargetEventResponderInstance( + listeningName: string, + eventComponentInstance: ReactEventComponentInstance, + eventResponderInstances: Array, + eventComponentResponders: null | Set, +): void { + const responder = eventComponentInstance.responder; + const targetEventTypes = responder.targetEventTypes; + // Validate the target event type exists on the responder + if (targetEventTypes !== undefined) { + const targetEventTypesSet = getDOMTargetEventTypesSet(targetEventTypes); + if (targetEventTypesSet.has(listeningName)) { + eventResponderInstances.push(eventComponentInstance); + if (eventComponentResponders !== null) { + eventComponentResponders.add(responder); + } + } + } +} + function getTargetEventResponderInstances( listeningName: string, targetFiber: null | Fiber, ): Array { + // We use this to know if we should check add hooks. If there are + // no event targets, then we don't add the hook forms. + const eventComponentResponders = new Set(); const eventResponderInstances = []; let node = targetFiber; while (node !== null) { // Traverse up the fiber tree till we find event component fibers. - if (node.tag === EventComponent) { + const tag = node.tag; + const events = node.events; + + if (tag === EventComponent) { const eventComponentInstance = node.stateNode; - const responder = eventComponentInstance.responder; - const targetEventTypes = responder.targetEventTypes; - // Validate the target event type exists on the responder - if (targetEventTypes !== undefined) { - const targetEventTypesSet = getDOMTargetEventTypesSet(targetEventTypes); - if (targetEventTypesSet.has(listeningName)) { - eventResponderInstances.push(eventComponentInstance); + // Switch to the current fiber tree + node = eventComponentInstance.currentFiber; + storeTargetEventResponderInstance( + listeningName, + eventComponentInstance, + eventResponderInstances, + eventComponentResponders, + ); + } else if (tag === FunctionComponent && events !== null) { + for (let i = 0; i < events.length; i++) { + const eventComponentInstance = events[i]; + if (eventComponentResponders.has(eventComponentInstance.responder)) { + storeTargetEventResponderInstance( + listeningName, + eventComponentInstance, + eventResponderInstances, + null, + ); } } } @@ -706,8 +769,9 @@ function shouldSkipEventComponent( eventResponderInstance: ReactEventComponentInstance, responder: ReactDOMEventResponder, propagatedEventResponders: null | Set, + localPropagation: boolean, ): boolean { - if (propagatedEventResponders !== null) { + if (propagatedEventResponders !== null && localPropagation) { if (propagatedEventResponders.has(responder)) { return true; } @@ -772,7 +836,12 @@ function traverseAndHandleEventResponderInstances( // Capture target phase for (i = length; i-- > 0; ) { const targetEventResponderInstance = targetEventResponderInstances[i]; - const {responder, props, state} = targetEventResponderInstance; + const { + localPropagation, + props, + responder, + state, + } = targetEventResponderInstance; const eventListener = responder.onEventCapture; if (eventListener !== undefined) { if ( @@ -780,16 +849,19 @@ function traverseAndHandleEventResponderInstances( targetEventResponderInstance, responder, propagatedEventResponders, + localPropagation, ) ) { continue; } currentInstance = targetEventResponderInstance; eventListener(responderEvent, eventResponderContext, props, state); - checkForLocalPropagationContinuation( - responder, - propagatedEventResponders, - ); + if (localPropagation) { + checkForLocalPropagationContinuation( + responder, + propagatedEventResponders, + ); + } } } // We clean propagated event responders between phases. @@ -797,7 +869,12 @@ function traverseAndHandleEventResponderInstances( // Bubble target phase for (i = 0; i < length; i++) { const targetEventResponderInstance = targetEventResponderInstances[i]; - const {responder, props, state} = targetEventResponderInstance; + const { + localPropagation, + props, + responder, + state, + } = targetEventResponderInstance; const eventListener = responder.onEvent; if (eventListener !== undefined) { if ( @@ -805,16 +882,19 @@ function traverseAndHandleEventResponderInstances( targetEventResponderInstance, responder, propagatedEventResponders, + localPropagation, ) ) { continue; } currentInstance = targetEventResponderInstance; eventListener(responderEvent, eventResponderContext, props, state); - checkForLocalPropagationContinuation( - responder, - propagatedEventResponders, - ); + if (localPropagation) { + checkForLocalPropagationContinuation( + responder, + propagatedEventResponders, + ); + } } } } @@ -826,11 +906,21 @@ function traverseAndHandleEventResponderInstances( if (length > 0) { for (i = 0; i < length; i++) { const rootEventResponderInstance = rootEventResponderInstances[i]; - const {responder, props, state} = rootEventResponderInstance; + const { + localPropagation, + props, + responder, + state, + } = rootEventResponderInstance; const eventListener = responder.onRootEvent; if (eventListener !== undefined) { if ( - shouldSkipEventComponent(rootEventResponderInstance, responder, null) + shouldSkipEventComponent( + rootEventResponderInstance, + responder, + null, + localPropagation, + ) ) { continue; } @@ -944,8 +1034,10 @@ export function dispatchEventForResponderEventSystem( const previousInstance = currentInstance; const previousTimers = currentTimers; const previousTimeStamp = currentTimeStamp; + const previousDocument = currentDocument; currentTimers = null; currentEventQueue = createEventQueue(); + currentDocument = (nativeEventTarget: any).ownerDocument; // We might want to control timeStamp another way here currentTimeStamp = (nativeEvent: any).timeStamp; try { @@ -962,6 +1054,7 @@ export function dispatchEventForResponderEventSystem( currentInstance = previousInstance; currentEventQueue = previousEventQueue; currentTimeStamp = previousTimeStamp; + currentDocument = previousDocument; } } } 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 07f8828b5375..547ec2607da7 100644 --- a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js @@ -28,8 +28,10 @@ function createReactEventComponent({ onUnmount, onOwnershipChange, allowMultipleHostChildren, + allowEventHooks, }) { const testEventResponder = { + displayName: 'TestEventComponent', targetEventTypes, rootEventTypes, createInitialState, @@ -40,16 +42,27 @@ function createReactEventComponent({ onUnmount, onOwnershipChange, allowMultipleHostChildren: allowMultipleHostChildren || false, + allowEventHooks: allowEventHooks || true, }; return { $$typeof: Symbol.for('react.event_component'), - displayName: 'TestEventComponent', props: null, responder: testEventResponder, }; } +const createEvent = (type, data) => { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent(type, true, true); + if (data != null) { + Object.entries(data).forEach(([key, value]) => { + event[key] = value; + }); + } + return event; +}; + function dispatchEvent(element, type) { const event = document.createEvent('Event'); event.initEvent(type, true, true); @@ -705,17 +718,6 @@ describe('DOMEventResponderSystem', () => { ); ReactDOM.render(, container); - const createEvent = (type, data) => { - const event = document.createEvent('CustomEvent'); - event.initCustomEvent(type, true, true); - if (data != null) { - Object.entries(data).forEach(([key, value]) => { - event[key] = value; - }); - } - return event; - }; - buttonRef.current.dispatchEvent( createEvent('pointerout', {relatedTarget: divRef.current}), ); @@ -1067,4 +1069,52 @@ describe('DOMEventResponderSystem', () => { ReactDOM.render(, container); }); + + it('should work with event component hooks', () => { + const buttonRef = React.createRef(); + const eventLogs = []; + const EventComponent = createReactEventComponent({ + 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); + } + }, + }); + + const Test = () => { + React.unstable_useEvent(EventComponent.responder, { + onFoo: e => eventLogs.push('hook'), + }); + return ( + eventLogs.push('prop')}> +