Skip to content

Commit 44942ca

Browse files
author
Shipra Gupta
committed
fix(menu): correct PropertyValues check and improve touch event detection
1 parent a8163fd commit 44942ca

File tree

1 file changed

+99
-32
lines changed

1 file changed

+99
-32
lines changed

1st-gen/packages/menu/src/MenuItem.ts

Lines changed: 99 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)