Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
ImamJanjua opened this issue May 21, 2024 · 6 comments
Open

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

ImamJanjua opened this issue May 21, 2024 · 6 comments

Comments

@ImamJanjua
Copy link

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
Copy link

hoop71 commented May 21, 2024

@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
Copy link

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

@hoop71
Copy link

hoop71 commented May 23, 2024

@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
Copy link

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
Copy link

@clinically-au Great feedback, thanks!

@joseinaqa
Copy link

But why not include this commit that fixes this issue in a new release? Besides, this issue suggests that we can render List component independently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants