@samitouri / QOS-React / commits / dd9cef9fc0

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 }