Skip to content

Commit e783f0b

Browse files
authored
Add support for continuePropagation to usePress (#4683)
1 parent 101fd8f commit e783f0b

File tree

4 files changed

+202
-64
lines changed

4 files changed

+202
-64
lines changed

packages/@react-aria/interactions/src/useLongPress.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export function useLongPress(props: LongPressProps): LongPressResult {
6969
let {pressProps} = usePress({
7070
isDisabled,
7171
onPressStart(e) {
72+
e.continuePropagation();
7273
if (e.pointerType === 'mouse' || e.pointerType === 'touch') {
7374
if (onLongPressStart) {
7475
onLongPressStart({

packages/@react-aria/interactions/src/usePress.ts

Lines changed: 111 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
// See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions
1717

1818
import {disableTextSelection, restoreTextSelection} from './textSelection';
19-
import {DOMAttributes, FocusableElement, PointerType, PressEvents} from '@react-types/shared';
19+
import {DOMAttributes, FocusableElement, PressEvent as IPressEvent, PointerType, PressEvents} from '@react-types/shared';
2020
import {focusWithoutScrolling, isVirtualClick, isVirtualPointerEvent, mergeProps, useEffectEvent, useGlobalListeners, useSyncRef} from '@react-aria/utils';
2121
import {PressResponderContext} from './context';
2222
import {RefObject, useContext, useEffect, useMemo, useRef, useState} from 'react';
@@ -84,6 +84,35 @@ function usePressResponderContext(props: PressHookProps): PressHookProps {
8484
return props;
8585
}
8686

87+
class PressEvent implements IPressEvent {
88+
type: IPressEvent['type'];
89+
pointerType: PointerType;
90+
target: Element;
91+
shiftKey: boolean;
92+
ctrlKey: boolean;
93+
metaKey: boolean;
94+
altKey: boolean;
95+
#shouldStopPropagation = true;
96+
97+
constructor(type: IPressEvent['type'], pointerType: PointerType, originalEvent: EventBase) {
98+
this.type = type;
99+
this.pointerType = pointerType;
100+
this.target = originalEvent.currentTarget as Element;
101+
this.shiftKey = originalEvent.shiftKey;
102+
this.metaKey = originalEvent.metaKey;
103+
this.ctrlKey = originalEvent.ctrlKey;
104+
this.altKey = originalEvent.altKey;
105+
}
106+
107+
continuePropagation() {
108+
this.#shouldStopPropagation = false;
109+
}
110+
111+
get shouldStopPropagation() {
112+
return this.#shouldStopPropagation;
113+
}
114+
}
115+
87116
/**
88117
* Handles press interactions across mouse, touch, keyboard, and screen readers.
89118
* It normalizes behavior across browsers and platforms, and handles many nuances
@@ -126,16 +155,11 @@ export function usePress(props: PressHookProps): PressResult {
126155
return;
127156
}
128157

158+
let shouldStopPropagation = true;
129159
if (onPressStart) {
130-
onPressStart({
131-
type: 'pressstart',
132-
pointerType,
133-
target: originalEvent.currentTarget as Element,
134-
shiftKey: originalEvent.shiftKey,
135-
metaKey: originalEvent.metaKey,
136-
ctrlKey: originalEvent.ctrlKey,
137-
altKey: originalEvent.altKey
138-
});
160+
let event = new PressEvent('pressstart', pointerType, originalEvent);
161+
onPressStart(event);
162+
shouldStopPropagation = event.shouldStopPropagation;
139163
}
140164

141165
if (onPressChange) {
@@ -144,6 +168,7 @@ export function usePress(props: PressHookProps): PressResult {
144168

145169
state.didFirePressStart = true;
146170
setPressed(true);
171+
return shouldStopPropagation;
147172
});
148173

149174
let triggerPressEnd = useEffectEvent((originalEvent: EventBase, pointerType: PointerType, wasPressed = true) => {
@@ -155,16 +180,11 @@ export function usePress(props: PressHookProps): PressResult {
155180
state.ignoreClickAfterPress = true;
156181
state.didFirePressStart = false;
157182

183+
let shouldStopPropagation = true;
158184
if (onPressEnd) {
159-
onPressEnd({
160-
type: 'pressend',
161-
pointerType,
162-
target: originalEvent.currentTarget as Element,
163-
shiftKey: originalEvent.shiftKey,
164-
metaKey: originalEvent.metaKey,
165-
ctrlKey: originalEvent.ctrlKey,
166-
altKey: originalEvent.altKey
167-
});
185+
let event = new PressEvent('pressend', pointerType, originalEvent);
186+
onPressEnd(event);
187+
shouldStopPropagation = event.shouldStopPropagation;
168188
}
169189

170190
if (onPressChange) {
@@ -174,16 +194,12 @@ export function usePress(props: PressHookProps): PressResult {
174194
setPressed(false);
175195

176196
if (onPress && wasPressed && !isDisabled) {
177-
onPress({
178-
type: 'press',
179-
pointerType,
180-
target: originalEvent.currentTarget as Element,
181-
shiftKey: originalEvent.shiftKey,
182-
metaKey: originalEvent.metaKey,
183-
ctrlKey: originalEvent.ctrlKey,
184-
altKey: originalEvent.altKey
185-
});
197+
let event = new PressEvent('press', pointerType, originalEvent);
198+
onPress(event);
199+
shouldStopPropagation &&= event.shouldStopPropagation;
186200
}
201+
202+
return shouldStopPropagation;
187203
});
188204

189205
let triggerPressUp = useEffectEvent((originalEvent: EventBase, pointerType: PointerType) => {
@@ -192,16 +208,12 @@ export function usePress(props: PressHookProps): PressResult {
192208
}
193209

194210
if (onPressUp) {
195-
onPressUp({
196-
type: 'pressup',
197-
pointerType,
198-
target: originalEvent.currentTarget as Element,
199-
shiftKey: originalEvent.shiftKey,
200-
metaKey: originalEvent.metaKey,
201-
ctrlKey: originalEvent.ctrlKey,
202-
altKey: originalEvent.altKey
203-
});
211+
let event = new PressEvent('pressup', pointerType, originalEvent);
212+
onPressUp(event);
213+
return event.shouldStopPropagation;
204214
}
215+
216+
return true;
205217
});
206218

207219
let cancel = useEffectEvent((e: EventBase) => {
@@ -235,20 +247,24 @@ export function usePress(props: PressHookProps): PressResult {
235247
if (shouldPreventDefaultKeyboard(e.target as Element, e.key)) {
236248
e.preventDefault();
237249
}
238-
e.stopPropagation();
239250

240251
// If the event is repeating, it may have started on a different element
241252
// after which focus moved to the current element. Ignore these events and
242253
// only handle the first key down event.
254+
let shouldStopPropagation = true;
243255
if (!state.isPressed && !e.repeat) {
244256
state.target = e.currentTarget;
245257
state.isPressed = true;
246-
triggerPressStart(e, 'keyboard');
258+
shouldStopPropagation = triggerPressStart(e, 'keyboard');
247259

248260
// Focus may move before the key up event, so register the event on the document
249261
// instead of the same element where the key down event occurred.
250262
addGlobalListener(document, 'keyup', onKeyUp, false);
251263
}
264+
265+
if (shouldStopPropagation) {
266+
e.stopPropagation();
267+
}
252268
} else if (e.key === 'Enter' && isHTMLAnchorLink(e.currentTarget)) {
253269
// If the target is a link, we won't have handled this above because we want the default
254270
// browser behavior to open the link when pressing Enter. But we still need to prevent
@@ -267,7 +283,7 @@ export function usePress(props: PressHookProps): PressResult {
267283
}
268284

269285
if (e && e.button === 0) {
270-
e.stopPropagation();
286+
let shouldStopPropagation = true;
271287
if (isDisabled) {
272288
e.preventDefault();
273289
}
@@ -280,13 +296,17 @@ export function usePress(props: PressHookProps): PressResult {
280296
focusWithoutScrolling(e.currentTarget);
281297
}
282298

283-
triggerPressStart(e, 'virtual');
284-
triggerPressUp(e, 'virtual');
285-
triggerPressEnd(e, 'virtual');
299+
let stopPressStart = triggerPressStart(e, 'virtual');
300+
let stopPressUp = triggerPressUp(e, 'virtual');
301+
let stopPressEnd = triggerPressEnd(e, 'virtual');
302+
shouldStopPropagation = stopPressStart && stopPressUp && stopPressEnd;
286303
}
287304

288305
state.ignoreEmulatedMouseEvents = false;
289306
state.ignoreClickAfterPress = false;
307+
if (shouldStopPropagation) {
308+
e.stopPropagation();
309+
}
290310
}
291311
}
292312
};
@@ -296,13 +316,16 @@ export function usePress(props: PressHookProps): PressResult {
296316
if (shouldPreventDefaultKeyboard(e.target as Element, e.key)) {
297317
e.preventDefault();
298318
}
299-
e.stopPropagation();
300319

301320
state.isPressed = false;
302321
let target = e.target as Element;
303-
triggerPressEnd(createEvent(state.target, e), 'keyboard', state.target.contains(target));
322+
let shouldStopPropagation = triggerPressEnd(createEvent(state.target, e), 'keyboard', state.target.contains(target));
304323
removeAllGlobalListeners();
305324

325+
if (shouldStopPropagation) {
326+
e.stopPropagation();
327+
}
328+
306329
// If the target is a link, trigger the click method to open the URL,
307330
// but defer triggering pressEnd until onClick event handler.
308331
if (state.target instanceof HTMLElement && state.target.contains(target) && (isHTMLAnchorLink(state.target) || state.target.getAttribute('role') === 'link')) {
@@ -335,7 +358,7 @@ export function usePress(props: PressHookProps): PressResult {
335358

336359
state.pointerType = e.pointerType;
337360

338-
e.stopPropagation();
361+
let shouldStopPropagation = true;
339362
if (!state.isPressed) {
340363
state.isPressed = true;
341364
state.isOverTarget = true;
@@ -350,12 +373,16 @@ export function usePress(props: PressHookProps): PressResult {
350373
disableTextSelection(state.target);
351374
}
352375

353-
triggerPressStart(e, state.pointerType);
376+
shouldStopPropagation = triggerPressStart(e, state.pointerType);
354377

355378
addGlobalListener(document, 'pointermove', onPointerMove, false);
356379
addGlobalListener(document, 'pointerup', onPointerUp, false);
357380
addGlobalListener(document, 'pointercancel', onPointerCancel, false);
358381
}
382+
383+
if (shouldStopPropagation) {
384+
e.stopPropagation();
385+
}
359386
};
360387

361388
pressProps.onMouseDown = (e) => {
@@ -453,8 +480,8 @@ export function usePress(props: PressHookProps): PressResult {
453480
e.preventDefault();
454481
}
455482

456-
e.stopPropagation();
457483
if (state.ignoreEmulatedMouseEvents) {
484+
e.stopPropagation();
458485
return;
459486
}
460487

@@ -467,7 +494,10 @@ export function usePress(props: PressHookProps): PressResult {
467494
focusWithoutScrolling(e.currentTarget);
468495
}
469496

470-
triggerPressStart(e, state.pointerType);
497+
let shouldStopPropagation = triggerPressStart(e, state.pointerType);
498+
if (shouldStopPropagation) {
499+
e.stopPropagation();
500+
}
471501

472502
addGlobalListener(document, 'mouseup', onMouseUp, false);
473503
};
@@ -477,10 +507,14 @@ export function usePress(props: PressHookProps): PressResult {
477507
return;
478508
}
479509

480-
e.stopPropagation();
510+
let shouldStopPropagation = true;
481511
if (state.isPressed && !state.ignoreEmulatedMouseEvents) {
482512
state.isOverTarget = true;
483-
triggerPressStart(e, state.pointerType);
513+
shouldStopPropagation = triggerPressStart(e, state.pointerType);
514+
}
515+
516+
if (shouldStopPropagation) {
517+
e.stopPropagation();
484518
}
485519
};
486520

@@ -489,12 +523,16 @@ export function usePress(props: PressHookProps): PressResult {
489523
return;
490524
}
491525

492-
e.stopPropagation();
526+
let shouldStopPropagation = true;
493527
if (state.isPressed && !state.ignoreEmulatedMouseEvents) {
494528
state.isOverTarget = false;
495-
triggerPressEnd(e, state.pointerType, false);
529+
shouldStopPropagation = triggerPressEnd(e, state.pointerType, false);
496530
cancelOnPointerExit(e);
497531
}
532+
533+
if (shouldStopPropagation) {
534+
e.stopPropagation();
535+
}
498536
};
499537

500538
pressProps.onMouseUp = (e) => {
@@ -535,7 +573,6 @@ export function usePress(props: PressHookProps): PressResult {
535573
return;
536574
}
537575

538-
e.stopPropagation();
539576
let touch = getTouchFromEvent(e.nativeEvent);
540577
if (!touch) {
541578
return;
@@ -557,7 +594,10 @@ export function usePress(props: PressHookProps): PressResult {
557594
disableTextSelection(state.target);
558595
}
559596

560-
triggerPressStart(e, state.pointerType);
597+
let shouldStopPropagation = triggerPressStart(e, state.pointerType);
598+
if (shouldStopPropagation) {
599+
e.stopPropagation();
600+
}
561601

562602
addGlobalListener(window, 'scroll', onScroll, true);
563603
};
@@ -567,40 +607,50 @@ export function usePress(props: PressHookProps): PressResult {
567607
return;
568608
}
569609

570-
e.stopPropagation();
571610
if (!state.isPressed) {
611+
e.stopPropagation();
572612
return;
573613
}
574614

575615
let touch = getTouchById(e.nativeEvent, state.activePointerId);
616+
let shouldStopPropagation = true;
576617
if (touch && isOverTarget(touch, e.currentTarget)) {
577618
if (!state.isOverTarget) {
578619
state.isOverTarget = true;
579-
triggerPressStart(e, state.pointerType);
620+
shouldStopPropagation = triggerPressStart(e, state.pointerType);
580621
}
581622
} else if (state.isOverTarget) {
582623
state.isOverTarget = false;
583-
triggerPressEnd(e, state.pointerType, false);
624+
shouldStopPropagation = triggerPressEnd(e, state.pointerType, false);
584625
cancelOnPointerExit(e);
585626
}
627+
628+
if (shouldStopPropagation) {
629+
e.stopPropagation();
630+
}
586631
};
587632

588633
pressProps.onTouchEnd = (e) => {
589634
if (!e.currentTarget.contains(e.target as Element)) {
590635
return;
591636
}
592637

593-
e.stopPropagation();
594638
if (!state.isPressed) {
639+
e.stopPropagation();
595640
return;
596641
}
597642

598643
let touch = getTouchById(e.nativeEvent, state.activePointerId);
644+
let shouldStopPropagation = true;
599645
if (touch && isOverTarget(touch, e.currentTarget)) {
600646
triggerPressUp(e, state.pointerType);
601-
triggerPressEnd(e, state.pointerType);
647+
shouldStopPropagation = triggerPressEnd(e, state.pointerType);
602648
} else if (state.isOverTarget) {
603-
triggerPressEnd(e, state.pointerType, false);
649+
shouldStopPropagation = triggerPressEnd(e, state.pointerType, false);
650+
}
651+
652+
if (shouldStopPropagation) {
653+
e.stopPropagation();
604654
}
605655

606656
state.isPressed = false;

0 commit comments

Comments
 (0)