Skip to content

Commit 03c23c4

Browse files
committed
refactor usePress to still have global listeners for cleanup across boundaries
1 parent 5aeb024 commit 03c23c4

File tree

2 files changed

+160
-62
lines changed

2 files changed

+160
-62
lines changed

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

Lines changed: 78 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,8 @@ export function usePress(props: PressHookProps): PressResult {
293293
let state = ref.current;
294294
let pressProps: DOMAttributes = {
295295
onKeyDown(e) {
296-
if (isValidKeyboardEvent(e.nativeEvent, e.currentTarget) && e.currentTarget.contains(e.target as Element)) {
297-
if (shouldPreventDefaultKeyboard(e.target as Element, e.key)) {
296+
if (isValidKeyboardEvent(e.nativeEvent, e.currentTarget) && nodeContains(e.currentTarget, e.nativeEvent.composedPath()[0] as Element)) {
297+
if (shouldPreventDefaultKeyboard(e.nativeEvent.composedPath()[0] as Element, e.key)) {
298298
e.preventDefault();
299299
}
300300

@@ -312,16 +312,12 @@ export function usePress(props: PressHookProps): PressResult {
312312
// before stopPropagation from useKeyboard on a child element may happen and thus we can still call triggerPress for the parent element.
313313
let originalTarget = e.currentTarget;
314314
let pressUp = (e) => {
315-
if (isValidKeyboardEvent(e, originalTarget) && !e.repeat && originalTarget.contains(e.target as Element) && state.target) {
315+
if (isValidKeyboardEvent(e, originalTarget) && !e.repeat && nodeContains(originalTarget, e.composedPath()[0] as Element) && state.target) {
316316
triggerPressUp(createEvent(state.target, e), 'keyboard');
317317
}
318318
};
319319

320-
const ownerDocument = getRootNode(e.currentTarget);
321-
322-
if (ownerDocument) {
323-
addGlobalListener(ownerDocument, 'keyup', chain(pressUp, onKeyUp), true);
324-
}
320+
addGlobalListener(getOwnerDocument(e.currentTarget), 'keyup', chain(pressUp, onKeyUp), true);
325321
}
326322

327323
if (shouldStopPropagation) {
@@ -343,7 +339,7 @@ export function usePress(props: PressHookProps): PressResult {
343339
}
344340
},
345341
onClick(e) {
346-
if (e && !e.currentTarget.contains(e.target as Element)) {
342+
if (e && !nodeContains(e.currentTarget, e.nativeEvent.composedPath()[0] as Element)) {
347343
return;
348344
}
349345

@@ -378,18 +374,18 @@ export function usePress(props: PressHookProps): PressResult {
378374

379375
let onKeyUp = (e: KeyboardEvent) => {
380376
if (state.isPressed && state.target && isValidKeyboardEvent(e, state.target)) {
381-
if (shouldPreventDefaultKeyboard(e.target as Element, e.key)) {
377+
if (shouldPreventDefaultKeyboard(e.composedPath()[0] as Element, e.key)) {
382378
e.preventDefault();
383379
}
384380

385-
let target = e.target as Element;
386-
triggerPressEnd(createEvent(state.target, e), 'keyboard', state.target.contains(target));
381+
let target = e.composedPath()[0] as Element;
382+
triggerPressEnd(createEvent(state.target, e), 'keyboard', nodeContains(state.target, e.composedPath()[0] as Element));
387383
removeAllGlobalListeners();
388384

389385
// If a link was triggered with a key other than Enter, open the URL ourselves.
390386
// This means the link has a role override, and the default browser behavior
391387
// only applies when using the Enter key.
392-
if (e.key !== 'Enter' && isHTMLAnchorLink(state.target) && state.target.contains(target) && !e[LINK_CLICKED]) {
388+
if (e.key !== 'Enter' && isHTMLAnchorLink(state.target) && nodeContains(state.target, target) && !e[LINK_CLICKED]) {
393389
// Store a hidden property on the event so we only trigger link click once,
394390
// even if there are multiple usePress instances attached to the element.
395391
e[LINK_CLICKED] = true;
@@ -413,7 +409,7 @@ export function usePress(props: PressHookProps): PressResult {
413409
if (typeof PointerEvent !== 'undefined') {
414410
pressProps.onPointerDown = (e) => {
415411
// Only handle left clicks, and ignore events that bubbled through portals.
416-
if (e.button !== 0 || !e.currentTarget.contains(e.target as Element)) {
412+
if (e.button !== 0 || !nodeContains(e.currentTarget, e.nativeEvent.composedPath()[0] as Element)) {
417413
return;
418414
}
419415

@@ -439,10 +435,10 @@ export function usePress(props: PressHookProps): PressResult {
439435
state.isPressed = true;
440436
state.isOverTarget = true;
441437
state.activePointerId = e.pointerId;
442-
state.target = e.currentTarget;
438+
state.target = e.nativeEvent.composedPath()[0] as FocusableElement;
443439

444440
if (!isDisabled && !preventFocusOnPress) {
445-
focusWithoutScrolling(e.currentTarget);
441+
focusWithoutScrolling(state.target);
446442
}
447443

448444
if (!allowTextSelectionOnPress) {
@@ -451,11 +447,15 @@ export function usePress(props: PressHookProps): PressResult {
451447

452448
shouldStopPropagation = triggerPressStart(e, state.pointerType);
453449

454-
const ownerDocument = getRootNode(e.currentTarget) || getOwnerDocument(e.currentTarget);
450+
// Release pointer capture so that touch interactions can leave the original target.
451+
// This enables onPointerLeave and onPointerEnter to fire.
452+
let target = e.nativeEvent.composedPath()[0] as Element;
453+
if ('releasePointerCapture' in target) {
454+
target.releasePointerCapture(e.pointerId);
455+
}
455456

456-
addGlobalListener(ownerDocument, 'pointermove', onPointerMove, false);
457-
addGlobalListener(ownerDocument, 'pointerup', onPointerUp, false);
458-
addGlobalListener(ownerDocument, 'pointercancel', onPointerCancel, false);
457+
addGlobalListener(getOwnerDocument(e.currentTarget), 'pointerup', onPointerUp, false);
458+
addGlobalListener(getOwnerDocument(e.currentTarget), 'pointercancel', onPointerCancel, false);
459459
}
460460

461461
if (shouldStopPropagation) {
@@ -464,7 +464,7 @@ export function usePress(props: PressHookProps): PressResult {
464464
};
465465

466466
pressProps.onMouseDown = (e) => {
467-
if (!e.currentTarget.contains(e.target as Element)) {
467+
if (!nodeContains(e.currentTarget, e.nativeEvent.composedPath()[0] as Element)) {
468468
return;
469469
}
470470

@@ -482,32 +482,25 @@ export function usePress(props: PressHookProps): PressResult {
482482

483483
pressProps.onPointerUp = (e) => {
484484
// iOS fires pointerup with zero width and height, so check the pointerType recorded during pointerdown.
485-
if (!e.currentTarget.contains(e.target as Element) || state.pointerType === 'virtual') {
485+
if (!nodeContains(e.currentTarget, e.nativeEvent.composedPath()[0] as Element) || state.pointerType === 'virtual') {
486486
return;
487487
}
488488

489489
// Only handle left clicks
490-
// Safari on iOS sometimes fires pointerup events, even
491-
// when the touch isn't over the target, so double check.
492-
if (e.button === 0 && isOverTarget(e, e.currentTarget)) {
490+
if (e.button === 0) {
493491
triggerPressUp(e, state.pointerType || e.pointerType);
494492
}
495493
};
496494

497-
// Safari on iOS < 13.2 does not implement pointerenter/pointerleave events correctly.
498-
// Use pointer move events instead to implement our own hit testing.
499-
// See https://bugs.webkit.org/show_bug.cgi?id=199803
500-
let onPointerMove = (e: PointerEvent) => {
501-
if (e.pointerId !== state.activePointerId) {
502-
return;
495+
pressProps.onPointerEnter = (e) => {
496+
if (e.pointerId === state.activePointerId && state.target && !state.isOverTarget && state.pointerType != null) {
497+
state.isOverTarget = true;
498+
triggerPressStart(createEvent(state.target, e), state.pointerType);
503499
}
500+
};
504501

505-
if (state.target && isOverTarget(e, state.target)) {
506-
if (!state.isOverTarget && state.pointerType != null) {
507-
state.isOverTarget = true;
508-
triggerPressStart(createEvent(state.target, e), state.pointerType);
509-
}
510-
} else if (state.target && state.isOverTarget && state.pointerType != null) {
502+
pressProps.onPointerLeave = (e) => {
503+
if (e.pointerId === state.activePointerId && state.target && state.isOverTarget && state.pointerType != null) {
511504
state.isOverTarget = false;
512505
triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
513506
cancelOnPointerExit(e);
@@ -516,7 +509,7 @@ export function usePress(props: PressHookProps): PressResult {
516509

517510
let onPointerUp = (e: PointerEvent) => {
518511
if (e.pointerId === state.activePointerId && state.isPressed && e.button === 0 && state.target) {
519-
if (isOverTarget(e, state.target) && state.pointerType != null) {
512+
if (nodeContains(state.target, e.composedPath()[0] as Element) && state.pointerType != null) {
520513
triggerPressEnd(createEvent(state.target, e), state.pointerType);
521514
} else if (state.isOverTarget && state.pointerType != null) {
522515
triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
@@ -557,7 +550,7 @@ export function usePress(props: PressHookProps): PressResult {
557550
};
558551

559552
pressProps.onDragStart = (e) => {
560-
if (!e.currentTarget.contains(e.target as Element)) {
553+
if (!nodeContains(e.currentTarget, e.nativeEvent.composedPath()[0] as Element)) {
561554
return;
562555
}
563556

@@ -567,7 +560,7 @@ export function usePress(props: PressHookProps): PressResult {
567560
} else {
568561
pressProps.onMouseDown = (e) => {
569562
// Only handle left clicks
570-
if (e.button !== 0 || !e.currentTarget.contains(e.target as Element)) {
563+
if (e.button !== 0 || !nodeContains(e.currentTarget, e.nativeEvent.composedPath()[0] as Element)) {
571564
return;
572565
}
573566

@@ -595,13 +588,12 @@ export function usePress(props: PressHookProps): PressResult {
595588
if (shouldStopPropagation) {
596589
e.stopPropagation();
597590
}
598-
const ownerDocument = getRootNode(e.currentTarget) || getOwnerDocument(e.currentTarget);
599591

600-
addGlobalListener(ownerDocument, 'mouseup', onMouseUp, false);
592+
addGlobalListener(getOwnerDocument(e.currentTarget), 'mouseup', onMouseUp, false);
601593
};
602594

603595
pressProps.onMouseEnter = (e) => {
604-
if (!e.currentTarget.contains(e.target as Element)) {
596+
if (!nodeContains(e.currentTarget, e.nativeEvent.composedPath()[0] as Element)) {
605597
return;
606598
}
607599

@@ -617,7 +609,7 @@ export function usePress(props: PressHookProps): PressResult {
617609
};
618610

619611
pressProps.onMouseLeave = (e) => {
620-
if (!e.currentTarget.contains(e.target as Element)) {
612+
if (!nodeContains(e.currentTarget, e.nativeEvent.composedPath()[0] as Element)) {
621613
return;
622614
}
623615

@@ -634,7 +626,7 @@ export function usePress(props: PressHookProps): PressResult {
634626
};
635627

636628
pressProps.onMouseUp = (e) => {
637-
if (!e.currentTarget.contains(e.target as Element)) {
629+
if (!nodeContains(e.currentTarget, e.nativeEvent.composedPath()[0] as Element)) {
638630
return;
639631
}
640632

@@ -667,7 +659,7 @@ export function usePress(props: PressHookProps): PressResult {
667659
};
668660

669661
pressProps.onTouchStart = (e) => {
670-
if (!e.currentTarget.contains(e.target as Element)) {
662+
if (!nodeContains(e.currentTarget, e.nativeEvent.composedPath()[0] as Element)) {
671663
return;
672664
}
673665

@@ -701,7 +693,7 @@ export function usePress(props: PressHookProps): PressResult {
701693
};
702694

703695
pressProps.onTouchMove = (e) => {
704-
if (!e.currentTarget.contains(e.target as Element)) {
696+
if (!nodeContains(e.currentTarget, e.nativeEvent.composedPath()[0] as Element)) {
705697
return;
706698
}
707699

@@ -729,7 +721,7 @@ export function usePress(props: PressHookProps): PressResult {
729721
};
730722

731723
pressProps.onTouchEnd = (e) => {
732-
if (!e.currentTarget.contains(e.target as Element)) {
724+
if (!nodeContains(e.currentTarget, e.nativeEvent.composedPath()[0] as Element)) {
733725
return;
734726
}
735727

@@ -762,7 +754,7 @@ export function usePress(props: PressHookProps): PressResult {
762754
};
763755

764756
pressProps.onTouchCancel = (e) => {
765-
if (!e.currentTarget.contains(e.target as Element)) {
757+
if (!nodeContains(e.currentTarget, e.nativeEvent.composedPath()[0] as Element)) {
766758
return;
767759
}
768760

@@ -773,7 +765,7 @@ export function usePress(props: PressHookProps): PressResult {
773765
};
774766

775767
let onScroll = (e: Event) => {
776-
if (state.isPressed && (e.target as Element).contains(state.target)) {
768+
if (state.isPressed && nodeContains(e.composedPath()[0] as Element, state.target)) {
777769
cancel({
778770
currentTarget: state.target,
779771
shiftKey: false,
@@ -785,7 +777,7 @@ export function usePress(props: PressHookProps): PressResult {
785777
};
786778

787779
pressProps.onDragStart = (e) => {
788-
if (!e.currentTarget.contains(e.target as Element)) {
780+
if (!nodeContains(e.currentTarget, e.nativeEvent.composedPath()[0] as Element)) {
789781
return;
790782
}
791783

@@ -808,7 +800,7 @@ export function usePress(props: PressHookProps): PressResult {
808800
]);
809801

810802
// Remove user-select: none in case component unmounts immediately after pressStart
811-
803+
812804
useEffect(() => {
813805
return () => {
814806
if (!allowTextSelectionOnPress) {
@@ -1001,3 +993,37 @@ function isValidInputKey(target: HTMLInputElement, key: string) {
1001993
? key === ' '
1002994
: nonTextInputTypes.has(target.type);
1003995
}
996+
997+
// https://github.com/microsoft/tabster/blob/a89fc5d7e332d48f68d03b1ca6e344489d1c3898/src/Shadowdomize/DOMFunctions.ts#L16
998+
export function nodeContains(
999+
node: Node | null | undefined,
1000+
otherNode: Node | null | undefined
1001+
): boolean {
1002+
if (!node || !otherNode) {
1003+
return false;
1004+
}
1005+
1006+
let currentNode: HTMLElement | Node | null | undefined = otherNode;
1007+
1008+
while (currentNode) {
1009+
if (currentNode === node) {
1010+
return true;
1011+
}
1012+
1013+
if (
1014+
typeof (currentNode as HTMLSlotElement).assignedElements !==
1015+
'function' &&
1016+
(currentNode as HTMLElement).assignedSlot?.parentNode
1017+
) {
1018+
// Element is slotted
1019+
currentNode = (currentNode as HTMLElement).assignedSlot?.parentNode;
1020+
} else if (currentNode.nodeType === document.DOCUMENT_FRAGMENT_NODE) {
1021+
// Element is in shadow root
1022+
currentNode = (currentNode as ShadowRoot).host;
1023+
} else {
1024+
currentNode = currentNode.parentNode;
1025+
}
1026+
}
1027+
1028+
return false;
1029+
}

0 commit comments

Comments
 (0)