mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Experimental Event API: Add targets and responder utility method for finding targets (#15372)
This commit is contained in:
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
}>,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user