diff --git a/.changeset/chubby-birds-leave.md b/.changeset/chubby-birds-leave.md new file mode 100644 index 00000000000..4fd0245bda3 --- /dev/null +++ b/.changeset/chubby-birds-leave.md @@ -0,0 +1,5 @@ +--- +'@spectrum-web-components/menu': patch +--- + +**Fixed**: Added touch device support for submenu component [#9999](https://github.com/adobe/spectrum-web-components/pull/5818) diff --git a/package.json b/package.json index f8d0da0d860..f3351ffb0ae 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ }, "devDependencies": { "@changesets/changelog-github": "0.5.1", - "@changesets/cli": "2.29.7", + "@changesets/cli": "^2.29.7", "@commitlint/cli": "19.8.1", "@commitlint/config-conventional": "19.8.1", "@custom-elements-manifest/analyzer": "0.10.6", diff --git a/packages/menu/src/MenuItem.ts b/packages/menu/src/MenuItem.ts index 75de1e0de3d..4450c23ef3c 100644 --- a/packages/menu/src/MenuItem.ts +++ b/packages/menu/src/MenuItem.ts @@ -181,6 +181,8 @@ export class MenuItem extends LikeAnchor( return this._value || this.itemText; } + private _lastPointerType?: string; + public set value(value: string) { if (value === this._value) { return; @@ -457,25 +459,15 @@ export class MenuItem extends LikeAnchor( } } - private handlePointerdown(event: PointerEvent): void { - if (event.target === this && this.hasSubmenu && this.open) { - this.addEventListener('focus', this.handleSubmenuFocus, { - once: true, - }); - this.overlayElement.addEventListener( - 'beforetoggle', - this.handleBeforetoggle - ); - } - } - protected override firstUpdated(changes: PropertyValues): void { super.firstUpdated(changes); this.setAttribute('tabindex', '-1'); this.addEventListener('keydown', this.handleKeydown); this.addEventListener('mouseover', this.handleMouseover); - this.addEventListener('pointerdown', this.handlePointerdown); - this.addEventListener('pointerenter', this.closeOverlaysForRoot); + // Register pointerenter/leave for ALL menu items (not just those with submenus) + // so items without submenus can close sibling submenus when hovered + this.addEventListener('pointerenter', this.handlePointerenter); + this.addEventListener('pointerleave', this.handlePointerleave); if (!this.hasAttribute('id')) { this.id = `sp-menu-item-${randomID()}`; } @@ -575,6 +567,7 @@ export class MenuItem extends LikeAnchor( return false; } + /** * forward key info from keydown event to parent menu */ @@ -594,11 +587,6 @@ export class MenuItem extends LikeAnchor( } }; - protected closeOverlaysForRoot(): void { - if (this.open) return; - this.menuData.parentMenu?.closeDescendentOverlays(); - } - protected handleFocus(event: FocusEvent): void { const { target } = event; if (target === this) { @@ -613,48 +601,64 @@ export class MenuItem extends LikeAnchor( } } - protected handleSubmenuClick(event: Event): void { + protected handleSubmenuTriggerClick(event: Event): void { if (event.composedPath().includes(this.overlayElement)) { return; } - this.openOverlay(true); - } - protected handleSubmenuFocus(): void { - requestAnimationFrame(() => { - // Wait till after `closeDescendentOverlays` has happened in Menu - // to reopen (keep open) the direct descendent of this Menu Item - this.overlayElement.open = this.open; - this.focused = false; - }); + // If submenu is already open, toggle it closed + if (this.open && this._lastPointerType === 'touch') { + event.preventDefault(); + event.stopPropagation(); // Don't let parent menu handle this + this.open = false; + return; + } + + // All: open if closed + if (!this.open) { + event.preventDefault(); + event.stopImmediatePropagation(); + this.openOverlay(true); + } } - protected handleBeforetoggle = (event: Event): void => { - if ((event as Event & { newState: string }).newState === 'closed') { - this.open = true; - this.overlayElement.manuallyKeepOpen(); - this.overlayElement.removeEventListener( - 'beforetoggle', - this.handleBeforetoggle - ); + protected handlePointerenter(event: PointerEvent): void { + this._lastPointerType = event.pointerType; // Track pointer type + + // For touch: don't handle pointerenter, let click handle it + if (event.pointerType === 'touch') { + return; } - }; - protected handlePointerenter(): void { + // Close sibling submenus before opening this one + this.menuData.parentMenu?.closeDescendentOverlays(); + if (this.leaveTimeout) { clearTimeout(this.leaveTimeout); delete this.leaveTimeout; this.recentlyLeftChild = false; return; } - this.focus(); + + // Only focus items with submenus on hover (to show they're interactive) + // Regular items should not show focus styling on hover, only on keyboard navigation + if (this.hasSubmenu) { + this.focus(); + } this.openOverlay(); } protected leaveTimeout?: ReturnType; protected recentlyLeftChild = false; - protected handlePointerleave(): void { + protected handlePointerleave(event: PointerEvent): void { + this._lastPointerType = event.pointerType; // Update on leave too + + // For touch: don't handle pointerleave, let click handle it + if (event.pointerType === 'touch') { + return; + } + this._closedViaPointer = true; if (this.open && !this.recentlyLeftChild) { this.leaveTimeout = setTimeout(() => { @@ -782,17 +786,7 @@ export class MenuItem extends LikeAnchor( const options = { signal: this.abortControllerSubmenu.signal }; this.addEventListener( 'click', - this.handleSubmenuClick, - options - ); - this.addEventListener( - 'pointerenter', - this.handlePointerenter, - options - ); - this.addEventListener( - 'pointerleave', - this.handlePointerleave, + this.handleSubmenuTriggerClick, options ); this.addEventListener( diff --git a/yarn.lock b/yarn.lock index 646ccceb503..22387fe51fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -175,7 +175,7 @@ __metadata: resolution: "@adobe/spectrum-web-components@workspace:." dependencies: "@changesets/changelog-github": "npm:0.5.1" - "@changesets/cli": "npm:2.29.7" + "@changesets/cli": "npm:^2.29.7" "@commitlint/cli": "npm:19.8.1" "@commitlint/config-conventional": "npm:19.8.1" "@custom-elements-manifest/analyzer": "npm:0.10.6" @@ -1581,7 +1581,7 @@ __metadata: languageName: node linkType: hard -"@changesets/cli@npm:2.29.7": +"@changesets/cli@npm:^2.29.7": version: 2.29.7 resolution: "@changesets/cli@npm:2.29.7" dependencies: