Experimental Event API: Add targets and responder utility method for finding targets (#15372)

This commit is contained in:
Dominic Gannaway
2019-04-10 18:52:34 +01:00
committed by GitHub
parent c64b330032
commit dd9cef9fc0
8 changed files with 358 additions and 11 deletions
@@ -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<ReactEventComponentInstance>,
@@ -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 = () => (
<EventComponent>
<div ref={divRef}>
<EventTarget foo={1} />
<button ref={buttonRef}>
<EventTarget foo={2} />
Press me!
</button>
</div>
</EventComponent>
);
ReactDOM.render(<Test />, 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 = () => (
<EventComponent>
<div ref={divRef}>
<EventTarget2 foo={1} />
<button ref={buttonRef}>
<EventTarget foo={2} />
Press me!
</button>
</div>
</EventComponent>
);
ReactDOM.render(<Test />, 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 = () => (
<EventComponent>
<div ref={divRef}>
<EventTarget foo={1} />
<button ref={buttonRef}>
<EventTarget key="a" foo={2} />
Press me!
</button>
</div>
</EventComponent>
);
ReactDOM.render(<Test />, 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 = () => (
<EventComponent>
<div ref={divRef}>
<EventTarget2 key="a" foo={1} />
<EventTarget2 key="b" foo={2} />
<button ref={buttonRef}>
<EventTarget key="c" foo={3} />
Press me!
</button>
</div>
</EventComponent>
);
ReactDOM.render(<Test />, 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([]);
});
});
+12
View File
@@ -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,
};
+4
View File
@@ -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;
}
@@ -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);
}
+6
View File
@@ -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';
+8
View File
@@ -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,
}>,
};
+23 -11
View File
@@ -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;
}
}
}
}