Fix search input losing focus when hovering over dropdown items (Vibe Kanban) (#1966)

* Done! Here's a summary of the changes:

## Changes Made

### 1. `Dropdown.tsx` (lines 140-190)
- Added `preventFocusOnHover?: boolean` prop to `DropdownMenuItemProps` interface
- Added `onPointerMove` handler that calls `e.preventDefault()` when `preventFocusOnHover` is true, which stops Radix from stealing focus on hover

### 2. `SearchableDropdown.tsx` (line 110)
- Added `preventFocusOnHover` prop to `DropdownMenuItem`

### 3. `ProjectSelectorContainer.tsx` (lines 181, 208)
- Added `preventFocusOnHover` prop to both `DropdownMenuItem` components (the "Create new project" button and the virtualized project items)

The fix uses `e.preventDefault()` on `onPointerMove` to disable Radix's hover-to-focus behavior, as documented in [Radix UI Issue #2193](https://github.com/radix-ui/primitives/issues/2193). This preserves all other functionality (click, keyboard navigation, selection) while allowing the search input to retain focus when hovering over items.

* Done! I've added the `onPointerLeave` handler to `DropdownMenuItem` alongside the existing `onPointerMove` handler. Both now call `e.preventDefault()` when `preventFocusOnHover` is true.

**Summary of the fix:**
- Added `onPointerLeave` destructuring from props (line 161)
- Added `onPointerLeave` handler that calls `e.preventDefault()` when `preventFocusOnHover` is true (lines 187-194)

Per the [Radix UI Issue #2193](https://github.com/radix-ui/primitives/issues/2193), **both** `onPointerMove` and `onPointerLeave` need `preventDefault()` to fully disable hover-to-focus behavior.
This commit is contained in:
Louis Knight-Webb
2026-01-12 15:16:33 +00:00
committed by GitHub
parent 8dc07ee2b2
commit 6cb45f54c1
3 changed files with 24 additions and 0 deletions

View File

@@ -180,6 +180,7 @@ export function ProjectSelectorContainer({
<DropdownMenuItem
onSelect={handleCreateClick}
onMouseEnter={() => setHighlightedIndex(0)}
preventFocusOnHover
icon={PlusIcon}
className={cn(
'text-accent',
@@ -208,6 +209,7 @@ export function ProjectSelectorContainer({
<DropdownMenuItem
onSelect={() => handleSelect(item)}
onMouseEnter={() => setHighlightedIndex(idx + 1)}
preventFocusOnHover
className={cn(
isSelected && 'bg-secondary',
isHighlighted && 'bg-secondary'

View File

@@ -142,6 +142,8 @@ interface DropdownMenuItemProps
icon?: Icon;
badge?: string;
variant?: 'default' | 'destructive';
/** When true, prevents hover from stealing focus (useful for searchable dropdowns) */
preventFocusOnHover?: boolean;
}
const DropdownMenuItem = React.forwardRef<
@@ -154,6 +156,9 @@ const DropdownMenuItem = React.forwardRef<
icon: IconComponent,
badge,
variant = 'default',
preventFocusOnHover = false,
onPointerMove,
onPointerLeave,
children,
...props
},
@@ -171,6 +176,22 @@ const DropdownMenuItem = React.forwardRef<
variant === 'destructive' && 'text-error',
className
)}
onPointerMove={
preventFocusOnHover
? (e) => {
e.preventDefault();
onPointerMove?.(e);
}
: onPointerMove
}
onPointerLeave={
preventFocusOnHover
? (e) => {
e.preventDefault();
onPointerLeave?.(e);
}
: onPointerLeave
}
{...props}
>
{IconComponent && <IconComponent weight="bold" />}

View File

@@ -108,6 +108,7 @@ export function SearchableDropdown<T>({
<DropdownMenuItem
onSelect={() => onSelect(item)}
onMouseEnter={() => onHighlightedIndexChange(idx)}
preventFocusOnHover
badge={getItemBadge?.(item)}
className={cn(
isSelected && 'bg-secondary',