@@ -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