fix: prevent autocomplete mouse hover from interfering with keyboard navigation

- Added separate hover and selection states in ContextMenu
- Mouse position no longer automatically highlights items
- Mouse must move at least 5 pixels to activate hover highlighting
- Keyboard selection (highlighted with activeSelectionBackground) takes precedence
- Mouse hover shows subtle hover background only after movement
- Fixes #8615
This commit is contained in:
Roo Code 2025-10-11 23:49:57 +00:00
parent 3a47c55a2e
commit 2a03b2b38f

View file

@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef, useState } from "react"
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { getIconForFilePath, getIconUrlByName, getIconForDirectoryPath } from "vscode-material-icons"
import { Settings } from "lucide-react"
@ -46,6 +46,9 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
}) => {
const [materialIconsBaseUri, setMaterialIconsBaseUri] = useState("")
const menuRef = useRef<HTMLDivElement>(null)
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
const [hasMouseMoved, setHasMouseMoved] = useState(false)
const lastMousePosRef = useRef<{ x: number; y: number } | null>(null)
const filteredOptions = useMemo(() => {
return getContextMenuOptions(searchQuery, selectedType, queryItems, dynamicSearchResults, modes, commands)
@ -73,6 +76,33 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
setMaterialIconsBaseUri(w.MATERIAL_ICONS_BASE_URI)
}, [])
// Track mouse movement to distinguish between actual hover and incidental cursor position
const handleMouseMove = useCallback((e: React.MouseEvent) => {
const currentPos = { x: e.clientX, y: e.clientY }
// If this is the first mouse position, just store it
if (!lastMousePosRef.current) {
lastMousePosRef.current = currentPos
return
}
// Check if the mouse has moved more than a threshold (e.g., 5 pixels)
const deltaX = Math.abs(currentPos.x - lastMousePosRef.current.x)
const deltaY = Math.abs(currentPos.y - lastMousePosRef.current.y)
if (deltaX > 5 || deltaY > 5) {
setHasMouseMoved(true)
lastMousePosRef.current = currentPos
}
}, [])
// Reset mouse tracking when menu opens/closes
useEffect(() => {
setHasMouseMoved(false)
setHoveredIndex(null)
lastMousePosRef.current = null
}, [searchQuery])
const renderOptionContent = (option: ContextMenuQueryItem) => {
switch (option.type) {
case ContextMenuOptionType.SectionHeader:
@ -340,14 +370,18 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
filteredOptions.map((option, index) => (
<div
key={`${option.type}-${option.value || index}`}
onClick={() => isOptionSelectable(option) && onSelect(option.type, option.value)}
onClick={() => {
if (isOptionSelectable(option)) {
setSelectedIndex(index)
onSelect(option.type, option.value)
}
}}
style={{
padding:
option.type === ContextMenuOptionType.SectionHeader
? "16px 8px 4px 8px"
: "4px 8px",
cursor: isOptionSelectable(option) ? "pointer" : "default",
color: "var(--vscode-dropdown-foreground)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
@ -358,14 +392,33 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
marginBottom: "2px",
}
: {}),
// Show different styles for selection vs hover
...(index === selectedIndex && isOptionSelectable(option)
? {
backgroundColor: "var(--vscode-list-activeSelectionBackground)",
color: "var(--vscode-list-activeSelectionForeground)",
}
: {}),
: index === hoveredIndex && isOptionSelectable(option) && hasMouseMoved
? {
backgroundColor: "var(--vscode-list-hoverBackground)",
color: "var(--vscode-dropdown-foreground)",
}
: {
color: "var(--vscode-dropdown-foreground)",
}),
}}
onMouseEnter={() => isOptionSelectable(option) && setSelectedIndex(index)}>
onMouseEnter={() => {
// Only update hover state if mouse has actually moved
if (isOptionSelectable(option) && hasMouseMoved) {
setHoveredIndex(index)
}
}}
onMouseMove={handleMouseMove}
onMouseLeave={() => {
if (index === hoveredIndex) {
setHoveredIndex(null)
}
}}>
<div
style={{
display: "flex",