Skip to content

Commit

Permalink
Experimental Event API: Add targets and responder utility method for …
Browse files Browse the repository at this point in the history
…finding targets (#15372)
  • Loading branch information
trueadm authored Apr 10, 2019
1 parent c64b330 commit dd9cef9
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 11 deletions.
67 changes: 67 additions & 0 deletions packages/react-dom/src/events/DOMEventResponderSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {AnyNativeEvent} from 'events/PluginModuleType';
import {
EventComponent,
EventTarget as EventTargetWorkTag,
HostComponent,
} from 'shared/ReactWorkTags';
import type {
ReactEventResponderEventType,
Expand Down Expand Up @@ -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>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
let React;
let ReactFeatureFlags;
let ReactDOM;
let ReactSymbols;

function createReactEventComponent(
targetEventTypes,
Expand Down Expand Up @@ -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', () => {
Expand All @@ -55,6 +64,7 @@ describe('DOMEventResponderSystem', () => {
ReactDOM = require('react-dom');
container = document.createElement('div');
document.body.appendChild(container);
ReactSymbols = require('shared/ReactSymbols');
});

afterEach(() => {
Expand Down Expand Up @@ -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 changes: 12 additions & 0 deletions packages/react-events/src/ReactEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,22 @@
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';

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 changes: 4 additions & 0 deletions packages/react-reconciler/src/ReactFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
3 changes: 3 additions & 0 deletions packages/react-reconciler/src/ReactFiberCompleteWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Loading

0 comments on commit dd9cef9

Please sign in to comment.