diff --git a/packages/react-dom/src/events/DOMEventResponderSystem.js b/packages/react-dom/src/events/DOMEventResponderSystem.js index ddadb18472..5d32eeac71 100644 --- a/packages/react-dom/src/events/DOMEventResponderSystem.js +++ b/packages/react-dom/src/events/DOMEventResponderSystem.js @@ -15,6 +15,7 @@ import type {AnyNativeEvent} from 'events/PluginModuleType'; import { EventComponent, EventTarget as EventTargetWorkTag, + HostComponent, } from 'shared/ReactWorkTags'; import type { ReactEventResponderEventType, @@ -237,8 +238,74 @@ const eventResponderContext: ReactResponderContext = { } }, delay); }, + getEventTargetsFromTarget( + target: Element | Document, + queryType?: Symbol | number, + queryKey?: string, + ): Array<{ + node: Element, + props: null | Object, + }> { + const eventTargetHostComponents = []; + let node = getClosestInstanceFromNode(target); + // We traverse up the fiber tree from the target fiber, to the + // current event component fiber. Along the way, we check if + // the fiber has any children that are event targets. If there + // are, we query them (optionally) to ensure they match the + // specified type and key. We then push the event target props + // along with the associated parent host component of that event + // target. + while (node !== null) { + if (node.stateNode === currentInstance) { + break; + } + let child = node.child; + + while (child !== null) { + if ( + child.tag === EventTargetWorkTag && + queryEventTarget(child, queryType, queryKey) + ) { + const props = child.stateNode.props; + let parent = child.return; + + if (parent !== null) { + if (parent.stateNode === currentInstance) { + break; + } + if (parent.tag === HostComponent) { + eventTargetHostComponents.push({ + node: parent.stateNode, + props, + }); + break; + } + parent = parent.return; + } + break; + } + child = child.sibling; + } + node = node.return; + } + return eventTargetHostComponents; + }, }; +function queryEventTarget( + child: Fiber, + queryType: void | Symbol | number, + queryKey: void | string, +): boolean { + if (queryType !== undefined && child.type.type !== queryType) { + return false; + } + if (queryKey !== undefined && child.key !== queryKey) { + return false; + } + return true; +} + const rootEventTypesToEventComponentInstances: Map< DOMTopLevelEventType | string, Set, 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 1fb5f3038a..d92a972920 100644 --- a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js @@ -12,6 +12,7 @@ let React; let ReactFeatureFlags; let ReactDOM; +let ReactSymbols; function createReactEventComponent( targetEventTypes, @@ -42,6 +43,14 @@ function dispatchClickEvent(element) { element.dispatchEvent(clickEvent); } +function createReactEventTarget(type) { + return { + $$typeof: ReactSymbols.REACT_EVENT_TARGET_TYPE, + displayName: 'TestEventTarget', + type, + }; +} + // This is a new feature in Fiber so I put it in its own test file. It could // probably move to one of the other test files once it is official. describe('DOMEventResponderSystem', () => { @@ -55,6 +64,7 @@ describe('DOMEventResponderSystem', () => { ReactDOM = require('react-dom'); container = document.createElement('div'); document.body.appendChild(container); + ReactSymbols = require('shared/ReactSymbols'); }); afterEach(() => { @@ -414,4 +424,229 @@ describe('DOMEventResponderSystem', () => { expect(ownershipGained).toEqual(true); expect(onOwnershipChangeFired).toEqual(1); }); + + it('should be possible to get event targets', () => { + let queryResult = null; + const buttonRef = React.createRef(); + const divRef = React.createRef(); + const eventTargetType = Symbol.for('react.event_target.test'); + const EventTarget = createReactEventTarget(eventTargetType); + + const EventComponent = createReactEventComponent( + ['click'], + undefined, + (event, context, props, state) => { + queryResult = Array.from( + context.getEventTargetsFromTarget(event.target), + ); + }, + ); + + const Test = () => ( + +
+ + +
+
+ ); + + ReactDOM.render(, container); + + let buttonElement = buttonRef.current; + let divElement = divRef.current; + dispatchClickEvent(buttonElement); + jest.runAllTimers(); + + expect(queryResult).toEqual([ + { + node: buttonElement, + props: { + foo: 2, + }, + }, + { + node: divElement, + props: { + foo: 1, + }, + }, + ]); + }); + + it('should be possible to query event targets by type', () => { + let queryResult = null; + const buttonRef = React.createRef(); + const divRef = React.createRef(); + const eventTargetType = Symbol.for('react.event_target.test'); + const EventTarget = createReactEventTarget(eventTargetType); + + const eventTargetType2 = Symbol.for('react.event_target.test2'); + const EventTarget2 = createReactEventTarget(eventTargetType2); + + const EventComponent = createReactEventComponent( + ['click'], + undefined, + (event, context, props, state) => { + queryResult = context.getEventTargetsFromTarget( + event.target, + eventTargetType2, + ); + }, + ); + + const Test = () => ( + +
+ + +
+
+ ); + + ReactDOM.render(, container); + + let buttonElement = buttonRef.current; + let divElement = divRef.current; + dispatchClickEvent(buttonElement); + jest.runAllTimers(); + + expect(queryResult).toEqual([ + { + node: divElement, + props: { + foo: 1, + }, + }, + ]); + }); + + it('should be possible to query event targets by key', () => { + let queryResult = null; + const buttonRef = React.createRef(); + const divRef = React.createRef(); + const eventTargetType = Symbol.for('react.event_target.test'); + const EventTarget = createReactEventTarget(eventTargetType); + + const EventComponent = createReactEventComponent( + ['click'], + undefined, + (event, context, props, state) => { + queryResult = context.getEventTargetsFromTarget( + event.target, + undefined, + 'a', + ); + }, + ); + + const Test = () => ( + +
+ + +
+
+ ); + + ReactDOM.render(, container); + + let buttonElement = buttonRef.current; + dispatchClickEvent(buttonElement); + jest.runAllTimers(); + + expect(queryResult).toEqual([ + { + node: buttonElement, + props: { + foo: 2, + }, + }, + ]); + }); + + it('should be possible to query event targets by type and key', () => { + let queryResult = null; + let queryResult2 = null; + let queryResult3 = null; + const buttonRef = React.createRef(); + const divRef = React.createRef(); + const eventTargetType = Symbol.for('react.event_target.test'); + const EventTarget = createReactEventTarget(eventTargetType); + + const eventTargetType2 = Symbol.for('react.event_target.test2'); + const EventTarget2 = createReactEventTarget(eventTargetType2); + + const EventComponent = createReactEventComponent( + ['click'], + undefined, + (event, context, props, state) => { + queryResult = context.getEventTargetsFromTarget( + event.target, + eventTargetType2, + 'a', + ); + + queryResult2 = context.getEventTargetsFromTarget( + event.target, + eventTargetType, + 'c', + ); + + // Should return an empty array as this doesn't exist + queryResult3 = context.getEventTargetsFromTarget( + event.target, + eventTargetType, + 'd', + ); + }, + ); + + const Test = () => ( + +
+ + + +
+
+ ); + + ReactDOM.render(, container); + + let buttonElement = buttonRef.current; + let divElement = divRef.current; + dispatchClickEvent(buttonElement); + jest.runAllTimers(); + + expect(queryResult).toEqual([ + { + node: divElement, + props: { + foo: 1, + }, + }, + ]); + expect(queryResult2).toEqual([ + { + node: buttonElement, + props: { + foo: 3, + }, + }, + ]); + expect(queryResult3).toEqual([]); + }); }); diff --git a/packages/react-events/src/ReactEvents.js b/packages/react-events/src/ReactEvents.js index af5c08b0b8..0230b9cdb9 100644 --- a/packages/react-events/src/ReactEvents.js +++ b/packages/react-events/src/ReactEvents.js @@ -10,6 +10,8 @@ import { REACT_EVENT_TARGET_TYPE, REACT_EVENT_TARGET_TOUCH_HIT, + REACT_EVENT_FOCUS_TARGET, + REACT_EVENT_PRESS_TARGET, } from 'shared/ReactSymbols'; import type {ReactEventTarget} from 'shared/ReactTypes'; @@ -17,3 +19,13 @@ export const TouchHitTarget: ReactEventTarget = { $$typeof: REACT_EVENT_TARGET_TYPE, type: REACT_EVENT_TARGET_TOUCH_HIT, }; + +export const FocusTarget: ReactEventTarget = { + $$typeof: REACT_EVENT_TARGET_TYPE, + type: REACT_EVENT_FOCUS_TARGET, +}; + +export const PressTarget: ReactEventTarget = { + $$typeof: REACT_EVENT_TARGET_TYPE, + type: REACT_EVENT_PRESS_TARGET, +}; diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index e4e4b77f81..8ca24a16a6 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -638,6 +638,10 @@ export function createFiberFromEventTarget( fiber.elementType = eventTarget; fiber.type = eventTarget; fiber.expirationTime = expirationTime; + // Store latest props + fiber.stateNode = { + props: pendingProps, + }; return fiber; } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 41b4ba71c4..56d9dd5b3c 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -841,6 +841,9 @@ function completeWork( rootContainerInstance, workInProgress, ); + // Update the latest props on the stateNode. This is used + // during the event phase to find the most current props. + workInProgress.stateNode.props = newProps; if (shouldUpdate) { markUpdate(workInProgress); } diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js index cde9f89c5b..152e360773 100644 --- a/packages/shared/ReactSymbols.js +++ b/packages/shared/ReactSymbols.js @@ -57,6 +57,12 @@ export const REACT_EVENT_TARGET_TYPE = hasSymbol export const REACT_EVENT_TARGET_TOUCH_HIT = hasSymbol ? Symbol.for('react.event_target.touch_hit') : 0xead7; +export const REACT_EVENT_FOCUS_TARGET = hasSymbol + ? Symbol.for('react.event_target.focus') + : 0xead8; +export const REACT_EVENT_PRESS_TARGET = hasSymbol + ? Symbol.for('react.event_target.press') + : 0xead9; const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; const FAUX_ITERATOR_SYMBOL = '@@iterator'; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 7b5f2d1c66..746f388db4 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -169,4 +169,12 @@ export type ReactResponderContext = { requestOwnership: () => boolean, releaseOwnership: () => boolean, setTimeout: (func: () => void, timeout: number) => TimeoutID, + getEventTargetsFromTarget: ( + target: Element | Document, + queryType?: Symbol | number, + queryKey?: string, + ) => Array<{ + node: Element, + props: null | Object, + }>, }; diff --git a/packages/shared/getComponentName.js b/packages/shared/getComponentName.js index cfa07d91e1..5b09f0c69b 100644 --- a/packages/shared/getComponentName.js +++ b/packages/shared/getComponentName.js @@ -25,10 +25,14 @@ import { REACT_EVENT_COMPONENT_TYPE, REACT_EVENT_TARGET_TYPE, REACT_EVENT_TARGET_TOUCH_HIT, + REACT_EVENT_FOCUS_TARGET, + REACT_EVENT_PRESS_TARGET, } from 'shared/ReactSymbols'; import {refineResolvedLazyComponent} from 'shared/ReactLazyComponent'; import type {ReactEventComponent, ReactEventTarget} from 'shared/ReactTypes'; +import {enableEventAPI} from './ReactFeatureFlags'; + function getWrappedName( outerType: mixed, innerType: any, @@ -94,21 +98,29 @@ function getComponentName(type: mixed): string | null { break; } case REACT_EVENT_COMPONENT_TYPE: { - const eventComponent = ((type: any): ReactEventComponent); - const displayName = eventComponent.displayName; - if (displayName !== undefined) { - return displayName; + if (enableEventAPI) { + const eventComponent = ((type: any): ReactEventComponent); + const displayName = eventComponent.displayName; + if (displayName !== undefined) { + return displayName; + } } break; } case REACT_EVENT_TARGET_TYPE: { - const eventTarget = ((type: any): ReactEventTarget); - if (eventTarget.type === REACT_EVENT_TARGET_TOUCH_HIT) { - return 'TouchHitTarget'; - } - const displayName = eventTarget.displayName; - if (displayName !== undefined) { - return displayName; + if (enableEventAPI) { + const eventTarget = ((type: any): ReactEventTarget); + if (eventTarget.type === REACT_EVENT_TARGET_TOUCH_HIT) { + return 'TouchHitTarget'; + } else if (eventTarget.type === REACT_EVENT_FOCUS_TARGET) { + return 'FocusTarget'; + } else if (eventTarget.type === REACT_EVENT_PRESS_TARGET) { + return 'PressTarget'; + } + const displayName = eventTarget.displayName; + if (displayName !== undefined) { + return displayName; + } } } }