Skip to content

Commit 0fb732c

Browse files
author
Shipra Gupta
committed
fix: improve touch device submenu interactions
1 parent 8e9965f commit 0fb732c

File tree

1 file changed

+98
-6
lines changed

1 file changed

+98
-6
lines changed

packages/menu/src/MenuItem.ts

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import {
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

Comments
 (0)