Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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