@samitouri / QOS-React / commits / 9ebe1768a8

Experimental Event API: Redesign event responder propagation (#15408)

* Event API: Redesign event instance propagation

Dominic Gannaway committed Apr 13, 2019 at 20:37 UTC 9ebe1768a8a58cb956d6c9c9c302acbb5a51760e
10 files changed +698 -356
packages/react-dom/src/events/DOMEventResponderSystem.js
+179 -113
@@ -42,11 +42,10 @@ export function setListenToResponderEventTypes(
42 listenToResponderEventTypesImpl = _listenToResponderEventTypesImpl;
43 }
44
45 -type EventObjectTypes = {|stopPropagation: true|} | $Shape<PartialEventObject>;
45 +type EventObjectType = $Shape<PartialEventObject>;
46
47 type EventQueue = {
48 - bubble: null | Array<EventObjectTypes>,
49 - capture: null | Array<EventObjectTypes>,
48 + events: Array<EventObjectType>,
49 discrete: boolean,
50 };
51
@@ -62,10 +61,13 @@ type ResponderTimeout = {|
61
62 type ResponderTimer = {|
63 instance: ReactEventComponentInstance,
65 - func: () => void,
64 + func: () => boolean,
65 id: Symbol,
66 |};
67
68 +const ROOT_PHASE = 0;
69 +const BUBBLE_PHASE = 1;
70 +const CAPTURE_PHASE = 2;
71 const activeTimeouts: Map<Symbol, ResponderTimeout> = new Map();
72 const rootEventTypesToEventComponentInstances: Map<
73 DOMTopLevelEventType | string,
@@ -93,7 +95,7 @@ const eventResponderContext: ReactResponderContext = {
95 dispatchEvent(
96 possibleEventObject: Object,
97 listener: ($Shape<PartialEventObject>) => void,
96 - {capture, discrete}: ReactResponderDispatchEventOptions,
98 + {discrete}: ReactResponderDispatchEventOptions,
99 ): void {
100 validateResponderContext();
101 const {target, type} = possibleEventObject;
@@ -122,17 +124,12 @@ const eventResponderContext: ReactResponderContext = {
124 const eventObject = ((possibleEventObject: any): $Shape<
125 PartialEventObject,
126 >);
125 - const events = getEventsFromEventQueue(capture);
127 + const eventQueue = ((currentEventQueue: any): EventQueue);
128 if (discrete) {
127 - ((currentEventQueue: any): EventQueue).discrete = true;
129 + eventQueue.discrete = true;
130 }
131 eventListeners.set(eventObject, listener);
130 - events.push(eventObject);
131 - },
132 - dispatchStopPropagation(capture?: boolean) {
133 - validateResponderContext();
134 - const events = getEventsFromEventQueue();
135 - events.push({stopPropagation: true});
132 + eventQueue.events.push(eventObject);
133 },
134 isPositionWithinTouchHitTarget(doc: Document, x: number, y: number): boolean {
135 validateResponderContext();
@@ -256,7 +253,7 @@ const eventResponderContext: ReactResponderContext = {
253 triggerOwnershipListeners();
254 return false;
255 },
259 - setTimeout(func: () => void, delay): Symbol {
256 + setTimeout(func: () => boolean, delay): Symbol {
257 validateResponderContext();
258 if (currentTimers === null) {
259 currentTimers = new Map();
@@ -350,37 +347,23 @@ const eventResponderContext: ReactResponderContext = {
347 },
348 };
349
353 -function getEventsFromEventQueue(capture?: boolean): Array<EventObjectTypes> {
354 - const eventQueue = ((currentEventQueue: any): EventQueue);
355 - let events;
356 - if (capture) {
357 - events = eventQueue.capture;
358 - if (events === null) {
359 - events = eventQueue.capture = [];
360 - }
361 - } else {
362 - events = eventQueue.bubble;
363 - if (events === null) {
364 - events = eventQueue.bubble = [];
365 - }
366 - }
367 - return events;
368 -}
369 -
350 function processTimers(timers: Map<Symbol, ResponderTimer>): void {
351 const timersArr = Array.from(timers.values());
352 + let shouldStopPropagation = false;
353 currentEventQueue = createEventQueue();
354 try {
355 for (let i = 0; i < timersArr.length; i++) {
356 const {instance, func, id} = timersArr[i];
357 currentInstance = instance;
358 try {
378 - func();
359 + if (!shouldStopPropagation) {
360 + shouldStopPropagation = func();
361 + }
362 } finally {
363 activeTimeouts.delete(id);
364 }
365 }
383 - batchedUpdates(processEventQueue, currentEventQueue);
366 + processEventQueue();
367 } finally {
368 currentTimers = null;
369 currentInstance = null;
@@ -407,20 +390,25 @@ function createResponderEvent(
390 nativeEvent: AnyNativeEvent,
391 nativeEventTarget: Element | Document,
392 eventSystemFlags: EventSystemFlags,
393 + phase: 0 | 1 | 2,
394 ): ReactResponderEvent {
411 - return {
395 + const responderEvent = {
396 nativeEvent: nativeEvent,
397 target: nativeEventTarget,
398 type: topLevelType,
399 passive: (eventSystemFlags & IS_PASSIVE) !== 0,
400 passiveSupported: (eventSystemFlags & PASSIVE_NOT_SUPPORTED) === 0,
401 + phase,
402 };
403 + if (__DEV__) {
404 + Object.freeze(responderEvent);
405 + }
406 + return responderEvent;
407 }
408
409 function createEventQueue(): EventQueue {
410 return {
422 - bubble: null,
423 - capture: null,
411 + events: [],
412 discrete: false,
413 };
414 }
@@ -433,41 +421,24 @@ function processEvent(event: $Shape<PartialEventObject>): void {
421 invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
422 }
423
436 -function processEvents(
437 - bubble: null | Array<EventObjectTypes>,
438 - capture: null | Array<EventObjectTypes>,
439 -): void {
440 - let i, length;
441 -
442 - if (capture !== null) {
443 - for (i = capture.length; i-- > 0; ) {
444 - const event = capture[i];
445 - if (event.stopPropagation === true) {
446 - return;
447 - }
448 - processEvent(((event: any): $Shape<PartialEventObject>));
449 - }
450 - }
451 - if (bubble !== null) {
452 - for (i = 0, length = bubble.length; i < length; ++i) {
453 - const event = bubble[i];
454 - if (event.stopPropagation === true) {
455 - return;
456 - }
457 - processEvent(((event: any): $Shape<PartialEventObject>));
458 - }
424 +function processEvents(events: Array<EventObjectType>): void {
425 + for (let i = 0, length = events.length; i < length; i++) {
426 + processEvent(events[i]);
427 }
428 }
429
430 export function processEventQueue(): void {
463 - const {bubble, capture, discrete} = ((currentEventQueue: any): EventQueue);
431 + const {events, discrete} = ((currentEventQueue: any): EventQueue);
432
433 + if (events.length === 0) {
434 + return;
435 + }
436 if (discrete) {
437 interactiveUpdates(() => {
467 - processEvents(bubble, capture);
438 + batchedUpdates(processEvents, events);
439 });
440 } else {
470 - processEvents(bubble, capture);
441 + batchedUpdates(processEvents, events);
442 }
443 }
444
@@ -489,77 +460,146 @@ function getTargetEventTypes(
460 return cachedSet;
461 }
462
492 -function handleTopLevelType(
463 +function getTargetEventResponderInstances(
464 topLevelType: DOMTopLevelEventType,
494 - responderEvent: ReactResponderEvent,
495 - eventComponentInstance: ReactEventComponentInstance,
496 - isRootLevelEvent: boolean,
497 -): void {
498 - let {props, responder, state} = eventComponentInstance;
499 - if (!isRootLevelEvent) {
500 - // Validate the target event type exists on the responder
501 - const targetEventTypes = getTargetEventTypes(responder.targetEventTypes);
502 - if (!targetEventTypes.has(topLevelType)) {
503 - return;
465 + targetFiber: null | Fiber,
466 +): Array<ReactEventComponentInstance> {
467 + const eventResponderInstances = [];
468 + let node = targetFiber;
469 + while (node !== null) {
470 + // Traverse up the fiber tree till we find event component fibers.
471 + if (node.tag === EventComponent) {
472 + const eventComponentInstance = node.stateNode;
473 + if (currentOwner === null || currentOwner === eventComponentInstance) {
474 + const responder = eventComponentInstance.responder;
475 + // Validate the target event type exists on the responder
476 + const targetEventTypes = getTargetEventTypes(
477 + responder.targetEventTypes,
478 + );
479 + if (targetEventTypes.has(topLevelType)) {
480 + eventResponderInstances.push(eventComponentInstance);
481 + }
482 + }
483 + }
484 + node = node.return;
485 + }
486 + return eventResponderInstances;
487 +}
488 +
489 +function getRootEventResponderInstances(
490 + topLevelType: DOMTopLevelEventType,
491 +): Array<ReactEventComponentInstance> {
492 + const eventResponderInstances = [];
493 + const rootEventInstances = rootEventTypesToEventComponentInstances.get(
494 + topLevelType,
495 + );
496 + if (rootEventInstances !== undefined) {
497 + const rootEventComponentInstances = Array.from(rootEventInstances);
498 +
499 + for (let i = 0; i < rootEventComponentInstances.length; i++) {
500 + const rootEventComponentInstance = rootEventComponentInstances[i];
501 +
502 + if (
503 + currentOwner === null ||
504 + currentOwner === rootEventComponentInstance
505 + ) {
506 + eventResponderInstances.push(rootEventComponentInstance);
507 + }
508 }
509 }
510 + return eventResponderInstances;
511 +}
512 +
513 +function triggerEventResponderEventListener(
514 + responderEvent: ReactResponderEvent,
515 + eventComponentInstance: ReactEventComponentInstance,
516 +): boolean {
517 + const {responder, props, state} = eventComponentInstance;
518 currentInstance = eventComponentInstance;
507 - responder.onEvent(responderEvent, eventResponderContext, props, state);
519 + return responder.onEvent(responderEvent, eventResponderContext, props, state);
520 }
521
510 -export function runResponderEventsInBatch(
522 +function traverseAndTriggerEventResponderInstances(
523 topLevelType: DOMTopLevelEventType,
524 targetFiber: null | Fiber,
525 nativeEvent: AnyNativeEvent,
526 nativeEventTarget: EventTarget,
527 eventSystemFlags: EventSystemFlags,
528 ): void {
517 - if (enableEventAPI) {
518 - currentEventQueue = createEventQueue();
519 - const responderEvent = createResponderEvent(
529 + // Trigger event responders in this order:
530 + // - Capture target phase
531 + // - Bubble target phase
532 + // - Root phase
533 +
534 + const targetEventResponderInstances = getTargetEventResponderInstances(
535 + topLevelType,
536 + targetFiber,
537 + );
538 + let length = targetEventResponderInstances.length;
539 + let i;
540 + let shouldStopPropagation = false;
541 + let responderEvent;
542 +
543 + // Capture target phase
544 + if (length > 0) {
545 + responderEvent = createResponderEvent(
546 ((topLevelType: any): string),
547 nativeEvent,
548 ((nativeEventTarget: any): Element | Document),
549 eventSystemFlags,
550 + CAPTURE_PHASE,
551 );
525 -
526 - try {
527 - let node = targetFiber;
528 - // Traverse up the fiber tree till we find event component fibers.
529 - while (node !== null) {
530 - if (node.tag === EventComponent) {
531 - const eventComponentInstance = node.stateNode;
532 - handleTopLevelType(
533 - topLevelType,
534 - responderEvent,
535 - eventComponentInstance,
536 - false,
537 - );
538 - }
539 - node = node.return;
552 + for (i = length; i-- > 0; ) {
553 + const targetEventResponderInstance = targetEventResponderInstances[i];
554 + shouldStopPropagation = triggerEventResponderEventListener(
555 + responderEvent,
556 + targetEventResponderInstance,
557 + );
558 + if (shouldStopPropagation) {
559 + return;
560 }
541 - // Handle root level events
542 - const rootEventInstances = rootEventTypesToEventComponentInstances.get(
543 - topLevelType,
561 + }
562 + // Bubble target phase
563 + responderEvent = createResponderEvent(
564 + ((topLevelType: any): string),
565 + nativeEvent,
566 + ((nativeEventTarget: any): Element | Document),
567 + eventSystemFlags,
568 + BUBBLE_PHASE,
569 + );
570 + for (i = 0; i < length; i++) {
571 + const targetEventResponderInstance = targetEventResponderInstances[i];
572 + shouldStopPropagation = triggerEventResponderEventListener(
573 + responderEvent,
574 + targetEventResponderInstance,
575 );
545 - if (rootEventInstances !== undefined) {
546 - const rootEventComponentInstances = Array.from(rootEventInstances);
547 -
548 - for (let i = 0; i < rootEventComponentInstances.length; i++) {
549 - const rootEventComponentInstance = rootEventComponentInstances[i];
550 - handleTopLevelType(
551 - topLevelType,
552 - responderEvent,
553 - rootEventComponentInstance,
554 - true,
555 - );
556 - }
576 + if (shouldStopPropagation) {
577 + return;
578 + }
579 + }
580 + }
581 + // Root phase
582 + const rootEventResponderInstances = getRootEventResponderInstances(
583 + topLevelType,
584 + );
585 + length = rootEventResponderInstances.length;
586 + if (length > 0) {
587 + responderEvent = createResponderEvent(
588 + ((topLevelType: any): string),
589 + nativeEvent,
590 + ((nativeEventTarget: any): Element | Document),
591 + eventSystemFlags,
592 + ROOT_PHASE,
593 + );
594 + for (i = 0; i < length; i++) {
595 + const targetEventResponderInstance = rootEventResponderInstances[i];
596 + shouldStopPropagation = triggerEventResponderEventListener(
597 + responderEvent,
598 + targetEventResponderInstance,
599 + );
600 + if (shouldStopPropagation) {
601 + return;
602 }
558 - processEventQueue();
559 - } finally {
560 - currentTimers = null;
561 - currentInstance = null;
562 - currentEventQueue = null;
603 }
604 }
605 }
@@ -621,3 +661,29 @@ function validateResponderContext(): void {
661 'Use context.setTimeout() to use asynchronous responder context outside of event cycle .',
662 );
663 }
664 +
665 +export function dispatchEventForResponderEventSystem(
666 + topLevelType: DOMTopLevelEventType,
667 + targetFiber: null | Fiber,
668 + nativeEvent: AnyNativeEvent,
669 + nativeEventTarget: EventTarget,
670 + eventSystemFlags: EventSystemFlags,
671 +): void {
672 + if (enableEventAPI) {
673 + currentEventQueue = createEventQueue();
674 + try {
675 + traverseAndTriggerEventResponderInstances(
676 + topLevelType,
677 + targetFiber,
678 + nativeEvent,
679 + nativeEventTarget,
680 + eventSystemFlags,
681 + );
682 + processEventQueue();
683 + } finally {
684 + currentTimers = null;
685 + currentInstance = null;
686 + currentEventQueue = null;
687 + }
688 + }
689 +}
packages/react-dom/src/events/ReactDOMEventListener.js
+54 -38
@@ -13,7 +13,7 @@ import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
13
14 import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching';
15 import {runExtractedPluginEventsInBatch} from 'events/EventPluginHub';
16 -import {runResponderEventsInBatch} from '../events/DOMEventResponderSystem';
16 +import {dispatchEventForResponderEventSystem} from '../events/DOMEventResponderSystem';
17 import {isFiberMounted} from 'react-reconciler/reflection';
18 import {HostRoot} from 'shared/ReactWorkTags';
19 import {
@@ -48,7 +48,6 @@ type BookKeepingInstance = {
48 nativeEvent: AnyNativeEvent | null,
49 targetInst: Fiber | null,
50 ancestors: Array<Fiber | null>,
51 - eventSystemFlags: EventSystemFlags,
51 };
52
53 /**
@@ -75,14 +74,12 @@ function getTopLevelCallbackBookKeeping(
74 topLevelType: DOMTopLevelEventType,
75 nativeEvent: AnyNativeEvent,
76 targetInst: Fiber | null,
78 - eventSystemFlags: EventSystemFlags,
77 ): BookKeepingInstance {
78 if (callbackBookkeepingPool.length) {
79 const instance = callbackBookkeepingPool.pop();
80 instance.topLevelType = topLevelType;
81 instance.nativeEvent = nativeEvent;
82 instance.targetInst = targetInst;
85 - instance.eventSystemFlags = eventSystemFlags;
83 return instance;
84 }
85 return {
@@ -90,7 +87,6 @@ function getTopLevelCallbackBookKeeping(
87 nativeEvent,
88 targetInst,
89 ancestors: [],
93 - eventSystemFlags,
90 };
91 }
92
@@ -101,7 +97,6 @@ function releaseTopLevelCallbackBookKeeping(
97 instance.nativeEvent = null;
98 instance.targetInst = null;
99 instance.ancestors.length = 0;
104 - instance.eventSystemFlags = 0;
100 if (callbackBookkeepingPool.length < CALLBACK_BOOKKEEPING_POOL_SIZE) {
101 callbackBookkeepingPool.push(instance);
102 }
@@ -131,28 +126,16 @@ function handleTopLevel(bookKeeping: BookKeepingInstance) {
126
127 for (let i = 0; i < bookKeeping.ancestors.length; i++) {
128 targetInst = bookKeeping.ancestors[i];
134 - const eventSystemFlags = bookKeeping.eventSystemFlags;
129 const eventTarget = getEventTarget(bookKeeping.nativeEvent);
130 const topLevelType = ((bookKeeping.topLevelType: any): DOMTopLevelEventType);
131 const nativeEvent = ((bookKeeping.nativeEvent: any): AnyNativeEvent);
132
139 - if (eventSystemFlags === PLUGIN_EVENT_SYSTEM) {
140 - runExtractedPluginEventsInBatch(
141 - topLevelType,
142 - targetInst,
143 - nativeEvent,
144 - eventTarget,
145 - );
146 - } else if (enableEventAPI) {
147 - // Responder event system (experimental event API)
148 - runResponderEventsInBatch(
149 - topLevelType,
150 - targetInst,
151 - nativeEvent,
152 - eventTarget,
153 - eventSystemFlags,
154 - );
155 - }
133 + runExtractedPluginEventsInBatch(
134 + topLevelType,
135 + targetInst,
136 + nativeEvent,
137 + eventTarget,
138 + );
139 }
140 }
141
@@ -242,6 +225,27 @@ function dispatchInteractiveEvent(topLevelType, eventSystemFlags, nativeEvent) {
225 );
226 }
227
228 +function dispatchEventForPluginEventSystem(
229 + topLevelType: DOMTopLevelEventType,
230 + eventSystemFlags: EventSystemFlags,
231 + nativeEvent: AnyNativeEvent,
232 + targetInst: null | Fiber,
233 +): void {
234 + const bookKeeping = getTopLevelCallbackBookKeeping(
235 + topLevelType,
236 + nativeEvent,
237 + targetInst,
238 + );
239 +
240 + try {
241 + // Event queue being processed in the same cycle allows
242 + // `preventDefault`.
243 + batchedUpdates(handleTopLevel, bookKeeping);
244 + } finally {
245 + releaseTopLevelCallbackBookKeeping(bookKeeping);
246 + }
247 +}
248 +
249 export function dispatchEvent(
250 topLevelType: DOMTopLevelEventType,
251 eventSystemFlags: EventSystemFlags,
@@ -250,9 +254,9 @@ export function dispatchEvent(
254 if (!_enabled) {
255 return;
256 }
253 -
257 const nativeEventTarget = getEventTarget(nativeEvent);
258 let targetInst = getClosestInstanceFromNode(nativeEventTarget);
259 +
260 if (
261 targetInst !== null &&
262 typeof targetInst.tag === 'number' &&
@@ -265,18 +269,30 @@ export function dispatchEvent(
269 targetInst = null;
270 }
271
268 - const bookKeeping = getTopLevelCallbackBookKeeping(
269 - topLevelType,
270 - nativeEvent,
271 - targetInst,
272 - eventSystemFlags,
273 - );
274 -
275 - try {
276 - // Event queue being processed in the same cycle allows
277 - // `preventDefault`.
278 - batchedUpdates(handleTopLevel, bookKeeping);
279 - } finally {
280 - releaseTopLevelCallbackBookKeeping(bookKeeping);
272 + if (enableEventAPI) {
273 + if (eventSystemFlags === PLUGIN_EVENT_SYSTEM) {
274 + dispatchEventForPluginEventSystem(
275 + topLevelType,
276 + eventSystemFlags,
277 + nativeEvent,
278 + targetInst,
279 + );
280 + } else {
281 + // Responder event system (experimental event API)
282 + dispatchEventForResponderEventSystem(
283 + topLevelType,
284 + targetInst,
285 + nativeEvent,
286 + nativeEventTarget,
287 + eventSystemFlags,
288 + );
289 + }
290 + } else {
291 + dispatchEventForPluginEventSystem(
292 + topLevelType,
293 + eventSystemFlags,
294 + nativeEvent,
295 + targetInst,
296 + );
297 }
298 }
packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
+212 -41
@@ -37,6 +37,18 @@ function createReactEventComponent(
37 };
38 }
39
40 +const ROOT_PHASE = 0;
41 +const BUBBLE_PHASE = 1;
42 +const CAPTURE_PHASE = 2;
43 +
44 +function phaseToString(phase) {
45 + return phase === ROOT_PHASE
46 + ? 'root'
47 + : phase === BUBBLE_PHASE
48 + ? 'bubble'
49 + : 'capture';
50 +}
51 +
52 function dispatchClickEvent(element) {
53 const clickEvent = document.createEvent('Event');
54 clickEvent.initEvent('click', true, true);
@@ -86,6 +98,7 @@ describe('DOMEventResponderSystem', () => {
98 name: event.type,
99 passive: event.passive,
100 passiveSupported: event.passiveSupported,
101 + phase: event.phase,
102 });
103 },
104 );
@@ -99,28 +112,37 @@ describe('DOMEventResponderSystem', () => {
112 ReactDOM.render(<Test />, container);
113 expect(container.innerHTML).toBe('<button>Click me!</button>');
114
102 - // Clicking the button should trigger the event responder onEvent()
115 + // Clicking the button should trigger the event responder onEvent() twice
116 let buttonElement = buttonRef.current;
117 dispatchClickEvent(buttonElement);
105 - expect(eventResponderFiredCount).toBe(1);
106 - expect(eventLog.length).toBe(1);
118 + expect(eventResponderFiredCount).toBe(2);
119 + expect(eventLog.length).toBe(2);
120 // JSDOM does not support passive events, so this will be false
108 - expect(eventLog[0]).toEqual({
109 - name: 'click',
110 - passive: false,
111 - passiveSupported: false,
112 - });
121 + expect(eventLog).toEqual([
122 + {
123 + name: 'click',
124 + passive: false,
125 + passiveSupported: false,
126 + phase: CAPTURE_PHASE,
127 + },
128 + {
129 + name: 'click',
130 + passive: false,
131 + passiveSupported: false,
132 + phase: BUBBLE_PHASE,
133 + },
134 + ]);
135
136 // Unmounting the container and clicking should not increment anything
137 ReactDOM.render(null, container);
138 dispatchClickEvent(buttonElement);
117 - expect(eventResponderFiredCount).toBe(1);
139 + expect(eventResponderFiredCount).toBe(2);
140
119 - // Re-rendering the container and clicking should increase the counter again
141 + // Re-rendering the container and clicking should increase the counters again
142 ReactDOM.render(<Test />, container);
143 buttonElement = buttonRef.current;
144 dispatchClickEvent(buttonElement);
123 - expect(eventResponderFiredCount).toBe(2);
145 + expect(eventResponderFiredCount).toBe(4);
146 });
147
148 it('the event responder onEvent() function should fire on click event (passive events forced)', () => {
@@ -139,6 +161,7 @@ describe('DOMEventResponderSystem', () => {
161 name: event.type,
162 passive: event.passive,
163 passiveSupported: event.passiveSupported,
164 + phase: event.phase,
165 });
166 },
167 );
@@ -154,12 +177,21 @@ describe('DOMEventResponderSystem', () => {
177 // Clicking the button should trigger the event responder onEvent()
178 let buttonElement = buttonRef.current;
179 dispatchClickEvent(buttonElement);
157 - expect(eventLog.length).toBe(1);
158 - expect(eventLog[0]).toEqual({
159 - name: 'click',
160 - passive: true,
161 - passiveSupported: true,
162 - });
180 + expect(eventLog.length).toBe(2);
181 + expect(eventLog).toEqual([
182 + {
183 + name: 'click',
184 + passive: true,
185 + passiveSupported: true,
186 + phase: CAPTURE_PHASE,
187 + },
188 + {
189 + name: 'click',
190 + passive: true,
191 + passiveSupported: true,
192 + phase: BUBBLE_PHASE,
193 + },
194 + ]);
195 });
196
197 it('nested event responders and their onEvent() function should fire multiple times', () => {
@@ -176,6 +208,7 @@ describe('DOMEventResponderSystem', () => {
208 name: event.type,
209 passive: event.passive,
210 passiveSupported: event.passiveSupported,
211 + phase: event.phase,
212 });
213 },
214 );
@@ -193,19 +226,35 @@ describe('DOMEventResponderSystem', () => {
226 // Clicking the button should trigger the event responder onEvent()
227 let buttonElement = buttonRef.current;
228 dispatchClickEvent(buttonElement);
196 - expect(eventResponderFiredCount).toBe(2);
197 - expect(eventLog.length).toBe(2);
229 + expect(eventResponderFiredCount).toBe(4);
230 + expect(eventLog.length).toBe(4);
231 // JSDOM does not support passive events, so this will be false
199 - expect(eventLog[0]).toEqual({
200 - name: 'click',
201 - passive: false,
202 - passiveSupported: false,
203 - });
204 - expect(eventLog[1]).toEqual({
205 - name: 'click',
206 - passive: false,
207 - passiveSupported: false,
208 - });
232 + expect(eventLog).toEqual([
233 + {
234 + name: 'click',
235 + passive: false,
236 + passiveSupported: false,
237 + phase: CAPTURE_PHASE,
238 + },
239 + {
240 + name: 'click',
241 + passive: false,
242 + passiveSupported: false,
243 + phase: CAPTURE_PHASE,
244 + },
245 + {
246 + name: 'click',
247 + passive: false,
248 + passiveSupported: false,
249 + phase: BUBBLE_PHASE,
250 + },
251 + {
252 + name: 'click',
253 + passive: false,
254 + passiveSupported: false,
255 + phase: BUBBLE_PHASE,
256 + },
257 + ]);
258 });
259
260 it('nested event responders and their onEvent() should fire in the correct order', () => {
@@ -215,16 +264,16 @@ describe('DOMEventResponderSystem', () => {
264 const ClickEventComponentA = createReactEventComponent(
265 ['click'],
266 undefined,
218 - (context, props) => {
219 - eventLog.push('A');
267 + (event, context, props) => {
268 + eventLog.push(`A [${phaseToString(event.phase)}]`);
269 },
270 );
271
272 const ClickEventComponentB = createReactEventComponent(
273 ['click'],
274 undefined,
226 - (context, props) => {
227 - eventLog.push('B');
275 + (event, context, props) => {
276 + eventLog.push(`B [${phaseToString(event.phase)}]`);
277 },
278 );
279
@@ -242,7 +291,109 @@ describe('DOMEventResponderSystem', () => {
291 let buttonElement = buttonRef.current;
292 dispatchClickEvent(buttonElement);
293
245 - expect(eventLog).toEqual(['B', 'A']);
294 + expect(eventLog).toEqual([
295 + 'A [capture]',
296 + 'B [capture]',
297 + 'B [bubble]',
298 + 'A [bubble]',
299 + ]);
300 + });
301 +
302 + it('nested event responders and their onEvent() should fire in the correct order with stopPropagation', () => {
303 + let eventLog;
304 + let stopPropagationOnPhase;
305 + const buttonRef = React.createRef();
306 +
307 + const ClickEventComponentA = createReactEventComponent(
308 + ['click'],
309 + undefined,
310 + (event, context, props) => {
311 + eventLog.push(`A [${phaseToString(event.phase)}]`);
312 + },
313 + );
314 +
315 + const ClickEventComponentB = createReactEventComponent(
316 + ['click'],
317 + undefined,
318 + (event, context, props) => {
319 + eventLog.push(`B [${phaseToString(event.phase)}]`);
320 + if (event.phase === stopPropagationOnPhase) {
321 + return true;
322 + }
323 + },
324 + );
325 +
326 + const Test = () => (
327 + <ClickEventComponentA>
328 + <ClickEventComponentB>
329 + <button ref={buttonRef}>Click me!</button>
330 + </ClickEventComponentB>
331 + </ClickEventComponentA>
332 + );
333 +
334 + function runTestWithPhase(phase) {
335 + eventLog = [];
336 + stopPropagationOnPhase = phase;
337 + ReactDOM.render(<Test />, container);
338 + let buttonElement = buttonRef.current;
339 + dispatchClickEvent(buttonElement);
340 + }
341 +
342 + runTestWithPhase(BUBBLE_PHASE);
343 + expect(eventLog).toEqual(['A [capture]', 'B [capture]', 'B [bubble]']);
344 + runTestWithPhase(CAPTURE_PHASE);
345 + expect(eventLog).toEqual(['A [capture]', 'B [capture]']);
346 + });
347 +
348 + it('nested event responders and their onEvent() should fire in the correct order with stopPropagation #2', () => {
349 + let eventLog;
350 + let stopPropagationOnPhase;
351 + const buttonRef = React.createRef();
352 +
353 + const ClickEventComponentA = createReactEventComponent(
354 + ['click'],
355 + undefined,
356 + (event, context, props) => {
357 + eventLog.push(`A [${phaseToString(event.phase)}]`);
358 + if (event.phase === stopPropagationOnPhase) {
359 + return true;
360 + }
361 + },
362 + );
363 +
364 + const ClickEventComponentB = createReactEventComponent(
365 + ['click'],
366 + undefined,
367 + (event, context, props) => {
368 + eventLog.push(`B [${phaseToString(event.phase)}]`);
369 + },
370 + );
371 +
372 + const Test = () => (
373 + <ClickEventComponentA>
374 + <ClickEventComponentB>
375 + <button ref={buttonRef}>Click me!</button>
376 + </ClickEventComponentB>
377 + </ClickEventComponentA>
378 + );
379 +
380 + function runTestWithPhase(phase) {
381 + eventLog = [];
382 + stopPropagationOnPhase = phase;
383 + ReactDOM.render(<Test />, container);
384 + let buttonElement = buttonRef.current;
385 + dispatchClickEvent(buttonElement);
386 + }
387 +
388 + runTestWithPhase(BUBBLE_PHASE);
389 + expect(eventLog).toEqual([
390 + 'A [capture]',
391 + 'B [capture]',
392 + 'B [bubble]',
393 + 'A [bubble]',
394 + ]);
395 + runTestWithPhase(CAPTURE_PHASE);
396 + expect(eventLog).toEqual(['A [capture]']);
397 });
398
399 it('custom event dispatching for click -> magicClick works', () => {
@@ -257,6 +408,7 @@ describe('DOMEventResponderSystem', () => {
408 const syntheticEvent = {
409 target: event.target,
410 type: 'magicclick',
411 + phase: phaseToString(event.phase),
412 };
413 context.dispatchEvent(syntheticEvent, props.onMagicClick, {
414 discrete: true,
@@ -266,7 +418,7 @@ describe('DOMEventResponderSystem', () => {
418 );
419
420 function handleMagicEvent(e) {
269 - eventLog.push('magic event fired', e.type);
421 + eventLog.push('magic event fired', e.type, e.phase);
422 }
423
424 const Test = () => (
@@ -281,7 +433,14 @@ describe('DOMEventResponderSystem', () => {
433 let buttonElement = buttonRef.current;
434 dispatchClickEvent(buttonElement);
435
284 - expect(eventLog).toEqual(['magic event fired', 'magicclick']);
436 + expect(eventLog).toEqual([
437 + 'magic event fired',
438 + 'magicclick',
439 + 'capture',
440 + 'magic event fired',
441 + 'magicclick',
442 + 'bubble',
443 + ]);
444 });
445
446 it('async event dispatching works', () => {
@@ -295,6 +454,7 @@ describe('DOMEventResponderSystem', () => {
454 const pressEvent = {
455 target: event.target,
456 type: 'press',
457 + phase: phaseToString(event.phase),
458 };
459 context.dispatchEvent(pressEvent, props.onPress, {discrete: true});
460
@@ -303,6 +463,7 @@ describe('DOMEventResponderSystem', () => {
463 const longPressEvent = {
464 target: event.target,
465 type: 'longpress',
466 + phase: phaseToString(event.phase),
467 };
468 context.dispatchEvent(longPressEvent, props.onLongPress, {
469 discrete: true,
@@ -313,6 +474,7 @@ describe('DOMEventResponderSystem', () => {
474 const longPressChangeEvent = {
475 target: event.target,
476 type: 'longpresschange',
477 + phase: phaseToString(event.phase),
478 };
479 context.dispatchEvent(
480 longPressChangeEvent,
@@ -330,9 +492,9 @@ describe('DOMEventResponderSystem', () => {
492
493 const Test = () => (
494 <LongPressEventComponent
333 - onPress={() => log('press')}
334 - onLongPress={() => log('longpress')}
335 - onLongPressChange={() => log('longpresschange')}>
495 + onPress={e => log('press ' + e.phase)}
496 + onLongPress={e => log('longpress ' + e.phase)}
497 + onLongPressChange={e => log('longpresschange ' + e.phase)}>
498 <button ref={buttonRef}>Click me!</button>
499 </LongPressEventComponent>
500 );
@@ -344,7 +506,14 @@ describe('DOMEventResponderSystem', () => {
506 dispatchClickEvent(buttonElement);
507 jest.runAllTimers();
508
347 - expect(eventLog).toEqual(['press', 'longpress', 'longpresschange']);
509 + expect(eventLog).toEqual([
510 + 'press capture',
511 + 'press bubble',
512 + 'longpress capture',
513 + 'longpresschange capture',
514 + 'longpress bubble',
515 + 'longpresschange bubble',
516 + ]);
517 });
518
519 it('the event responder onUnmount() function should fire', () => {
@@ -404,7 +573,9 @@ describe('DOMEventResponderSystem', () => {
573 ['click'],
574 undefined,
575 (event, context, props, state) => {
407 - ownershipGained = context.requestOwnership();
576 + if (event.phase === BUBBLE_PHASE) {
577 + ownershipGained = context.requestOwnership();
578 + }
579 },
580 undefined,
581 () => {
packages/react-events/src/Drag.js
+9 -3
@@ -13,6 +13,7 @@ import type {
13 } from 'shared/ReactTypes';
14 import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
15
16 +const CAPTURE_PHASE = 2;
17 const targetEventTypes = ['pointerdown', 'pointercancel'];
18 const rootEventTypes = ['pointerup', {name: 'pointermove', passive: false}];
19
@@ -92,9 +93,13 @@ const DragResponder = {
93 context: ReactResponderContext,
94 props: Object,
95 state: DragState,
95 - ): void {
96 - const {target, type, nativeEvent} = event;
96 + ): boolean {
97 + const {target, phase, type, nativeEvent} = event;
98
99 + // Drag doesn't handle capture target events at this point
100 + if (phase === CAPTURE_PHASE) {
101 + return false;
102 + }
103 switch (type) {
104 case 'touchstart':
105 case 'mousedown':
@@ -132,7 +137,7 @@ const DragResponder = {
137 case 'mousemove':
138 case 'pointermove': {
139 if (event.passive) {
135 - return;
140 + return false;
141 }
142 if (state.isPointerDown) {
143 const obj =
@@ -225,6 +230,7 @@ const DragResponder = {
230 break;
231 }
232 }
233 + return false;
234 },
235 };
236
packages/react-events/src/Focus.js
+10 -3
@@ -13,6 +13,8 @@ import type {
13 } from 'shared/ReactTypes';
14 import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
15
16 +const CAPTURE_PHASE = 2;
17 +
18 type FocusProps = {
19 disabled: boolean,
20 onBlur: (e: FocusEvent) => void,
@@ -136,12 +138,16 @@ const FocusResponder = {
138 context: ReactResponderContext,
139 props: Object,
140 state: FocusState,
139 - ): void {
140 - const {type, target} = event;
141 + ): boolean {
142 + const {type, phase, target} = event;
143
144 + // Focus doesn't handle capture target events at this point
145 + if (phase === CAPTURE_PHASE) {
146 + return false;
147 + }
148 switch (type) {
149 case 'focus': {
144 - if (!state.isFocused && !context.hasOwnership()) {
150 + if (!state.isFocused) {
151 state.focusTarget = target;
152 dispatchFocusInEvents(event, context, props, state);
153 state.isFocused = true;
@@ -157,6 +163,7 @@ const FocusResponder = {
163 break;
164 }
165 }
166 + return false;
167 },
168 onUnmount(
169 context: ReactResponderContext,
packages/react-events/src/Hover.js
+15 -6
@@ -13,6 +13,8 @@ import type {
13 } from 'shared/ReactTypes';
14 import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
15
16 +const CAPTURE_PHASE = 2;
17 +
18 type HoverProps = {
19 disabled: boolean,
20 delayHoverEnd: number,
@@ -132,6 +134,7 @@ function dispatchHoverStartEvents(
134 state.hoverStartTimeout = context.setTimeout(() => {
135 state.hoverStartTimeout = null;
136 activate();
137 + return false;
138 }, delayHoverStart);
139 } else {
140 activate();
@@ -186,6 +189,7 @@ function dispatchHoverEndEvents(
189 if (delayHoverEnd > 0) {
190 state.hoverEndTimeout = context.setTimeout(() => {
191 deactivate();
192 + return false;
193 }, delayHoverEnd);
194 } else {
195 deactivate();
@@ -226,9 +230,13 @@ const HoverResponder = {
230 context: ReactResponderContext,
231 props: HoverProps,
232 state: HoverState,
229 - ): void {
230 - const {type, target, nativeEvent} = event;
233 + ): boolean {
234 + const {type, phase, target, nativeEvent} = event;
235
236 + // Hover doesn't handle capture target events at this point
237 + if (phase === CAPTURE_PHASE) {
238 + return false;
239 + }
240 switch (type) {
241 /**
242 * Prevent hover events when touch is being used.
@@ -242,10 +250,10 @@ const HoverResponder = {
250
251 case 'pointerover':
252 case 'mouseover': {
245 - if (!state.isHovered && !state.isTouched && !context.hasOwnership()) {
253 + if (!state.isHovered && !state.isTouched) {
254 if ((nativeEvent: any).pointerType === 'touch') {
255 state.isTouched = true;
248 - return;
256 + return false;
257 }
258 if (type === 'pointerover') {
259 state.skipMouseAfterPointer = true;
@@ -258,7 +266,7 @@ const HoverResponder = {
266 )
267 ) {
268 state.isInHitSlop = true;
261 - return;
269 + return false;
270 }
271 state.hoverTarget = target;
272 dispatchHoverStartEvents(event, context, props, state);
@@ -280,7 +288,7 @@ const HoverResponder = {
288 case 'pointermove':
289 case 'mousemove': {
290 if (type === 'mousemove' && state.skipMouseAfterPointer === true) {
283 - return;
291 + return false;
292 }
293
294 if (state.isHovered && !state.isTouched) {
@@ -330,6 +338,7 @@ const HoverResponder = {
338 break;
339 }
340 }
341 + return false;
342 },
343 onUnmount(
344 context: ReactResponderContext,
packages/react-events/src/Press.js
+52 -48
@@ -14,6 +14,8 @@ import type {
14 } from 'shared/ReactTypes';
15 import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
16
17 +const CAPTURE_PHASE = 2;
18 +
19 type PressProps = {
20 disabled: boolean,
21 delayLongPress: number,
@@ -40,7 +42,6 @@ type PressProps = {
42 type PointerType = '' | 'mouse' | 'keyboard' | 'pen' | 'touch';
43
44 type PressState = {
43 - didDispatchEvent: boolean,
45 isActivePressed: boolean,
46 isActivePressStart: boolean,
47 isAnchorTouched: boolean,
@@ -140,7 +141,6 @@ function dispatchEvent(
141 discrete: true,
142 },
143 );
143 - state.didDispatchEvent = true;
144 }
145
146 function dispatchPressChangeEvent(
@@ -200,6 +200,8 @@ function dispatchPressStartEvents(
200 props: PressProps,
201 state: PressState,
202 ): void {
203 + const shouldStopPropagation =
204 + props.stopPropagation === undefined ? true : props.stopPropagation;
205 state.isPressed = true;
206
207 if (state.pressEndTimeout !== null) {
@@ -229,14 +231,7 @@ function dispatchPressStartEvents(
231 if (props.onLongPressChange) {
232 dispatchLongPressChangeEvent(context, props, state);
233 }
232 - if (state.didDispatchEvent) {
233 - const shouldStopPropagation =
234 - props.stopPropagation === undefined ? true : props.stopPropagation;
235 - if (shouldStopPropagation) {
236 - context.dispatchStopPropagation();
237 - }
238 - state.didDispatchEvent = false;
239 - }
234 + return shouldStopPropagation;
235 }, delayLongPress);
236 }
237 };
@@ -251,6 +246,7 @@ function dispatchPressStartEvents(
246 state.pressStartTimeout = context.setTimeout(() => {
247 state.pressStartTimeout = null;
248 dispatch();
249 + return shouldStopPropagation;
250 }, delayPressStart);
251 } else {
252 dispatch();
@@ -263,6 +259,8 @@ function dispatchPressEndEvents(
259 props: PressProps,
260 state: PressState,
261 ): void {
262 + const shouldStopPropagation =
263 + props.stopPropagation === undefined ? true : props.stopPropagation;
264 const wasActivePressStart = state.isActivePressStart;
265 let activationWasForced = false;
266
@@ -298,6 +296,7 @@ function dispatchPressEndEvents(
296 state.pressEndTimeout = context.setTimeout(() => {
297 state.pressEndTimeout = null;
298 deactivate(context, props, state);
299 + return shouldStopPropagation;
300 }, delayPressEnd);
301 } else {
302 deactivate(context, props, state);
@@ -425,9 +424,16 @@ const PressResponder = {
424 context: ReactResponderContext,
425 props: PressProps,
426 state: PressState,
428 - ): void {
429 - const {target, type} = event;
427 + ): boolean {
428 + const {phase, target, type} = event;
429 +
430 + // Press doesn't handle capture target events at this point
431 + if (phase === CAPTURE_PHASE) {
432 + return false;
433 + }
434 const nativeEvent: any = event.nativeEvent;
435 + const shouldStopPropagation =
436 + props.stopPropagation === undefined ? true : props.stopPropagation;
437
438 switch (type) {
439 /**
@@ -435,19 +441,10 @@ const PressResponder = {
441 */
442 case 'pointerdown':
443 case 'mousedown': {
438 - if (
439 - !state.isPressed &&
440 - !context.hasOwnership() &&
441 - !state.shouldSkipMouseAfterTouch
442 - ) {
444 + if (!state.isPressed && !state.shouldSkipMouseAfterTouch) {
445 const pointerType = getPointerType(nativeEvent);
446 state.pointerType = pointerType;
447
446 - // Ignore any device buttons except left-mouse and touch/pen contact
447 - if (nativeEvent.button > 0) {
448 - return;
449 - }
450 -
448 // Ignore pressing on hit slop area with mouse
449 if (
450 (pointerType === 'mouse' || type === 'mousedown') &&
@@ -457,22 +454,28 @@ const PressResponder = {
454 nativeEvent.y,
455 )
456 ) {
460 - return;
457 + return false;
458 + }
459 +
460 + // Ignore any device buttons except left-mouse and touch/pen contact
461 + if (nativeEvent.button > 0) {
462 + return shouldStopPropagation;
463 }
464
465 state.pressTarget = target;
466 state.isPressWithinResponderRegion = true;
467 dispatchPressStartEvents(context, props, state);
468 context.addRootEventTypes(target.ownerDocument, rootEventTypes);
469 + return shouldStopPropagation;
470 }
468 - break;
471 + return false;
472 }
473 case 'pointermove':
474 case 'mousemove':
475 case 'touchmove': {
476 if (state.isPressed) {
477 if (state.shouldSkipMouseAfterTouch) {
475 - return;
478 + return shouldStopPropagation;
479 }
480
481 const pointerType = getPointerType(nativeEvent);
@@ -503,15 +506,16 @@ const PressResponder = {
506 state.isPressWithinResponderRegion = false;
507 dispatchPressEndEvents(context, props, state);
508 }
509 + return shouldStopPropagation;
510 }
507 - break;
511 + return false;
512 }
513 case 'pointerup':
514 case 'mouseup': {
515 if (state.isPressed) {
516 if (state.shouldSkipMouseAfterTouch) {
517 state.shouldSkipMouseAfterTouch = false;
514 - return;
518 + return shouldStopPropagation;
519 }
520
521 const pointerType = getPointerType(nativeEvent);
@@ -535,10 +539,11 @@ const PressResponder = {
539 }
540 }
541 context.removeRootEventTypes(rootEventTypes);
542 + return shouldStopPropagation;
543 }
544 state.isAnchorTouched = false;
545 state.shouldSkipMouseAfterTouch = false;
541 - break;
546 + return false;
547 }
548
549 /**
@@ -546,12 +551,12 @@ const PressResponder = {
551 * support for pointer events.
552 */
553 case 'touchstart': {
549 - if (!state.isPressed && !context.hasOwnership()) {
554 + if (!state.isPressed) {
555 // We bail out of polyfilling anchor tags, given the same heuristics
556 // explained above in regards to needing to use click events.
557 if (isAnchorTagElement(target)) {
558 state.isAnchorTouched = true;
554 - return;
559 + return shouldStopPropagation;
560 }
561 const pointerType = getPointerType(nativeEvent);
562 state.pointerType = pointerType;
@@ -559,13 +564,14 @@ const PressResponder = {
564 state.isPressWithinResponderRegion = true;
565 dispatchPressStartEvents(context, props, state);
566 context.addRootEventTypes(target.ownerDocument, rootEventTypes);
567 + return shouldStopPropagation;
568 }
563 - break;
569 + return false;
570 }
571 case 'touchend': {
572 if (state.isAnchorTouched) {
573 state.isAnchorTouched = false;
568 - return;
574 + return shouldStopPropagation;
575 }
576 if (state.isPressed) {
577 const pointerType = getPointerType(nativeEvent);
@@ -600,8 +606,9 @@ const PressResponder = {
606 }
607 state.shouldSkipMouseAfterTouch = true;
608 context.removeRootEventTypes(rootEventTypes);
609 + return shouldStopPropagation;
610 }
604 - break;
611 + return false;
612 }
613
614 /**
@@ -610,7 +617,7 @@ const PressResponder = {
617 */
618 case 'keydown':
619 case 'keypress': {
613 - if (!context.hasOwnership() && isValidKeyPress(nativeEvent.key)) {
620 + if (isValidKeyPress(nativeEvent.key)) {
621 if (state.isPressed) {
622 // Prevent spacebar press from scrolling the window
623 if (nativeEvent.key === ' ') {
@@ -623,8 +630,9 @@ const PressResponder = {
630 dispatchPressStartEvents(context, props, state);
631 context.addRootEventTypes(target.ownerDocument, rootEventTypes);
632 }
633 + return shouldStopPropagation;
634 }
627 - break;
635 + return false;
636 }
637 case 'keyup': {
638 if (state.isPressed && isValidKeyPress(nativeEvent.key)) {
@@ -642,8 +650,9 @@ const PressResponder = {
650 }
651 }
652 context.removeRootEventTypes(rootEventTypes);
653 + return shouldStopPropagation;
654 }
646 - break;
655 + return false;
656 }
657
658 case 'pointercancel':
@@ -653,8 +662,9 @@ const PressResponder = {
662 state.shouldSkipMouseAfterTouch = false;
663 dispatchPressEndEvents(context, props, state);
664 context.removeRootEventTypes(rootEventTypes);
665 + return shouldStopPropagation;
666 }
657 - break;
667 + return false;
668 }
669
670 case 'click': {
@@ -665,8 +675,9 @@ const PressResponder = {
675 if (preventDefault !== false && !shiftKey && !metaKey && !ctrlKey) {
676 nativeEvent.preventDefault();
677 }
678 + return shouldStopPropagation;
679 }
669 - break;
680 + return false;
681 }
682
683 case 'contextmenu': {
@@ -678,19 +689,12 @@ const PressResponder = {
689 dispatchPressEndEvents(context, props, state);
690 context.removeRootEventTypes(rootEventTypes);
691 }
692 + return shouldStopPropagation;
693 }
682 - break;
683 - }
684 - }
685 -
686 - if (state.didDispatchEvent) {
687 - const shouldStopPropagation =
688 - props.stopPropagation === undefined ? true : props.stopPropagation;
689 - if (shouldStopPropagation) {
690 - context.dispatchStopPropagation();
694 + return false;
695 }
692 - state.didDispatchEvent = false;
696 }
697 + return false;
698 },
699 onUnmount(
700 context: ReactResponderContext,
packages/react-events/src/Swipe.js
+12 -6
@@ -13,6 +13,7 @@ import type {
13 } from 'shared/ReactTypes';
14 import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
15
16 +const CAPTURE_PHASE = 2;
17 const targetEventTypes = ['pointerdown', 'pointercancel'];
18 const rootEventTypes = ['pointerup', {name: 'pointermove', passive: false}];
19
@@ -96,14 +97,18 @@ const SwipeResponder = {
97 context: ReactResponderContext,
98 props: Object,
99 state: SwipeState,
99 - ): void {
100 - const {target, type, nativeEvent} = event;
100 + ): boolean {
101 + const {target, phase, type, nativeEvent} = event;
102
103 + // Swipe doesn't handle capture target events at this point
104 + if (phase === CAPTURE_PHASE) {
105 + return false;
106 + }
107 switch (type) {
108 case 'touchstart':
109 case 'mousedown':
110 case 'pointerdown': {
106 - if (!state.isSwiping && !context.hasOwnership()) {
111 + if (!state.isSwiping) {
112 let obj = nativeEvent;
113 if (type === 'touchstart') {
114 obj = (nativeEvent: any).targetTouches[0];
@@ -135,7 +140,7 @@ const SwipeResponder = {
140 case 'mousemove':
141 case 'pointermove': {
142 if (event.passive) {
138 - return;
143 + return false;
144 }
145 if (state.isSwiping) {
146 let obj = null;
@@ -155,7 +160,7 @@ const SwipeResponder = {
160 state.swipeTarget = null;
161 state.touchId = null;
162 context.removeRootEventTypes(rootEventTypes);
158 - return;
163 + return false;
164 }
165 const x = (obj: any).screenX;
166 const y = (obj: any).screenY;
@@ -191,7 +196,7 @@ const SwipeResponder = {
196 case 'pointerup': {
197 if (state.isSwiping) {
198 if (state.x === state.startX && state.y === state.startY) {
194 - return;
199 + return false;
200 }
201 if (props.onShouldClaimOwnership) {
202 context.releaseOwnership();
@@ -235,6 +240,7 @@ const SwipeResponder = {
240 break;
241 }
242 }
243 + return false;
244 },
245 };
246
packages/react-events/src/__tests__/Press-test.internal.js
+152 -94
@@ -429,38 +429,6 @@ describe('Event responder: Press', () => {
429 expect(onPressChange).toHaveBeenCalledTimes(2);
430 expect(onPressChange).toHaveBeenCalledWith(false);
431 });
432 -
433 - it('is called but does not bubble', () => {
434 - const element = (
435 - <Press onPressChange={onPressChange}>
436 - <Press onPressChange={onPressChange}>
437 - <div ref={ref} />
438 - </Press>
439 - </Press>
440 - );
441 - ReactDOM.render(element, container);
442 -
443 - ref.current.dispatchEvent(createPointerEvent('pointerdown'));
444 - expect(onPressChange).toHaveBeenCalledTimes(1);
445 - ref.current.dispatchEvent(createPointerEvent('pointerup'));
446 - expect(onPressChange).toHaveBeenCalledTimes(2);
447 - });
448 -
449 - it('is called and bubbles correctly with stopPropagation set to false', () => {
450 - const element = (
451 - <Press onPressChange={onPressChange}>
452 - <Press onPressChange={onPressChange} stopPropagation={false}>
453 - <div ref={ref} />
454 - </Press>
455 - </Press>
456 - );
457 - ReactDOM.render(element, container);
458 -
459 - ref.current.dispatchEvent(createPointerEvent('pointerdown'));
460 - expect(onPressChange).toHaveBeenCalledTimes(2);
461 - ref.current.dispatchEvent(createPointerEvent('pointerup'));
462 - expect(onPressChange).toHaveBeenCalledTimes(4);
463 - });
432 });
433
434 describe('onPress', () => {
@@ -517,36 +485,6 @@ describe('Event responder: Press', () => {
485 // ref.current.dispatchEvent(createPointerEvent('touchend'));
486 // expect(onPress).toHaveBeenCalledTimes(1);
487 // });
520 -
521 - it('is called but does not bubble', () => {
522 - const element = (
523 - <Press onPress={onPress}>
524 - <Press onPress={onPress}>
525 - <div ref={ref} />
526 - </Press>
527 - </Press>
528 - );
529 - ReactDOM.render(element, container);
530 -
531 - ref.current.dispatchEvent(createPointerEvent('pointerdown'));
532 - ref.current.dispatchEvent(createPointerEvent('pointerup'));
533 - expect(onPress).toHaveBeenCalledTimes(1);
534 - });
535 -
536 - it('is called and bubbles correctly with stopPropagation set to false', () => {
537 - const element = (
538 - <Press onPress={onPress}>
539 - <Press onPress={onPress} stopPropagation={false}>
540 - <div ref={ref} />
541 - </Press>
542 - </Press>
543 - );
544 - ReactDOM.render(element, container);
545 -
546 - ref.current.dispatchEvent(createPointerEvent('pointerdown'));
547 - ref.current.dispatchEvent(createPointerEvent('pointerup'));
548 - expect(onPress).toHaveBeenCalledTimes(2);
549 - });
488 });
489
490 describe('onLongPress', () => {
@@ -603,38 +541,6 @@ describe('Event responder: Press', () => {
541 expect(onLongPress).not.toBeCalled();
542 });
543
606 - it('is called but does not bubble', () => {
607 - const element = (
608 - <Press onLongPress={onLongPress}>
609 - <Press onLongPress={onLongPress}>
610 - <div ref={ref} />
611 - </Press>
612 - </Press>
613 - );
614 - ReactDOM.render(element, container);
615 -
616 - ref.current.dispatchEvent(createPointerEvent('pointerdown'));
617 - jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY);
618 - ref.current.dispatchEvent(createPointerEvent('pointerup'));
619 - expect(onLongPress).toHaveBeenCalledTimes(1);
620 - });
621 -
622 - it('is called and bubbles correctly with stopPropagation set to false', () => {
623 - const element = (
624 - <Press onLongPress={onLongPress}>
625 - <Press onLongPress={onLongPress} stopPropagation={false}>
626 - <div ref={ref} />
627 - </Press>
628 - </Press>
629 - );
630 - ReactDOM.render(element, container);
631 -
632 - ref.current.dispatchEvent(createPointerEvent('pointerdown'));
633 - jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY);
634 - ref.current.dispatchEvent(createPointerEvent('pointerup'));
635 - expect(onLongPress).toHaveBeenCalledTimes(2);
636 - });
637 -
544 describe('delayLongPress', () => {
545 it('can be configured', () => {
546 const element = (
@@ -1136,6 +1042,158 @@ describe('Event responder: Press', () => {
1042 'outer: onPress',
1043 ]);
1044 });
1045 +
1046 + describe('correctly get propagation stopped and do not bubble', () => {
1047 + it('for onPress', () => {
1048 + const ref = React.createRef();
1049 + const fn = jest.fn();
1050 + const element = (
1051 + <Press onPress={fn}>
1052 + <Press onPress={fn}>
1053 + <div ref={ref} />
1054 + </Press>
1055 + </Press>
1056 + );
1057 + ReactDOM.render(element, container);
1058 +
1059 + ref.current.dispatchEvent(createPointerEvent('pointerdown'));
1060 + ref.current.dispatchEvent(createPointerEvent('pointerup'));
1061 + expect(fn).toHaveBeenCalledTimes(1);
1062 + });
1063 +
1064 + it('for onLongPress', () => {
1065 + const ref = React.createRef();
1066 + const fn = jest.fn();
1067 + const element = (
1068 + <Press onLongPress={fn}>
1069 + <Press onLongPress={fn}>
1070 + <div ref={ref} />
1071 + </Press>
1072 + </Press>
1073 + );
1074 + ReactDOM.render(element, container);
1075 +
1076 + ref.current.dispatchEvent(createPointerEvent('pointerdown'));
1077 + jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY);
1078 + ref.current.dispatchEvent(createPointerEvent('pointerup'));
1079 + expect(fn).toHaveBeenCalledTimes(1);
1080 + });
1081 +
1082 + it('for onPressStart/onPressEnd', () => {
1083 + const ref = React.createRef();
1084 + const fn = jest.fn();
1085 + const fn2 = jest.fn();
1086 + const element = (
1087 + <Press onPressStart={fn} onPressEnd={fn2}>
1088 + <Press onPressStart={fn} onPressEnd={fn2}>
1089 + <div ref={ref} />
1090 + </Press>
1091 + </Press>
1092 + );
1093 + ReactDOM.render(element, container);
1094 +
1095 + ref.current.dispatchEvent(createPointerEvent('pointerdown'));
1096 + expect(fn).toHaveBeenCalledTimes(1);
1097 + expect(fn2).toHaveBeenCalledTimes(0);
1098 + ref.current.dispatchEvent(createPointerEvent('pointerup'));
1099 + expect(fn).toHaveBeenCalledTimes(1);
1100 + expect(fn2).toHaveBeenCalledTimes(1);
1101 + });
1102 +
1103 + it('for onPressChange', () => {
1104 + const ref = React.createRef();
1105 + const fn = jest.fn();
1106 + const element = (
1107 + <Press onPressChange={fn}>
1108 + <Press onPressChange={fn}>
1109 + <div ref={ref} />
1110 + </Press>
1111 + </Press>
1112 + );
1113 + ReactDOM.render(element, container);
1114 +
1115 + ref.current.dispatchEvent(createPointerEvent('pointerdown'));
1116 + expect(fn).toHaveBeenCalledTimes(1);
1117 + ref.current.dispatchEvent(createPointerEvent('pointerup'));
1118 + expect(fn).toHaveBeenCalledTimes(2);
1119 + });
1120 + });
1121 +
1122 + describe('correctly bubble to other event responders when stopPropagation is set to false', () => {
1123 + it('for onPress', () => {
1124 + const ref = React.createRef();
1125 + const fn = jest.fn();
1126 + const element = (
1127 + <Press onPress={fn}>
1128 + <Press onPress={fn} stopPropagation={false}>
1129 + <div ref={ref} />
1130 + </Press>
1131 + </Press>
1132 + );
1133 + ReactDOM.render(element, container);
1134 +
1135 + ref.current.dispatchEvent(createPointerEvent('pointerdown'));
1136 + ref.current.dispatchEvent(createPointerEvent('pointerup'));
1137 + expect(fn).toHaveBeenCalledTimes(2);
1138 + });
1139 +
1140 + it('for onLongPress', () => {
1141 + const ref = React.createRef();
1142 + const fn = jest.fn();
1143 + const element = (
1144 + <Press onLongPress={fn}>
1145 + <Press onLongPress={fn} stopPropagation={false}>
1146 + <div ref={ref} />
1147 + </Press>
1148 + </Press>
1149 + );
1150 + ReactDOM.render(element, container);
1151 +
1152 + ref.current.dispatchEvent(createPointerEvent('pointerdown'));
1153 + jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY);
1154 + ref.current.dispatchEvent(createPointerEvent('pointerup'));
1155 + expect(fn).toHaveBeenCalledTimes(2);
1156 + });
1157 +
1158 + it('for onPressStart/onPressEnd', () => {
1159 + const ref = React.createRef();
1160 + const fn = jest.fn();
1161 + const fn2 = jest.fn();
1162 + const element = (
1163 + <Press onPressStart={fn} onPressEnd={fn2}>
1164 + <Press onPressStart={fn} onPressEnd={fn2} stopPropagation={false}>
1165 + <div ref={ref} />
1166 + </Press>
1167 + </Press>
1168 + );
1169 + ReactDOM.render(element, container);
1170 +
1171 + ref.current.dispatchEvent(createPointerEvent('pointerdown'));
1172 + expect(fn).toHaveBeenCalledTimes(2);
1173 + expect(fn2).toHaveBeenCalledTimes(0);
1174 + ref.current.dispatchEvent(createPointerEvent('pointerup'));
1175 + expect(fn).toHaveBeenCalledTimes(2);
1176 + expect(fn2).toHaveBeenCalledTimes(2);
1177 + });
1178 +
1179 + it('for onPressChange', () => {
1180 + const ref = React.createRef();
1181 + const fn = jest.fn();
1182 + const element = (
1183 + <Press onPressChange={fn}>
1184 + <Press onPressChange={fn} stopPropagation={false}>
1185 + <div ref={ref} />
1186 + </Press>
1187 + </Press>
1188 + );
1189 + ReactDOM.render(element, container);
1190 +
1191 + ref.current.dispatchEvent(createPointerEvent('pointerdown'));
1192 + expect(fn).toHaveBeenCalledTimes(2);
1193 + ref.current.dispatchEvent(createPointerEvent('pointerup'));
1194 + expect(fn).toHaveBeenCalledTimes(4);
1195 + });
1196 + });
1197 });
1198
1199 describe('link components', () => {
packages/shared/ReactTypes.js
+3 -4
@@ -93,7 +93,7 @@ export type ReactEventResponder = {
93 context: ReactResponderContext,
94 props: null | Object,
95 state: null | Object,
96 - ) => void,
96 + ) => boolean,
97 onUnmount: (
98 context: ReactResponderContext,
99 props: null | Object,
@@ -135,10 +135,10 @@ export type ReactResponderEvent = {
135 type: string,
136 passive: boolean,
137 passiveSupported: boolean,
138 + phase: 0 | 1 | 2,
139 };
140
141 export type ReactResponderDispatchEventOptions = {
141 - capture?: boolean,
142 discrete?: boolean,
143 };
144
@@ -148,7 +148,6 @@ export type ReactResponderContext = {
148 listener: (Object) => void,
149 otpions: ReactResponderDispatchEventOptions,
150 ) => void,
151 - dispatchStopPropagation: (passive?: boolean) => void,
151 isTargetWithinElement: (
152 childTarget: Element | Document,
153 parentTarget: Element | Document,
@@ -169,7 +168,7 @@ export type ReactResponderContext = {
168 hasOwnership: () => boolean,
169 requestOwnership: () => boolean,
170 releaseOwnership: () => boolean,
172 - setTimeout: (func: () => void, timeout: number) => Symbol,
171 + setTimeout: (func: () => boolean, timeout: number) => Symbol,
172 clearTimeout: (timerId: Symbol) => void,
173 getEventTargetsFromTarget: (
174 target: Element | Document,