Skip to content

Commit 2a7a50b

Browse files
author
Helen Le
committed
fix(menu): handle touch device submenu interactions
1 parent f739749 commit 2a7a50b

File tree

1 file changed

+45
-52
lines changed

1 file changed

+45
-52
lines changed

packages/menu/src/MenuItem.ts

Lines changed: 45 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ export class MenuItem extends LikeAnchor(
195195

196196
private _value = '';
197197

198+
private _lastPointerType?: string;
199+
198200
/**
199201
* @private
200202
* text content of the menu item minus whitespace
@@ -457,25 +459,15 @@ export class MenuItem extends LikeAnchor(
457459
}
458460
}
459461

460-
private handlePointerdown(event: PointerEvent): void {
461-
if (event.target === this && this.hasSubmenu && this.open) {
462-
this.addEventListener('focus', this.handleSubmenuFocus, {
463-
once: true,
464-
});
465-
this.overlayElement.addEventListener(
466-
'beforetoggle',
467-
this.handleBeforetoggle
468-
);
469-
}
470-
}
471-
472462
protected override firstUpdated(changes: PropertyValues): void {
473463
super.firstUpdated(changes);
474464
this.setAttribute('tabindex', '-1');
475465
this.addEventListener('keydown', this.handleKeydown);
476466
this.addEventListener('mouseover', this.handleMouseover);
477-
this.addEventListener('pointerdown', this.handlePointerdown);
478-
this.addEventListener('pointerenter', this.closeOverlaysForRoot);
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);
479471
if (!this.hasAttribute('id')) {
480472
this.id = `sp-menu-item-${randomID()}`;
481473
}
@@ -594,11 +586,6 @@ export class MenuItem extends LikeAnchor(
594586
}
595587
};
596588

597-
protected closeOverlaysForRoot(): void {
598-
if (this.open) return;
599-
this.menuData.parentMenu?.closeDescendentOverlays();
600-
}
601-
602589
protected handleFocus(event: FocusEvent): void {
603590
const { target } = event;
604591
if (target === this) {
@@ -613,48 +600,64 @@ export class MenuItem extends LikeAnchor(
613600
}
614601
}
615602

616-
protected handleSubmenuClick(event: Event): void {
603+
protected handleSubmenuTriggerClick(event: Event): void {
617604
if (event.composedPath().includes(this.overlayElement)) {
618605
return;
619606
}
620-
this.openOverlay(true);
621-
}
622607

623-
protected handleSubmenuFocus(): void {
624-
requestAnimationFrame(() => {
625-
// Wait till after `closeDescendentOverlays` has happened in Menu
626-
// to reopen (keep open) the direct descendent of this Menu Item
627-
this.overlayElement.open = this.open;
628-
this.focused = false;
629-
});
608+
// If submenu is already open, toggle it closed
609+
if (this.open && this._lastPointerType === 'touch') {
610+
event.preventDefault();
611+
event.stopPropagation(); // Don't let parent menu handle this
612+
this.open = false;
613+
return;
614+
}
615+
616+
// All: open if closed
617+
if (!this.open) {
618+
event.preventDefault();
619+
event.stopImmediatePropagation();
620+
this.openOverlay(true);
621+
}
630622
}
631623

632-
protected handleBeforetoggle = (event: Event): void => {
633-
if ((event as Event & { newState: string }).newState === 'closed') {
634-
this.open = true;
635-
this.overlayElement.manuallyKeepOpen();
636-
this.overlayElement.removeEventListener(
637-
'beforetoggle',
638-
this.handleBeforetoggle
639-
);
624+
protected handlePointerenter(event: PointerEvent): void {
625+
this._lastPointerType = event.pointerType; // Track pointer type
626+
627+
// For touch: don't handle pointerenter, let click handle it
628+
if (event.pointerType === 'touch') {
629+
return;
640630
}
641-
};
642631

643-
protected handlePointerenter(): void {
632+
// Close sibling submenus before opening this one
633+
this.menuData.parentMenu?.closeDescendentOverlays();
634+
644635
if (this.leaveTimeout) {
645636
clearTimeout(this.leaveTimeout);
646637
delete this.leaveTimeout;
647638
this.recentlyLeftChild = false;
648639
return;
649640
}
650-
this.focus();
641+
642+
// Only focus items with submenus on hover (to show they're interactive)
643+
// Regular items should not show focus styling on hover, only on keyboard navigation
644+
if (this.hasSubmenu) {
645+
this.focus();
646+
}
651647
this.openOverlay();
652648
}
653649

654650
protected leaveTimeout?: ReturnType<typeof setTimeout>;
655651
protected recentlyLeftChild = false;
656652

657-
protected handlePointerleave(): void {
653+
protected handlePointerleave(event: PointerEvent): void {
654+
this._lastPointerType = event.pointerType; // Update on leave too
655+
656+
// For touch: don't handle pointerleave, let click handle it
657+
if (event.pointerType === 'touch') {
658+
return;
659+
}
660+
658661
this._closedViaPointer = true;
659662
if (this.open && !this.recentlyLeftChild) {
660663
this.leaveTimeout = setTimeout(() => {
@@ -782,17 +785,7 @@ export class MenuItem extends LikeAnchor(
782785
const options = { signal: this.abortControllerSubmenu.signal };
783786
this.addEventListener(
784787
'click',
785-
this.handleSubmenuClick,
786-
options
787-
);
788-
this.addEventListener(
789-
'pointerenter',
790-
this.handlePointerenter,
791-
options
792-
);
793-
this.addEventListener(
794-
'pointerleave',
795-
this.handlePointerleave,
788+
this.handleSubmenuTriggerClick,
796789
options
797790
);
798791
this.addEventListener(

0 commit comments

Comments
 (0)