1313import {
1414 CSSResultArray ,
1515 html ,
16+ INPUT_COMPONENT_PATTERN ,
1617 nothing ,
1718 PropertyValues ,
1819 TemplateResult ,
@@ -462,11 +463,107 @@ export class MenuItem extends LikeAnchor(
462463 super . firstUpdated ( changes ) ;
463464 this . setAttribute ( 'tabindex' , '-1' ) ;
464465 this . addEventListener ( 'keydown' , this . handleKeydown ) ;
466+ this . addEventListener ( 'mouseover' , this . handleMouseover ) ;
465467 if ( ! this . hasAttribute ( 'id' ) ) {
466468 this . id = `sp-menu-item-${ randomID ( ) } ` ;
467469 }
468470 }
469471
472+ private getActiveElementSafely ( ) : HTMLElement | null {
473+ let root = this . getRootNode ( ) as Document | ShadowRoot ;
474+ let activeElement = root . activeElement as HTMLElement ;
475+
476+ // If no active element in current context and we're in shadow DOM,
477+ // traverse up to find the document-level active element
478+ if ( ! activeElement && root !== document ) {
479+ while ( root && root !== document && 'host' in root ) {
480+ root = ( root as ShadowRoot ) . host . getRootNode ( ) as
481+ | Document
482+ | ShadowRoot ;
483+ activeElement = root . activeElement as HTMLElement ;
484+ if ( activeElement ) break ;
485+ }
486+ }
487+
488+ return activeElement ;
489+ }
490+
491+ handleMouseover ( event : MouseEvent ) : void {
492+ const target = event . target as HTMLElement ;
493+ if ( target === this ) {
494+ // Check for active input elements across shadow boundaries
495+ const activeElement = this . getActiveElementSafely ( ) ;
496+
497+ // Only focus this menu item if no input element is currently active
498+ // This prevents interrupting user input in search boxes, text fields, etc.
499+ if ( ! activeElement || ! this . isInputElement ( activeElement ) ) {
500+ this . focus ( ) ;
501+ }
502+ this . focused = false ;
503+ }
504+ }
505+
506+ /**
507+ * Determines if an element is an input field that should retain focus.
508+ * Uses multiple detection strategies to identify input elements generically.
509+ */
510+ private isInputElement ( element : HTMLElement ) : boolean {
511+ // Check for native HTML input elements
512+ if ( this . isNativeInputElement ( element ) ) {
513+ return true ;
514+ }
515+
516+ // Check for contenteditable elements (rich text editors)
517+ if ( element . contentEditable === 'true' ) {
518+ return true ;
519+ }
520+
521+ // Check for Spectrum Web Components with input-like behavior
522+ if ( this . isSpectrumInputComponent ( element ) ) {
523+ return true ;
524+ }
525+
526+ return false ;
527+ }
528+
529+ /**
530+ * Checks if an element is a native HTML input element.
531+ */
532+ private isNativeInputElement ( element : HTMLElement ) : boolean {
533+ return (
534+ element instanceof HTMLInputElement ||
535+ element instanceof HTMLTextAreaElement ||
536+ element instanceof HTMLSelectElement
537+ ) ;
538+ }
539+
540+ /**
541+ * Checks if an element is a Spectrum Web Component with input behavior.
542+ * Uses ARIA roles and component patterns for generic detection.
543+ */
544+ private isSpectrumInputComponent ( element : HTMLElement ) : boolean {
545+ // Check if it's a Spectrum Web Component
546+ if ( ! element . tagName . startsWith ( 'SP-' ) ) {
547+ return false ;
548+ }
549+
550+ // Check ARIA role for input-like behavior
551+ const role = element . getAttribute ( 'role' ) ;
552+ const inputRoles = [ 'textbox' , 'searchbox' , 'combobox' , 'slider' ] ;
553+ if ( role && inputRoles . includes ( role ) ) {
554+ return true ;
555+ }
556+
557+ // Check for components that typically contain input elements
558+ // This covers components like sp-search, sp-textfield, sp-number-field, etc.
559+ const inputComponentPattern = INPUT_COMPONENT_PATTERN ;
560+ if ( inputComponentPattern . test ( element . tagName ) ) {
561+ return true ;
562+ }
563+
564+ return false ;
565+ }
566+
470567 /**
471568 * forward key info from keydown event to parent menu
472569 */
@@ -486,11 +583,6 @@ export class MenuItem extends LikeAnchor(
486583 }
487584 } ;
488585
489- protected closeOverlaysForRoot ( ) : void {
490- // if (this.open) return;
491- // this.menuData.parentMenu?.closeDescendentOverlays();
492- }
493-
494586 protected handleFocus ( event : FocusEvent ) : void {
495587 const { target } = event ;
496588 if ( target === this ) {
@@ -554,7 +646,7 @@ export class MenuItem extends LikeAnchor(
554646 return ;
555647 }
556648
557- // Close other submenus (from closeOverlaysForRoot)
649+ // Close other submenus
558650 if ( ! this . open ) {
559651 this . menuData . parentMenu ?. closeDescendentOverlays ( ) ;
560652 }
0 commit comments