Skip to content

Commit dd9cef9

Browse files
authored
Experimental Event API: Add targets and responder utility method for finding targets (#15372)
1 parent c64b330 commit dd9cef9

File tree

8 files changed

+358
-11
lines changed

8 files changed

+358
-11
lines changed

packages/react-dom/src/events/DOMEventResponderSystem.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {AnyNativeEvent} from 'events/PluginModuleType';
1515
import {
1616
EventComponent,
1717
EventTarget as EventTargetWorkTag,
18+
HostComponent,
1819
} from 'shared/ReactWorkTags';
1920
import type {
2021
ReactEventResponderEventType,
@@ -237,8 +238,74 @@ const eventResponderContext: ReactResponderContext = {
237238
}
238239
}, delay);
239240
},
241+
getEventTargetsFromTarget(
242+
target: Element | Document,
243+
queryType?: Symbol | number,
244+
queryKey?: string,
245+
): Array<{
246+
node: Element,
247+
props: null | Object,
248+
}> {
249+
const eventTargetHostComponents = [];
250+
let node = getClosestInstanceFromNode(target);
251+
// We traverse up the fiber tree from the target fiber, to the
252+
// current event component fiber. Along the way, we check if
253+
// the fiber has any children that are event targets. If there
254+
// are, we query them (optionally) to ensure they match the
255+
// specified type and key. We then push the event target props
256+
// along with the associated parent host component of that event
257+
// target.
258+
while (node !== null) {
259+
if (node.stateNode === currentInstance) {
260+
break;
261+
}
262+
let child = node.child;
263+
264+
while (child !== null) {
265+
if (
266+
child.tag === EventTargetWorkTag &&
267+
queryEventTarget(child, queryType, queryKey)
268+
) {
269+
const props = child.stateNode.props;
270+
let parent = child.return;
271+
272+
if (parent !== null) {
273+
if (parent.stateNode === currentInstance) {
274+
break;
275+
}
276+
if (parent.tag === HostComponent) {
277+
eventTargetHostComponents.push({
278+
node: parent.stateNode,
279+
props,
280+
});
281+
break;
282+
}
283+
parent = parent.return;
284+
}
285+
break;
286+
}
287+
child = child.sibling;
288+
}
289+
node = node.return;
290+
}
291+
return eventTargetHostComponents;
292+
},
240293
};
241294

295+
function queryEventTarget(
296+
child: Fiber,
297+
queryType: void | Symbol | number,
298+
queryKey: void | string,
299+
): boolean {
300+
if (queryType !== undefined && child.type.type !== queryType) {
301+
return false;
302+
}
303+
if (queryKey !== undefined && child.key !== queryKey) {
304+
return false;
305+
}
306+
return true;
307+
}
308+
242309
const rootEventTypesToEventComponentInstances: Map<
243310
DOMTopLevelEventType | string,
244311
Set<ReactEventComponentInstance>,

packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
let React;
1313
let ReactFeatureFlags;
1414
let ReactDOM;
15+
let ReactSymbols;
1516

