@@ -459,15 +459,53 @@ export class MenuItem extends LikeAnchor(
459459 }
460460 }
461461
462+ private handlePointerdown ( event : PointerEvent ) : void {
463+ // Track pointer type for touch detection
464+ this . _lastPointerType = event . pointerType ;
465+
466+ // For touch devices with submenus, handle on pointerup instead of click
467+ // Only if the touch is directly on this menu item (not on overlay or child elements)
468+ if (
469+ event . pointerType === 'touch' &&
470+ this . hasSubmenu &&
471+ event . target === this
472+ ) {
473+ event . preventDefault ( ) ; // Prevent click suppression
474+ event . stopPropagation ( ) ; // Prevent bubbling to parent menu items
475+ this . addEventListener ( 'pointerup' , this . handleTouchSubmenuToggle , {
476+ once : true ,
477+ } ) ;
478+ }
479+
480+ if ( event . target === this && this . hasSubmenu && this . open ) {
481+ this . addEventListener ( 'focus' , this . handleSubmenuFocus , {
482+ once : true ,
483+ } ) ;
484+ this . overlayElement . addEventListener (
485+ 'beforetoggle' ,
486+ this . handleBeforetoggle
487+ ) ;
488+ }
489+ }
490+
491+ private handleTouchSubmenuToggle = ( event : PointerEvent ) : void => {
492+ event . preventDefault ( ) ;
493+ event . stopPropagation ( ) ;
494+
495+ if ( this . open ) {
496+ this . open = false ;
497+ } else {
498+ this . openOverlay ( true ) ;
499+ }
500+ } ;
501+
462502 protected override firstUpdated ( changes : PropertyValues ) : void {
463503 super . firstUpdated ( changes ) ;
464504 this . setAttribute ( 'tabindex' , '-1' ) ;
465505 this . addEventListener ( 'keydown' , this . handleKeydown ) ;
466506 this . addEventListener ( 'mouseover' , this . handleMouseover ) ;
467- // Register pointerenter/leave for ALL menu items (not just those with submenus)
468- // so items without submenus can close sibling submenus when hovered
469- this . addEventListener ( 'pointerenter' , this . handlePointerenter ) ;
470- this . addEventListener ( 'pointerleave' , this . handlePointerleave ) ;
507+ this . addEventListener ( 'pointerdown' , this . handlePointerdown ) ;
508+ this . addEventListener ( 'pointerenter' , this . closeOverlaysForRoot ) ;
471509 if ( ! this . hasAttribute ( 'id' ) ) {
472510 this . id = `sp-menu-item-${ randomID ( ) } ` ;
473511 }
@@ -567,7 +605,6 @@ export class MenuItem extends LikeAnchor(
567605
568606 return false ;
569607 }
570-
571608 /**
572609 * forward key info from keydown event to parent menu
573610 */
@@ -587,6 +624,11 @@ export class MenuItem extends LikeAnchor(
587624 }
588625 } ;
589626
627+ protected closeOverlaysForRoot ( ) : void {
628+ if ( this . open ) return ;
629+ this . menuData . parentMenu ?. closeDescendentOverlays ( ) ;
630+ }
631+
590632 protected handleFocus ( event : FocusEvent ) : void {
591633 const { target } = event ;
592634 if ( target === this ) {
@@ -601,60 +643,75 @@ export class MenuItem extends LikeAnchor(
601643 }
602644 }
603645
604- protected handleSubmenuTriggerClick ( event : Event ) : void {
605- if ( event . composedPath ( ) . includes ( this . overlayElement ) ) {
606- return ;
607- }
646+ protected handleSubmenuClick ( event : Event ) : void {
647+ const pointerEvent = event as PointerEvent ;
648+
649+ // Check if this is a touch event
650+ const isTouchEvent =
651+ pointerEvent . pointerType === 'touch' ||
652+ this . _lastPointerType === 'touch' ;
608653
609- // If submenu is already open, toggle it closed
610- if ( this . open && this . _lastPointerType === 'touch' ) {
654+ // For touch events, we handle EVERYTHING via pointerup, so completely ignore click
655+ if ( isTouchEvent ) {
656+ event . stopPropagation ( ) ;
611657 event . preventDefault ( ) ;
612- event . stopPropagation ( ) ; // Don't let parent menu handle this
613- this . open = false ;
614658 return ;
615659 }
616660
617- // All: open if closed
618- if ( ! this . open ) {
619- event . preventDefault ( ) ;
620- event . stopImmediatePropagation ( ) ;
621- this . openOverlay ( true ) ;
661+ // For non-touch (mouse) events, ignore clicks inside the overlay (on child items)
662+ if ( event . composedPath ( ) . includes ( this . overlayElement ) ) {
663+ return ;
622664 }
665+
666+ // For mouse: just open (close is handled by pointerleave)
667+ this . openOverlay ( true ) ;
623668 }
624669
670+ protected handleSubmenuFocus ( ) : void {
671+ requestAnimationFrame ( ( ) => {
672+ // Wait till after `closeDescendentOverlays` has happened in Menu
673+ // to reopen (keep open) the direct descendent of this Menu Item
674+ this . overlayElement . open = this . open ;
675+ this . focused = false ;
676+ } ) ;
677+ }
678+
679+ protected handleBeforetoggle = ( event : Event ) : void => {
680+ if ( ( event as Event & { newState : string } ) . newState === 'closed' ) {
681+ this . open = true ;
682+ this . overlayElement . manuallyKeepOpen ( ) ;
683+ this . overlayElement . removeEventListener (
684+ 'beforetoggle' ,
685+ this . handleBeforetoggle
686+ ) ;
687+ }
688+ } ;
689+
625690 protected handlePointerenter ( event : PointerEvent ) : void {
626- this . _lastPointerType = event . pointerType ; // Track pointer type
691+ this . _lastPointerType = event . pointerType ;
627692
628- // For touch: don't handle pointerenter, let click handle it
693+ // For touch devices, don't open on pointerenter - let click handle it
629694 if ( event . pointerType === 'touch' ) {
630695 return ;
631696 }
632697
633- // Close sibling submenus before opening this one
634- this . menuData . parentMenu ?. closeDescendentOverlays ( ) ;
635-
636698 if ( this . leaveTimeout ) {
637699 clearTimeout ( this . leaveTimeout ) ;
638700 delete this . leaveTimeout ;
639701 this . recentlyLeftChild = false ;
640702 return ;
641703 }
642-
643- // Only focus items with submenus on hover (to show they're interactive)
644- // Regular items should not show focus styling on hover, only on keyboard navigation
645- if ( this . hasSubmenu ) {
646- this . focus ( ) ;
647- }
704+ this . focus ( ) ;
648705 this . openOverlay ( ) ;
649706 }
650707
651708 protected leaveTimeout ?: ReturnType < typeof setTimeout > ;
652709 protected recentlyLeftChild = false ;
653710
654711 protected handlePointerleave ( event : PointerEvent ) : void {
655- this . _lastPointerType = event . pointerType ; // Update on leave too
712+ this . _lastPointerType = event . pointerType ;
656713
657- // For touch: don't handle pointerleave, let click handle it
714+ // For touch devices, don't close on pointerleave - let click handle it
658715 if ( event . pointerType === 'touch' ) {
659716 return ;
660717 }
@@ -786,7 +843,17 @@ export class MenuItem extends LikeAnchor(
786843 const options = { signal : this . abortControllerSubmenu . signal } ;
787844 this . addEventListener (
788845 'click' ,
789- this . handleSubmenuTriggerClick ,
846+ this . handleSubmenuClick ,
847+ options
848+ ) ;
849+ this . addEventListener (
850+ 'pointerenter' ,
851+ this . handlePointerenter ,
852+ options
853+ ) ;
854+ this . addEventListener (
855+ 'pointerleave' ,
856+ this . handlePointerleave ,
790857 options
791858 ) ;
792859 this . addEventListener (
0 commit comments