cmdk
cmdk copied to clipboard
[Bug] v1.1.0 force focuses the wrong input
Hi,
After upgrading to v1.1.0, I noticed that whenever you have 2 Command.Inputs inside one Command and you start typing in the second input, the first one gets incorrectly focused.
Below you can see how I have 2 inputs that both share an auto-complete popup (the content for the Command.Group is dynamic):
As soon as you start typing in the second input, the focus shifts to the top input. I traced the issue down to the change in #254, specifically these lines: https://github.com/pacocoursey/cmdk/blob/v1.1.0/cmdk/src/index.tsx#L244-L249
Here is the code for this component for reference:
import { useCallback, useEffect, useRef } from "react";
import { Command as CommandPrimitive } from "cmdk";
import { X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { CommandEmpty, CommandList } from "@/components/ui/command";
import {
Popover,
PopoverAnchor,
PopoverContent,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import type { Coordinates, Location } from "@/types";
type AutoCompleteInputProps = {
className?: string;
searchValue: string;
onSelected?: () => void;
onSearchValueChange?: (value: string) => void;
autoCompleteOpen: boolean;
setAutoCompleteOpen: (value: boolean) => void;
placeholder?: string;
};
const AutoCompleteInput = ({
className,
searchValue,
onSelected,
onSearchValueChange,
autoCompleteOpen,
setAutoCompleteOpen,
placeholder = "Zoeken...",
}: AutoCompleteInputProps) => {
const inputRef = useRef<HTMLInputElement | null>(null);
const onInputBlur = useCallback(
(e: React.FocusEvent<HTMLInputElement>) => {
if (!e.relatedTarget?.hasAttribute("cmdk-list")) {
setAutoCompleteOpen(false);
}
},
[setAutoCompleteOpen],
);
const openAutoCompleteWhenInputIsLongEnough = useCallback(() => {
onSelected?.();
setAutoCompleteOpen(searchValue.length > 1);
}, [searchValue, setAutoCompleteOpen, onSelected]);
const clearSearchValue = useCallback(() => {
onSearchValueChange?.("");
}, [onSearchValueChange]);
useEffect(() => {
// Automatically close the autocomplete popover when the user selects an item,
// only if the search value is long enough to open the autocomplete in the first place.
if (searchValue.length > 1 && !autoCompleteOpen) {
inputRef.current?.blur();
}
}, [searchValue, autoCompleteOpen]);
return (
<div className="flex w-full items-center">
<CommandPrimitive.Input
ref={inputRef}
asChild
value={searchValue}
onValueChange={(value) => {
// Update the value first to prevent any preservable lag in the controlled input
onSearchValueChange?.(value);
// Then, open the autocomplete if the input is long enough
setAutoCompleteOpen(value.length > 1);
}}
onKeyDown={(e) => {
if (e.key === "Escape") {
setAutoCompleteOpen(false);
}
}}
onMouseDown={openAutoCompleteWhenInputIsLongEnough}
onFocus={openAutoCompleteWhenInputIsLongEnough}
onBlur={onInputBlur}
>
<input
type="text"
className={cn(
"w-full truncate bg-transparent placeholder:text-muted-foreground focus-visible:outline-none",
className,
)}
placeholder={placeholder}
/>
</CommandPrimitive.Input>
<Button
variant="ghost"
size="icon"
className={`ml-2 h-auto w-auto cursor-pointer dark:hover:bg-transparent ${
searchValue.length === 0 ? "hidden" : ""
}`}
onClick={clearSearchValue}
>
<X className="stroke-muted-foreground size-5" />
</Button>
</div>
);
};
AutoCompleteInput.displayName = "AutoCompleteInput";
type AutoCompleteProps = {
className?: string;
open: boolean;
setOpen: (value: boolean) => void;
onValueChanged: (value: string) => void;
onLocationSelected: (location: Location) => void;
items: {
title: string;
subtitle: string;
value: string;
coordinates: Coordinates;
}[];
isLoading?: boolean;
loadingItemsCount?: number;
emptyMessage?: string;
children?: React.ReactNode;
};
const AutoComplete = ({
className,
open,
setOpen,
onValueChanged,
onLocationSelected,
items,
isLoading = false,
loadingItemsCount = 4,
emptyMessage = "Geen resultaten gevonden.",
children,
...props
}: AutoCompleteProps) => {
const onSelectItem = useCallback(
(value: string, coordinates: Coordinates) => {
onValueChanged(value);
onLocationSelected({
name: value,
longitude: coordinates.longitude,
latitude: coordinates.latitude,
});
setOpen(false);
},
[onValueChanged, onLocationSelected, setOpen],
);
return (
<Popover open={open} {...props}>
<CommandPrimitive loop shouldFilter={false}>
<PopoverAnchor asChild>{children}</PopoverAnchor>
{!open && <CommandList aria-hidden="true" className="hidden" />}
{open && (
<PopoverContent
onOpenAutoFocus={(e) => e.preventDefault()}
onInteractOutside={(e) => {
if (
e.target instanceof Element &&
e.target.hasAttribute("cmdk-input")
) {
e.preventDefault();
}
}}
className={cn(
"w-(--radix-popover-trigger-width) overflow-hidden rounded-lg border-none bg-white p-0 text-sm text-primary-foreground shadow-[0_0_0_2px_rgba(0,0,0,0.1)]",
className,
)}
>
<CommandList>
{isLoading ? (
<CommandPrimitive.Loading>
{[...Array(loadingItemsCount).keys()].map((key) => (
<div key={key} className="px-4 py-2">
<Skeleton className="mb-1 h-5 w-1/2 bg-muted/10" />
<Skeleton className="h-4 w-full bg-muted/10" />
</div>
))}
</CommandPrimitive.Loading>
) : items.length > 0 ? (
<CommandPrimitive.Group>
{items.map((option) => (
<CommandPrimitive.Item
key={option.value}
value={option.value}
onMouseDown={(e) => e.preventDefault()}
onSelect={() =>
onSelectItem(option.value, option.coordinates)
}
className="cursor-pointer px-4 py-2 data-[selected=true]:bg-gray-100"
>
<div className="font-semibold">{option.title}</div>
<div className="truncate">{option.subtitle}</div>
</CommandPrimitive.Item>
))}
</CommandPrimitive.Group>
) : (
<CommandEmpty className="p-4 text-center text-muted-foreground">
{emptyMessage}
</CommandEmpty>
)}
</CommandList>
</PopoverContent>
)}
</CommandPrimitive>
</Popover>
);
};
AutoComplete.displayName = "AutoComplete";
export { AutoComplete, AutoCompleteInput };
same here