1617
function createReactEventComponent(
1718
targetEventTypes,
@@ -42,6 +43,14 @@ function dispatchClickEvent(element) {
4243
element.dispatchEvent(clickEvent);
4344
}
4445

46+
function createReactEventTarget(type) {
47+
return {
48+
$$typeof: ReactSymbols.REACT_EVENT_TARGET_TYPE,
49+
displayName: 'TestEventTarget',
50+
type,
51+
};
52+
}
53+
4554
// This is a new feature in Fiber so I put it in its own test file. It could
4655
// probably move to one of the other test files once it is official.
4756
describe('DOMEventResponderSystem', () => {
@@ -55,6 +64,7 @@ describe('DOMEventResponderSystem', () => {
5564
ReactDOM = require('react-dom');
5665
container = document.createElement('div');
5766
document.body.appendChild(container);
67+
ReactSymbols = require('shared/ReactSymbols');
5868
});
5969

6070
afterEach(() => {
@@ -414,4 +424,229 @@ describe('DOMEventResponderSystem', () => {
414424
expect(ownershipGained).toEqual(true);
415425
expect(onOwnershipChangeFired).toEqual(1);
416426
});
427+
428+
it('should be possible to get event targets', () => {
429+
let queryResult = null;
430+
const buttonRef = React.createRef();
431+
const divRef = React.createRef();
432+
const eventTargetType = Symbol.for('react.event_target.test');
433+
const EventTarget = createReactEventTarget(eventTargetType);
434+
435+
const EventComponent = createReactEventComponent(
436+
['click'],
437+
undefined,
438+
(event, context, props, state) => {
439+
queryResult = Array.from(
440+
context.getEventTargetsFromTarget(event.target),
441+
);
442+
},
443+
);
444+
445+
const Test = () => (
446+
<EventComponent>
447+
<div ref={divRef}>
448+
<EventTarget foo={1} />
449+
<button ref={buttonRef}>
450+
<EventTarget foo={2} />
451+
Press me!
452+
</button>
453+
</div>
454+
</EventComponent>
455+
);
456+
457+
ReactDOM.render(<Test />, container);
458+
459+
let buttonElement = buttonRef.current;
460+
let divElement = divRef.current;
461+
dispatchClickEvent(buttonElement);
462+
jest.runAllTimers();
463+
464+
expect(queryResult).toEqual([
465+
{
466+
node: buttonElement,
467+
props: {
468+
foo: 2,
469+
},
470+
},
471+
{
472+
node: divElement,
473+
props: {
474+
foo: 1,
475+
},
476+
},
477+
]);
478+
});
479+
480+
it('should be possible to query event targets by type', () => {
481+
let queryResult = null;
482+
const buttonRef = React.createRef();
483+
const divRef = React.createRef();
484+
const eventTargetType = Symbol.for('react.event_target.test');
485+
const EventTarget = createReactEventTarget(eventTargetType);
486+
487+
const eventTargetType2 = Symbol.for('react.event_target.test2');
488+
const EventTarget2 = createReactEventTarget(eventTargetType2);
489+
490+
const EventComponent = createReactEventComponent(
491+
['click'],
492+
undefined,
493+
(event, context, props, state) => {
494+
queryResult = context.getEventTargetsFromTarget(
495+
event.target,
496+
eventTargetType2,
497+
);
498+
},
499+
);
500+
501+
const Test = () => (
502+
<EventComponent>
503+
<div ref={divRef}>
504+
<EventTarget2 foo={1} />
505+
<button ref={buttonRef}>
506+
<EventTarget foo={2} />
507+
Press me!
508+
</button>
509+
</div>
510+
</EventComponent>
511+
);
512+
513+
ReactDOM.render(<Test />, container);
514+
515+
let buttonElement = buttonRef.current;
516+
let divElement = divRef.current;
517+
dispatchClickEvent(buttonElement);
518+
jest.runAllTimers();
519+
520+
expect(queryResult).toEqual([
521+
{
522+
node: divElement,
523+
props: {
524+
foo: 1,
525+
},
526+
},
527+
]);
528+
});
529+
530+
it('should be possible to query event targets by key', () => {
531+
let queryResult = null;
532+
const buttonRef = React.createRef();
533+
const divRef = React.createRef();
534+
const eventTargetType = Symbol.for('react.event_target.test');
535+
const EventTarget = createReactEventTarget(eventTargetType);
536+
537+
const EventComponent = createReactEventComponent(
538+
['click'],
539+
undefined,
540+
(event, context, props, state) => {
541+
queryResult = context.getEventTargetsFromTarget(
542+
event.target,
543+
undefined,
544+
'a',
545+
);
546+
},
547+
);
548+
549+
const Test = () => (
550+
<EventComponent>
551+
<div ref={divRef}>
552+
<EventTarget foo={1} />
553+
<button ref={buttonRef}>
554+
<EventTarget key="a" foo={2} />
555+
Press me!
556+
</button>
557+
</div>
558+
</EventComponent>
559+
);
560+
561+
ReactDOM.render(<Test />, container);
562+
563+
let buttonElement = buttonRef.current;
564+
dispatchClickEvent(buttonElement);
565+
jest.runAllTimers();
566+
567+
expect(queryResult).toEqual([
568+
{
569+
node: buttonElement,
570+
props: {
571+
foo: 2,
572+
},
573+
},
574+
]);
575+
});
576+
577+
it('should be possible to query event targets by type and key', () => {
578+
let queryResult = null;
579+
let queryResult2 = null;
580+
let queryResult3 = null;
581+
const buttonRef = React.createRef();
582+
const divRef = React.createRef();
583+
const eventTargetType = Symbol.for('react.event_target.test');
584+
const EventTarget = createReactEventTarget(eventTargetType);
585+
586+
const eventTargetType2 = Symbol.for('react.event_target.test2');
587+
const EventTarget2 = createReactEventTarget(eventTargetType2);
588+
589+
const EventComponent = createReactEventComponent(
590+
['click'],
591+
undefined,
592+
(event, context, props, state) => {
593+
queryResult = context.getEventTargetsFromTarget(
594+
event.target,
595+
eventTargetType2,
596+
'a',
597+
);
598+
599+
queryResult2 = context.getEventTargetsFromTarget(
600+
event.target,
601+
eventTargetType,
602+
'c',
603+
);
604+
605+
// Should return an empty array as this doesn't exist
606+
queryResult3 = context.getEventTargetsFromTarget(
607+
event.target,
608+
eventTargetType,
609+
'd',
610+
);
611+
},
612+
);
613+
614+
const Test = () => (
615+
<EventComponent>
616+
<div ref={divRef}>
617+
<EventTarget2 key="a" foo={1} />
618+
<EventTarget2 key="b" foo={2} />
619+
<button ref={buttonRef}>
620+
<EventTarget key="c" foo={3} />
621+
Press me!
622+
</button>
623+
</div>
624+
</EventComponent>
625+
);
626+
627+
ReactDOM.render(<Test />, container);
628+
629+
let buttonElement = buttonRef.current;
630+
let divElement = divRef.current;
631+
dispatchClickEvent(buttonElement);
632+
jest.runAllTimers();
633+
634+
expect(queryResult).toEqual([
635+
{
636+
node: divElement,
637+
props: {
638+
foo: 1,
639+
},
640+
},
641+
]);
642+
expect(queryResult2).toEqual([
643+
{
644+
node: buttonElement,
645+
props: {
646+
foo: 3,
647+
},
648+
},
649+
]);
650+
expect(queryResult3).toEqual([]);
651+
});
417652
});

packages/react-events/src/ReactEvents.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,22 @@
1010
import {
1111
REACT_EVENT_TARGET_TYPE,
1212
REACT_EVENT_TARGET_TOUCH_HIT,
13+
REACT_EVENT_FOCUS_TARGET,
14+
REACT_EVENT_PRESS_TARGET,
1315
} from 'shared/ReactSymbols';
1416
import type {ReactEventTarget} from 'shared/ReactTypes';
1517

1618
export const TouchHitTarget: ReactEventTarget = {
1719
$$typeof: REACT_EVENT_TARGET_TYPE,
1820
type: REACT_EVENT_TARGET_TOUCH_HIT,
1921
};
22+
23+
export const FocusTarget: ReactEventTarget = {
24+
$$typeof: REACT_EVENT_TARGET_TYPE,
25+
type: REACT_EVENT_FOCUS_TARGET,
26+
};
27+
28+
export const PressTarget: ReactEventTarget = {
29+
$$typeof: REACT_EVENT_TARGET_TYPE,
30+
type: REACT_EVENT_PRESS_TARGET,
31+
};

packages/react-reconciler/src/ReactFiber.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,10 @@ export function createFiberFromEventTarget(
638638
fiber.elementType = eventTarget;
639639
fiber.type = eventTarget;
640640
fiber.expirationTime = expirationTime;
641+
// Store latest props
642+
fiber.stateNode = {
643+
props: pendingProps,
644+
};
641645
return fiber;
642646
}
643647

packages/react-reconciler/src/ReactFiberCompleteWork.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,9 @@ function completeWork(
841841
rootContainerInstance,
842842
workInProgress,
843843
);
844+
// Update the latest props on the stateNode. This is used
845+
// during the event phase to find the most current props.
846+
workInProgress.stateNode.props = newProps;
844847
if (shouldUpdate) {
845848
markUpdate(workInProgress);
846849
}

0 commit comments

Comments
 (0)