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,