cmdk icon indicating copy to clipboard operation
cmdk copied to clipboard

[Bug] v1.1.0 force focuses the wrong input

Open Maher4Ever opened this issue 6 months ago • 1 comments

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):

Image

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 };

Maher4Ever avatar Jun 17 '25 09:06 Maher4Ever

same here

tigfamon avatar Jun 17 '25 09:06 tigfamon