Experimental Event API: Add targets and responder utility method for finding targets (#15372)
Dominic Gannaway committed
Apr 10, 2019 at 18:52 UTC
dd9cef9fc001e174e177bac47d88fd28ff8de316
8 files changed
+358
-11
packages/react-dom/src/events/DOMEventResponderSystem.js
+67
@@ -15,6 +15,7 @@ import type {AnyNativeEvent} from 'events/PluginModuleType';
15
import {
16
EventComponent,
17
EventTarget as EventTargetWorkTag,
18
+ HostComponent,
19
} from 'shared/ReactWorkTags';
20
import type {
21
ReactEventResponderEventType,
@@ -237,8 +238,74 @@ const eventResponderContext: ReactResponderContext = {
238
}
239
}, delay);
240
},
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
+ },
293
};
294
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
+
309
const rootEventTypesToEventComponentInstances: Map<
310
DOMTopLevelEventType | string,
311
Set<ReactEventComponentInstance>,
packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
+235
@@ -12,6 +12,7 @@
12
let React;
13
let ReactFeatureFlags;
14
let ReactDOM;
15
+let ReactSymbols;
16
17
function createReactEventComponent(
18
targetEventTypes,
@@ -42,6 +43,14 @@ function dispatchClickEvent(element) {
43
element.dispatchEvent(clickEvent);
44
}
45
46
+function createReactEventTarget(type) {
47
+ return {
48
+ $$typeof: ReactSymbols.REACT_EVENT_TARGET_TYPE,
49
+ displayName: 'TestEventTarget',
50
+ type,
51
+ };
52
+}
53
+
54
// This is a new feature in Fiber so I put it in its own test file. It could
55
// probably move to one of the other test files once it is official.
56
describe('DOMEventResponderSystem', () => {
@@ -55,6 +64,7 @@ describe('DOMEventResponderSystem', () => {
64
ReactDOM = require('react-dom');
65
container = document.createElement('div');
66
document.body.appendChild(container);
67
+ ReactSymbols = require('shared/ReactSymbols');
68
});
69
70
afterEach(() => {
@@ -414,4 +424,229 @@ describe('DOMEventResponderSystem', () => {
424
expect(ownershipGained).toEqual(true);
425
expect(onOwnershipChangeFired).toEqual(1);
426
});
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
+ });
652
});
packages/react-events/src/ReactEvents.js
+12
@@ -10,6 +10,8 @@
10
import {
11
REACT_EVENT_TARGET_TYPE,
12
REACT_EVENT_TARGET_TOUCH_HIT,
13
+ REACT_EVENT_FOCUS_TARGET,
14
+ REACT_EVENT_PRESS_TARGET,
15
} from 'shared/ReactSymbols';
16
import type {ReactEventTarget} from 'shared/ReactTypes';
17
@@ -17,3 +19,13 @@ export const TouchHitTarget: ReactEventTarget = {
19
$$typeof: REACT_EVENT_TARGET_TYPE,
20
type: REACT_EVENT_TARGET_TOUCH_HIT,
21
};
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
+4
@@ -638,6 +638,10 @@ export function createFiberFromEventTarget(
638
fiber.elementType = eventTarget;
639
fiber.type = eventTarget;
640
fiber.expirationTime = expirationTime;
641
+ // Store latest props
642
+ fiber.stateNode = {
643
+ props: pendingProps,
644
+ };
645
return fiber;
646
}
647
packages/react-reconciler/src/ReactFiberCompleteWork.js
+3
@@ -841,6 +841,9 @@ function completeWork(
841
rootContainerInstance,
842
workInProgress,
843
);
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;
847
if (shouldUpdate) {
848
markUpdate(workInProgress);
849
}
packages/shared/ReactSymbols.js
+6
@@ -57,6 +57,12 @@ export const REACT_EVENT_TARGET_TYPE = hasSymbol
57
export const REACT_EVENT_TARGET_TOUCH_HIT = hasSymbol
58
? Symbol.for('react.event_target.touch_hit')
59
: 0xead7;
60
+export const REACT_EVENT_FOCUS_TARGET = hasSymbol
61
+ ? Symbol.for('react.event_target.focus')
62
+ : 0xead8;
63
+export const REACT_EVENT_PRESS_TARGET = hasSymbol
64
+ ? Symbol.for('react.event_target.press')
65
+ : 0xead9;
66
67
const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
68
const FAUX_ITERATOR_SYMBOL = '@@iterator';
packages/shared/ReactTypes.js
+8
@@ -169,4 +169,12 @@ export type ReactResponderContext = {
169
requestOwnership: () => boolean,
170
releaseOwnership: () => boolean,
171
setTimeout: (func: () => void, timeout: number) => TimeoutID,
172
+ getEventTargetsFromTarget: (
173
+ target: Element | Document,
174
+ queryType?: Symbol | number,
175
+ queryKey?: string,
176
+ ) => Array<{
177
+ node: Element,
178
+ props: null | Object,
179
+ }>,
180
};
packages/shared/getComponentName.js
+23
-11
@@ -25,10 +25,14 @@ import {
25
REACT_EVENT_COMPONENT_TYPE,
26
REACT_EVENT_TARGET_TYPE,
27
REACT_EVENT_TARGET_TOUCH_HIT,
28
+ REACT_EVENT_FOCUS_TARGET,
29
+ REACT_EVENT_PRESS_TARGET,
30
} from 'shared/ReactSymbols';
31
import {refineResolvedLazyComponent} from 'shared/ReactLazyComponent';
32
import type {ReactEventComponent, ReactEventTarget} from 'shared/ReactTypes';
33
34
+import {enableEventAPI} from './ReactFeatureFlags';
35
+
36
function getWrappedName(
37
outerType: mixed,
38
innerType: any,
@@ -94,21 +98,29 @@ function getComponentName(type: mixed): string | null {
98
break;
99
}
100
case REACT_EVENT_COMPONENT_TYPE: {
97
- const eventComponent = ((type: any): ReactEventComponent);
98
- const displayName = eventComponent.displayName;
99
- if (displayName !== undefined) {
100
- return displayName;
101
+ if (enableEventAPI) {
102
+ const eventComponent = ((type: any): ReactEventComponent);
103
+ const displayName = eventComponent.displayName;
104
+ if (displayName !== undefined) {
105
+ return displayName;
106
+ }
107
}
108
break;
109
}
110
case REACT_EVENT_TARGET_TYPE: {
105
- const eventTarget = ((type: any): ReactEventTarget);
106
- if (eventTarget.type === REACT_EVENT_TARGET_TOUCH_HIT) {
107
- return 'TouchHitTarget';
108
- }
109
- const displayName = eventTarget.displayName;
110
- if (displayName !== undefined) {
111
- return displayName;
111
+ if (enableEventAPI) {
112
+ const eventTarget = ((type: any): ReactEventTarget);
113
+ if (eventTarget.type === REACT_EVENT_TARGET_TOUCH_HIT) {
114
+ return 'TouchHitTarget';
115
+ } else if (eventTarget.type === REACT_EVENT_FOCUS_TARGET) {
116
+ return 'FocusTarget';
117
+ } else if (eventTarget.type === REACT_EVENT_PRESS_TARGET) {
118
+ return 'PressTarget';
119
+ }
120
+ const displayName = eventTarget.displayName;
121
+ if (displayName !== undefined) {
122
+ return displayName;
123
+ }
124
}
125
}
126
}