diff --git a/.changeset/cruel-eels-open.md b/.changeset/cruel-eels-open.md
new file mode 100644
index 00000000000..1a262f4e355
--- /dev/null
+++ b/.changeset/cruel-eels-open.md
@@ -0,0 +1,5 @@
+---
+'@spectrum-web-components/menu': minor
+---
+
+**Fixed** MenuItem focus stealing from input elements on mouseover by enhanceing MenuItem's `handleMouseover` method to detect when an input element currently has focus and prevent stealing focus in those cases.
diff --git a/packages/menu/src/MenuItem.ts b/packages/menu/src/MenuItem.ts
index 05f899b1f1a..f6f92cfe822 100644
--- a/packages/menu/src/MenuItem.ts
+++ b/packages/menu/src/MenuItem.ts
@@ -13,6 +13,7 @@
import {
CSSResultArray,
html,
+ INPUT_COMPONENT_PATTERN,
nothing,
PropertyValues,
TemplateResult,
@@ -479,13 +480,83 @@ export class MenuItem extends LikeAnchor(
this.id = `sp-menu-item-${randomID()}`;
}
}
+
handleMouseover(event: MouseEvent): void {
const target = event.target as HTMLElement;
if (target === this) {
- this.focus();
+ // Get the currently focused element within the component's root context
+ const rootNode = this.getRootNode() as Document | ShadowRoot;
+ const activeElement = rootNode.activeElement as HTMLElement;
+
+ // Only focus this menu item if no input element is currently active
+ // This prevents interrupting user input in search boxes, text fields, etc.
+ if (!activeElement || !this.isInputElement(activeElement)) {
+ this.focus();
+ }
this.focused = false;
}
}
+
+ /**
+ * Determines if an element is an input field that should retain focus.
+ * Uses multiple detection strategies to identify input elements generically.
+ */
+ private isInputElement(element: HTMLElement): boolean {
+ // Check for native HTML input elements
+ if (this.isNativeInputElement(element)) {
+ return true;
+ }
+
+ // Check for contenteditable elements (rich text editors)
+ if (element.contentEditable === 'true') {
+ return true;
+ }
+
+ // Check for Spectrum Web Components with input-like behavior
+ if (this.isSpectrumInputComponent(element)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks if an element is a native HTML input element.
+ */
+ private isNativeInputElement(element: HTMLElement): boolean {
+ return (
+ element instanceof HTMLInputElement ||
+ element instanceof HTMLTextAreaElement ||
+ element instanceof HTMLSelectElement
+ );
+ }
+
+ /**
+ * Checks if an element is a Spectrum Web Component with input behavior.
+ * Uses ARIA roles and component patterns for generic detection.
+ */
+ private isSpectrumInputComponent(element: HTMLElement): boolean {
+ // Check if it's a Spectrum Web Component
+ if (!element.tagName.startsWith('SP-')) {
+ return false;
+ }
+
+ // Check ARIA role for input-like behavior
+ const role = element.getAttribute('role');
+ const inputRoles = ['textbox', 'searchbox', 'combobox', 'slider'];
+ if (role && inputRoles.includes(role)) {
+ return true;
+ }
+
+ // Check for components that typically contain input elements
+ // This covers components like sp-search, sp-textfield, sp-number-field, etc.
+ const inputComponentPattern = INPUT_COMPONENT_PATTERN;
+ if (inputComponentPattern.test(element.tagName)) {
+ return true;
+ }
+
+ return false;
+ }
/**
* forward key info from keydown event to parent menu
*/
diff --git a/packages/menu/stories/menu.stories.ts b/packages/menu/stories/menu.stories.ts
index bebe33f6148..992d1a1d1ad 100644
--- a/packages/menu/stories/menu.stories.ts
+++ b/packages/menu/stories/menu.stories.ts
@@ -28,6 +28,11 @@ import '@spectrum-web-components/icons-workflow/icons/sp-icon-export.js';
import '@spectrum-web-components/icons-workflow/icons/sp-icon-folder-open.js';
import '@spectrum-web-components/icons-workflow/icons/sp-icon-share.js';
import '@spectrum-web-components/icons-workflow/icons/sp-icon-show-menu.js';
+import '@spectrum-web-components/search/sp-search.js';
+import '@spectrum-web-components/textfield/sp-textfield.js';
+import '@spectrum-web-components/number-field/sp-number-field.js';
+import '@spectrum-web-components/combobox/sp-combobox.js';
+import '@spectrum-web-components/color-field/sp-color-field.js';
export default {
component: 'sp-menu',
@@ -484,3 +489,105 @@ export const dynamicRemoval = (): TemplateResult => {
`;
};
+
+export const InputsWithMenu = (): TemplateResult => {
+ return html`
+
+
Input Focus Demo
+
+ Try typing in any input field below, then hover over the menu
+ items. The input should maintain focus and not be interrupted.
+ This demonstrates the fix for focus stealing from all supported
+ input types.
+