pacocoursey / cmdk

Fast, unstyled command menu React component.
https://cmdk.paco.me
MIT License
9.03k stars 258 forks source link

correctly updated to V1, still getting not iteratable error #263

Open ImamJanjua opened 1 month ago

ImamJanjua commented 1 month ago

Hi guys, I correctly updated to v1 so changed the classes and added CommandList and CommandEmpty to the component. Here is a implementation of a autocomplete but on select getting the error.

Probably just a small thing but can't catch the error. Help would be 🔥🔥

import { useState, useRef, useCallback, type KeyboardEvent } from "react";
import { Command as CommandPrimitive } from "cmdk";

import { cn } from "@/lib/cn";

import { Skeleton } from "@/components/ui/Skeleton";
import {
  CommandGroup,
  CommandItem,
  CommandList,
  CommandInput,
} from "@/components/ui/Command";
import { Check as CheckIcon } from "lucide-react";

export type Option = Record<"value" | "label", string> & Record<string, string>;

type AutoCompleteProps = {
  options: Option[];
  emptyMessage: string;
  value?: Option;
  onValueChange?: (value: Option) => void;
  isLoading?: boolean;
  disabled?: boolean;
  placeholder?: string;
};

const AutoComplete = ({
  options,
  emptyMessage,
  value,
  onValueChange,
  isLoading = false,
  disabled,
  placeholder,
}: AutoCompleteProps) => {
  const [isOpen, setOpen] = useState(false);
  const [selected, setSelected] = useState<Option>(value as Option);
  const [inputValue, setInputValue] = useState<string>(value?.label || "");
  const inputRef = useRef<HTMLInputElement>(null);

  const handleKeyDown = useCallback(
    (event: KeyboardEvent<HTMLDivElement>) => {
      const input = inputRef.current;
      if (!input) {
        return;
      }

      // Keep the options displayed when the user is typing
      if (!isOpen) {
        setOpen(true);
      }

      // This is not a default behaviour of the <input /> field
      if (event.key === "Enter" && input.value !== "") {
        const optionToSelect = options.find(
          (option) => option.label === input.value,
        );
        if (optionToSelect) {
          setSelected(optionToSelect);
          onValueChange?.(optionToSelect);
        }
      }

      if (event.key === "Escape") {
        input.blur();
      }
    },
    [isOpen, options, onValueChange],
  );

  const handleBlur = useCallback(() => {
    setOpen(false);
    setInputValue(selected?.label);
  }, [selected]);

  const handleSelectOption = useCallback(
    (selectedOption: Option) => {
      setInputValue(selectedOption.label);

      setSelected(selectedOption);
      onValueChange?.(selectedOption);

      // This is a hack to prevent the input from being focused after the user selects an option
      // We can call this hack: "The next tick"
      setTimeout(() => {
        inputRef?.current?.blur();
      }, 0);
    },
    [onValueChange],
  );

  return (
    <CommandPrimitive onKeyDown={handleKeyDown}>
      <div>
        <CommandInput
          ref={inputRef}
          value={inputValue}
          onValueChange={isLoading ? undefined : setInputValue}
          onBlur={handleBlur}
          onFocus={() => setOpen(true)}
          placeholder={placeholder}
          disabled={disabled}
          className="text-base"
        />
      </div>
      <div className="relative mt-1">
        {isOpen ? (
          <div className="absolute top-0 z-10 w-full rounded-xl bg-stone-50 outline-none animate-in fade-in-0 zoom-in-95">
            <CommandList className="rounded-lg ring-1 ring-slate-200">
              {isLoading ? (
                <CommandPrimitive.Loading>
                  <div className="p-1">
                    <Skeleton className="h-8 w-full" />
                  </div>
                </CommandPrimitive.Loading>
              ) : null}
              {options.length > 0 && !isLoading ? (
                <CommandGroup>
                  {options.map((option) => {
                    const isSelected = selected?.value === option.value;
                    return (
                      <CommandItem
                        key={option.value}
                        value={option.label}
                        onMouseDown={(event) => {
                          event.preventDefault();
                          event.stopPropagation();
                        }}
                        onSelect={() => handleSelectOption(option)}
                        className={cn(
                          "flex w-full items-center gap-2",
                          !isSelected ? "pl-8" : null,
                        )}
                      >
                        {isSelected ? <CheckIcon className="w-4" /> : null}
                        {option.label}
                      </CommandItem>
                    );
                  })}
                </CommandGroup>
              ) : null}
              {!isLoading ? (
                <CommandPrimitive.Empty className="select-none rounded-sm px-2 py-3 text-center text-sm">
                  {emptyMessage}
                </CommandPrimitive.Empty>
              ) : null}
            </CommandList>
          </div>
        ) : null}
      </div>
    </CommandPrimitive>
  );
};

export { AutoComplete };

can be used like like âž”


const FRAMEWORKS = [
  {
    value: "next.js",
    label: "Next.js",
  },
  {
    value: "sveltekit",
    label: "SvelteKit",
  },
  {
    value: "nuxt.js",
    label: "Nuxt.js",
  },
];

  <AutoComplete
        options={FRAMEWORKS}
        emptyMessage="No resulsts."
      />
hoop71 commented 1 month ago

@ImamJanjua <CommandList /> needs to be present always, not conditionally as you have shown here. I solution I used was using CSS to make it hidden vs. removing it from the DOM.

darenmalfait commented 1 month ago

@hoop71 if using Command inside a Popover, Dialog or similar, you get that error. What would be a way to make that work?

hoop71 commented 1 month ago

@darenmalfait Can you provide a little more information there? I'm able to use them inside those components successfully. Here is a working example: https://ui.shadcn.com/docs/components/combobox

clinically-au commented 1 month ago

To be fair, many of the examples on the shadcn page don't work because they don't use <CommandList> at all - and that's when this error occurs. @darenmalfait if you wrap all the <CommandItem> tags in a <CommandList> it should work.

darenmalfait commented 1 month ago

@clinically-au Great feedback, thanks!