Skip to content

[Bug] v1.1.0 force focuses the wrong input #368

@Maher4Ever

Description

@Maher4Ever

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions