1313import {
1414 CSSResultArray ,
1515 html ,
16- INPUT_COMPONENT_PATTERN ,
1716 nothing ,
1817 PropertyValues ,
1918 TemplateResult ,
@@ -181,6 +180,8 @@ export class MenuItem extends LikeAnchor(
181180 return this . _value || this . itemText ;
182181 }
183182
183+ private _lastPointerType ?: string ;
184+
184185 public set value ( value : string ) {
185186 if ( value === this . _value ) {
186187 return ;
@@ -457,124 +458,15 @@ export class MenuItem extends LikeAnchor(
457458 }
458459 }
459460
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-
472461 protected override firstUpdated ( changes : PropertyValues ) : void {
473462 super . firstUpdated ( changes ) ;
474463 this . setAttribute ( 'tabindex' , '-1' ) ;
475464 this . addEventListener ( 'keydown' , this . handleKeydown ) ;
476- this . addEventListener ( 'mouseover' , this . handleMouseover ) ;
477- this . addEventListener ( 'pointerdown' , this . handlePointerdown ) ;
478- this . addEventListener ( 'pointerenter' , this . closeOverlaysForRoot ) ;
479465 if ( ! this . hasAttribute ( 'id' ) ) {
480466 this . id = `sp-menu-item-${ randomID ( ) } ` ;
481467 }
482468 }
483469
484- private getActiveElementSafely ( ) : HTMLElement | null {
485- let root = this . getRootNode ( ) as Document | ShadowRoot ;
486- let activeElement = root . activeElement as HTMLElement ;
487-
488- // If no active element in current context and we're in shadow DOM,
489- // traverse up to find the document-level active element
490- if ( ! activeElement && root !== document ) {
491- while ( root && root !== document && 'host' in root ) {
492- root = ( root as ShadowRoot ) . host . getRootNode ( ) as
493- | Document
494- | ShadowRoot ;
495- activeElement = root . activeElement as HTMLElement ;
496- if ( activeElement ) break ;
497- }
498- }
499-
500- return activeElement ;
501- }
502-
503- handleMouseover ( event : MouseEvent ) : void {
504- const target = event . target as HTMLElement ;
505- if ( target === this ) {
506- // Check for active input elements across shadow boundaries
507- const activeElement = this . getActiveElementSafely ( ) ;
508-
509- // Only focus this menu item if no input element is currently active
510- // This prevents interrupting user input in search boxes, text fields, etc.
511- if ( ! activeElement || ! this . isInputElement ( activeElement ) ) {
512- this . focus ( ) ;
513- }
514- this . focused = false ;
515- }
516- }
517-
518- /**
519- * Determines if an element is an input field that should retain focus.
520- * Uses multiple detection strategies to identify input elements generically.
521- */
522- private isInputElement ( element : HTMLElement ) : boolean {
523- // Check for native HTML input elements
524- if ( this . isNativeInputElement ( element ) ) {
525- return true ;
526- }
527-
528- // Check for contenteditable elements (rich text editors)
529- if ( element . contentEditable === 'true' ) {
530- return true ;
531- }
532-
533- // Check for Spectrum Web Components with input-like behavior
534- if ( this . isSpectrumInputComponent ( element ) ) {
535- return true ;
536- }
537-
538- return false ;
539- }
540-
541- /**
542- * Checks if an element is a native HTML input element.
543- */
544- private isNativeInputElement ( element : HTMLElement ) : boolean {
545- return (
546- element instanceof HTMLInputElement ||
547- element instanceof HTMLTextAreaElement ||
548- element instanceof HTMLSelectElement
549- ) ;
550- }
551-
552- /**
553- * Checks if an element is a Spectrum Web Component with input behavior.
554- * Uses ARIA roles and component patterns for generic detection.
555- */
556- private isSpectrumInputComponent ( element : HTMLElement ) : boolean {
557- // Check if it's a Spectrum Web Component
558- if ( ! element . tagName . startsWith ( 'SP-' ) ) {
559- return false ;
560- }
561-
562- // Check ARIA role for input-like behavior
563- const role = element . getAttribute ( 'role' ) ;
564- const inputRoles = [ 'textbox' , 'searchbox' , 'combobox' , 'slider' ] ;
565- if ( role && inputRoles . includes ( role ) ) {
566- return true ;
567- }
568-
569- // Check for components that typically contain input elements
570- // This covers components like sp-search, sp-textfield, sp-number-field, etc.
571- const inputComponentPattern = INPUT_COMPONENT_PATTERN ;
572- if ( inputComponentPattern . test ( element . tagName ) ) {
573- return true ;
574- }
575-
576- return false ;
577- }
578470 /**
579471 * forward key info from keydown event to parent menu
580472 */
@@ -595,8 +487,8 @@ export class MenuItem extends LikeAnchor(
595487 } ;
596488
597489 protected closeOverlaysForRoot ( ) : void {
598- if ( this . open ) return ;
599- this . menuData . parentMenu ?. closeDescendentOverlays ( ) ;
490+ // if (this.open) return;
491+ // this.menuData.parentMenu?.closeDescendentOverlays();
600492 }
601493
602494 protected handleFocus ( event : FocusEvent ) : void {
@@ -613,11 +505,25 @@ export class MenuItem extends LikeAnchor(
613505 }
614506 }
615507
616- protected handleSubmenuClick ( event : Event ) : void {
508+ protected handleSubmenuTriggerClick ( event : Event ) : void {
617509 if ( event . composedPath ( ) . includes ( this . overlayElement ) ) {
618510 return ;
619511 }
620- this . openOverlay ( true ) ;
512+
513+ // If submenu is already open, toggle it closed
514+ if ( this . open && this . _lastPointerType === 'touch' ) {
515+ event . preventDefault ( ) ;
516+ event . stopPropagation ( ) ; // Don't let parent menu handle this
517+ this . open = false ;
518+ return ;
519+ }
520+
521+ // All: open if closed
522+ if ( ! this . open ) {
523+ event . preventDefault ( ) ;
524+ event . stopImmediatePropagation ( ) ;
525+ this . openOverlay ( true ) ;
526+ }
621527 }
622528
623529 protected handleSubmenuFocus ( ) : void {
@@ -640,7 +546,19 @@ export class MenuItem extends LikeAnchor(
640546 }
641547 } ;
642548
643- protected handlePointerenter ( ) : void {
549+ protected handlePointerenter ( event : PointerEvent ) : void {
550+ this . _lastPointerType = event . pointerType ; // Track pointer type
551+
552+ // For touch: don't handle pointerenter, let click handle it
553+ if ( event . pointerType === 'touch' ) {
554+ return ;
555+ }
556+
557+ // Close other submenus (from closeOverlaysForRoot)
558+ if ( ! this . open ) {
559+ this . menuData . parentMenu ?. closeDescendentOverlays ( ) ;
560+ }
561+
644562 if ( this . leaveTimeout ) {
645563 clearTimeout ( this . leaveTimeout ) ;
646564 delete this . leaveTimeout ;
@@ -654,7 +572,14 @@ export class MenuItem extends LikeAnchor(
654572 protected leaveTimeout ?: ReturnType < typeof setTimeout > ;
655573 protected recentlyLeftChild = false ;
656574
657- protected handlePointerleave ( ) : void {
575+ protected handlePointerleave ( event : PointerEvent ) : void {
576+ this . _lastPointerType = event . pointerType ; // Update on leave too
577+
578+ // For touch: don't handle pointerleave, let click handle it
579+ if ( event . pointerType === 'touch' ) {
580+ return ;
581+ }
582+
658583 this . _closedViaPointer = true ;
659584 if ( this . open && ! this . recentlyLeftChild ) {
660585 this . leaveTimeout = setTimeout ( ( ) => {
@@ -782,7 +707,7 @@ export class MenuItem extends LikeAnchor(
782707 const options = { signal : this . abortControllerSubmenu . signal } ;
783708 this . addEventListener (
784709 'click' ,
785- this . handleSubmenuClick ,
710+ this . handleSubmenuTriggerClick ,
786711 options
787712 ) ;
788713 this . addEventListener (
0 commit comments