Skip to content

Commit 9672cf6

Browse files
authored
Experimental Event API: adds stopPropagation by default to Press (#15384)
1 parent a9eff32 commit 9672cf6

File tree

5 files changed

+249
-70
lines changed

5 files changed

+249
-70
lines changed

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

Lines changed: 117 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ export function setListenToResponderEventTypes(
4141
listenToResponderEventTypesImpl = _listenToResponderEventTypesImpl;
4242
}
4343

44+
type EventObjectTypes = {|stopPropagation: true|} | $Shape<PartialEventObject>;
45+
4446
type EventQueue = {
45-
bubble: null | Array<$Shape<PartialEventObject>>,
46-
capture: null | Array<$Shape<PartialEventObject>>,
47+
bubble: null | Array<EventObjectTypes>,
48+
capture: null | Array<EventObjectTypes>,
4749
discrete: boolean,
4850
};
4951

@@ -53,16 +55,38 @@ type PartialEventObject = {
5355
type: string,
5456
};
5557

58+
type ResponderTimeout = {|
59+
id: TimeoutID,
60+
timers: Map<Symbol, ResponderTimer>,
61+
|};
62+
63+
type ResponderTimer = {|
64+
instance: ReactEventComponentInstance,
65+
func: () => void,
66+
id: Symbol,
67+
|};
68+
69+
const activeTimeouts: Map<Symbol, ResponderTimeout> = new Map();
70+
const rootEventTypesToEventComponentInstances: Map<
71+
DOMTopLevelEventType | string,
72+
Set<ReactEventComponentInstance>,
73+
> = new Map();
74+
const targetEventTypeCached: Map<
75+
Array<ReactEventResponderEventType>,
76+
Set<DOMTopLevelEventType>,
77+
> = new Map();
78+
const ownershipChangeListeners: Set<ReactEventComponentInstance> = new Set();
79+
80+
let currentTimers = new Map();
5681
let currentOwner = null;
5782
let currentInstance: ReactEventComponentInstance;
5883
let currentEventQueue: EventQueue;
5984

6085
const eventResponderContext: ReactResponderContext = {
6186
dispatchEvent(
6287
possibleEventObject: Object,
63-
{capture, discrete, stopPropagation}: ReactResponderDispatchEventOptions,
88+
{capture, discrete}: ReactResponderDispatchEventOptions,
6489
): void {
65-
const eventQueue = currentEventQueue;
6690
const {listener, target, type} = possibleEventObject;
6791

6892
if (listener == null || target == null || type == null) {
@@ -89,27 +113,15 @@ const eventResponderContext: ReactResponderContext = {
89113
const eventObject = ((possibleEventObject: any): $Shape<
90114
PartialEventObject,
91115
>);
92-
let events;
93-
94-
if (capture) {
95-
events = eventQueue.capture;
96-
if (events === null) {
97-
events = eventQueue.capture = [];
98-
}
99-
} else {
100-
events = eventQueue.bubble;
101-
if (events === null) {
102-
events = eventQueue.bubble = [];
103-
}
104-
}
116+
const events = getEventsFromEventQueue(capture);
105117
if (discrete) {
106-
eventQueue.discrete = true;
118+
currentEventQueue.discrete = true;
107119
}
108120
events.push(eventObject);
109-
110-
if (stopPropagation) {
111-
eventsWithStopPropagation.add(eventObject);
112-
}
121+
},
122+
dispatchStopPropagation(capture?: boolean) {
123+
const events = getEventsFromEventQueue();
124+
events.push({stopPropagation: true});
113125
},
114126
isPositionWithinTouchHitTarget(doc: Document, x: number, y: number): boolean {
115127
// This isn't available in some environments (JSDOM)
@@ -222,21 +234,42 @@ const eventResponderContext: ReactResponderContext = {
222234
triggerOwnershipListeners();
223235
return false;
224236
},
225-
setTimeout(func: () => void, delay): TimeoutID {
226-
const contextInstance = currentInstance;
227-
return setTimeout(() => {
228-
const previousEventQueue = currentEventQueue;
229-
const previousInstance = currentInstance;
230-
currentEventQueue = createEventQueue();
231-
currentInstance = contextInstance;
232-
try {
233-
func();
234-
batchedUpdates(processEventQueue, currentEventQueue);
235-
} finally {
236-
currentInstance = previousInstance;
237-
currentEventQueue = previousEventQueue;
237+
setTimeout(func: () => void, delay): Symbol {
238+
if (currentTimers === null) {
239+
currentTimers = new Map();
240+
}
241+
let timeout = currentTimers.get(delay);
242+
243+
const timerId = Symbol();
244+
if (timeout === undefined) {
245+
const timers = new Map();
246+
const id = setTimeout(() => {
247+
processTimers(timers);
248+
}, delay);
249+
timeout = {
250+
id,
251+
timers,
252+
};
253+
currentTimers.set(delay, timeout);
254+
}
255+
timeout.timers.set(timerId, {
256+
instance: currentInstance,
257+
func,
258+
id: timerId,
259+
});
260+
activeTimeouts.set(timerId, timeout);
261+
return timerId;
262+
},
263+
clearTimeout(timerId: Symbol): void {
264+
const timeout = activeTimeouts.get(timerId);
265+
266+
if (timeout !== undefined) {
267+
const timers = timeout.timers;
268+
timers.delete(timerId);
269+
if (timers.size === 0) {
270+
clearTimeout(timeout.id);
238271
}
239-
}, delay);
272+
}
240273
},
241274
getEventTargetsFromTarget(
242275
target: Element | Document,
@@ -292,6 +325,46 @@ const eventResponderContext: ReactResponderContext = {
292325
},
293326
};
294327

328+
function getEventsFromEventQueue(capture?: boolean): Array<EventObjectTypes> {
329+
let events;
330+
if (capture) {
331+
events = currentEventQueue.capture;
332+
if (events === null) {
333+
events = currentEventQueue.capture = [];
334+
}
335+
} else {
336+
events = currentEventQueue.bubble;
337+
if (events === null) {
338+
events = currentEventQueue.bubble = [];
339+
}
340+
}
341+
return events;
342+
}
343+
344+
function processTimers(timers: Map<Symbol, ResponderTimer>): void {
345+
const previousEventQueue = currentEventQueue;
346+
const previousInstance = currentInstance;
347+
currentEventQueue = createEventQueue();
348+
349+
try {
350+
const timersArr = Array.from(timers.values());
351+
for (let i = 0; i < timersArr.length; i++) {
352+
const {instance, func, id} = timersArr[i];
353+
currentInstance = instance;
354+
try {
355+
func();
356+
} finally {
357+
activeTimeouts.delete(id);
358+
}
359+
}
360+
batchedUpdates(processEventQueue, currentEventQueue);
361+
} finally {
362+
currentInstance = previousInstance;
363+
currentEventQueue = previousEventQueue;
364+
currentTimers = null;
365+
}
366+
}
367+
295368
function queryEventTarget(
296369
child: Fiber,
297370
queryType: void | Symbol | number,
@@ -306,20 +379,6 @@ function queryEventTarget(
306379
return true;
307380
}
308381

309-
const rootEventTypesToEventComponentInstances: Map<
310-
DOMTopLevelEventType | string,
311-
Set<ReactEventComponentInstance>,
312-
> = new Map();
313-
const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
314-
const eventsWithStopPropagation:
315-
| WeakSet
316-
| Set<$Shape<PartialEventObject>> = new PossiblyWeakSet();
317-
const targetEventTypeCached: Map<
318-
Array<ReactEventResponderEventType>,
319-
Set<DOMTopLevelEventType>,
320-
> = new Map();
321-
const ownershipChangeListeners: Set<ReactEventComponentInstance> = new Set();
322-
323382
function createResponderEvent(
324383
topLevelType: string,
325384
nativeEvent: AnyNativeEvent,
@@ -350,27 +409,27 @@ function processEvent(event: $Shape<PartialEventObject>): void {
350409
}
351410

352411
function processEvents(
353-
bubble: null | Array<$Shape<PartialEventObject>>,
354-
capture: null | Array<$Shape<PartialEventObject>>,
412+
bubble: null | Array<EventObjectTypes>,
413+
capture: null | Array<EventObjectTypes>,
355414
): void {
356415
let i, length;
357416

358417
if (capture !== null) {
359418
for (i = capture.length; i-- > 0; ) {
360419
const event = capture[i];
361-
processEvent(capture[i]);
362-
if (eventsWithStopPropagation.has(event)) {
420+
if (event.stopPropagation === true) {
363421
return;
364422
}
423+
processEvent(((event: any): $Shape<PartialEventObject>));
365424
}
366425
}
367426
if (bubble !== null) {
368427
for (i = 0, length = bubble.length; i < length; ++i) {
369428
const event = bubble[i];
370-
processEvent(event);
371-
if (eventsWithStopPropagation.has(event)) {
429+
if (event.stopPropagation === true) {
372430
return;
373431
}
432+
processEvent(((event: any): $Shape<PartialEventObject>));
374433
}
375434
}
376435
}
@@ -475,6 +534,7 @@ export function runResponderEventsInBatch(
475534
}
476535
}
477536
processEventQueue();
537+
currentTimers = null;
478538
}
479539
}
480540

@@ -518,6 +578,7 @@ export function unmountEventResponder(
518578
} finally {
519579
currentEventQueue = previousEventQueue;
520580
currentInstance = previousInstance;
581+
currentTimers = null;
521582
}
522583
}
523584
if (currentOwner === eventComponentInstance) {

packages/react-events/src/Hover.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ type HoverState = {
2727
isHovered: boolean,
2828
isInHitSlop: boolean,
2929
isTouched: boolean,
30-
hoverStartTimeout: null | TimeoutID,
31-
hoverEndTimeout: null | TimeoutID,
30+
hoverStartTimeout: null | Symbol,
31+
hoverEndTimeout: null | Symbol,
3232
};
3333

3434
type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange';
@@ -97,7 +97,7 @@ function dispatchHoverStartEvents(
9797
state.isHovered = true;
9898

9999
if (state.hoverEndTimeout !== null) {
100-
clearTimeout(state.hoverEndTimeout);
100+
context.clearTimeout(state.hoverEndTimeout);
101101
state.hoverEndTimeout = null;
102102
}
103103

@@ -148,7 +148,7 @@ function dispatchHoverEndEvents(
148148
state.isHovered = false;
149149

150150
if (state.hoverStartTimeout !== null) {
151-
clearTimeout(state.hoverStartTimeout);
151+
context.clearTimeout(state.hoverStartTimeout);
152152
state.hoverStartTimeout = null;
153153
}
154154

packages/react-events/src/Press.js

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,21 @@ type PressProps = {
3333
left: number,
3434
},
3535
preventDefault: boolean,
36+
stopPropagation: boolean,
3637
};
3738

3839
type PressState = {
40+
didDispatchEvent: boolean,
3941
isActivePressed: boolean,
4042
isActivePressStart: boolean,
4143
isAnchorTouched: boolean,
4244
isLongPressed: boolean,
4345
isPressed: boolean,
4446
isPressWithinResponderRegion: boolean,
45-
longPressTimeout: null | TimeoutID,
47+
longPressTimeout: null | Symbol,
4648
pressTarget: null | Element | Document,
47-
pressEndTimeout: null | TimeoutID,
48-
pressStartTimeout: null | TimeoutID,
49+
pressEndTimeout: null | Symbol,
50+
pressStartTimeout: null | Symbol,
4951
responderRegion: null | $ReadOnly<{|
5052
bottom: number,
5153
left: number,
@@ -124,7 +126,10 @@ function dispatchEvent(
124126
): void {
125127
const target = ((state.pressTarget: any): Element | Document);
126128
const syntheticEvent = createPressEvent(name, target, listener);
127-
context.dispatchEvent(syntheticEvent, {discrete: true});
129+
context.dispatchEvent(syntheticEvent, {
130+
discrete: true,
131+
});
132+
state.didDispatchEvent = true;
128133
}
129134

130135
function dispatchPressChangeEvent(
@@ -185,7 +190,7 @@ function dispatchPressStartEvents(
185190
state.isPressed = true;
186191

187192
if (state.pressEndTimeout !== null) {
188-
clearTimeout(state.pressEndTimeout);
193+
context.clearTimeout(state.pressEndTimeout);
189194
state.pressEndTimeout = null;
190195
}
191196

@@ -211,6 +216,14 @@ function dispatchPressStartEvents(
211216
if (props.onLongPressChange) {
212217
dispatchLongPressChangeEvent(context, props, state);
213218
}
219+
if (state.didDispatchEvent) {
220+
const shouldStopPropagation =
221+
props.stopPropagation === undefined ? true : props.stopPropagation;
222+
if (shouldStopPropagation) {
223+
context.dispatchStopPropagation();
224+
}
225+
state.didDispatchEvent = false;
226+
}
214227
}, delayLongPress);
215228
}
216229
};
@@ -243,12 +256,12 @@ function dispatchPressEndEvents(
243256
state.isPressed = false;
244257

245258
if (state.longPressTimeout !== null) {
246-
clearTimeout(state.longPressTimeout);
259+
context.clearTimeout(state.longPressTimeout);
247260
state.longPressTimeout = null;
248261
}
249262

250263
if (!wasActivePressStart && state.pressStartTimeout !== null) {
251-
clearTimeout(state.pressStartTimeout);
264+
context.clearTimeout(state.pressStartTimeout);
252265
state.pressStartTimeout = null;
253266
// don't activate if a press has moved beyond the responder region
254267
if (state.isPressWithinResponderRegion) {
@@ -356,6 +369,7 @@ const PressResponder = {
356369
targetEventTypes,
357370
createInitialState(): PressState {
358371
return {
372+
didDispatchEvent: false,
359373
isActivePressed: false,
360374
isActivePressStart: false,
361375
isAnchorTouched: false,
@@ -602,6 +616,14 @@ const PressResponder = {
602616
}
603617
}
604618
}
619+
if (state.didDispatchEvent) {
620+
const shouldStopPropagation =
621+
props.stopPropagation === undefined ? true : props.stopPropagation;
622+
if (shouldStopPropagation) {
623+
context.dispatchStopPropagation();
624+
}
625+
state.didDispatchEvent = false;
626+
}
605627
},
606628
onUnmount(
607629
context: ReactResponderContext,

0 commit comments

Comments
 (0)