ui icon indicating copy to clipboard operation
ui copied to clipboard

[bug]: Combo box is not working.

Open akeakun opened this issue 1 year ago • 10 comments

Describe the bug

the default code for combobox in a next.js app throws this error:

Unhandled Runtime Error TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))

Affected component/components

ComboBox

How to reproduce

just copy the default code for combo box from shadcnui.com/combobox and paste it in page.tsx run the code and the error should pop up while trying to click the PopoverTrigger button.

Codesandbox/StackBlitz link

No response

Logs

No response

System Info

browser

Before submitting

  • [ ] I've made research efforts and searched the documentation
  • [x] I've searched for existing issues

akeakun avatar May 10 '24 13:05 akeakun

me too. I use the example code in the popover anchor, maybe we should add the CommandList comp: image

QAQmmttyyy avatar May 11 '24 13:05 QAQmmttyyy

maybe the cmdk package has some changes and the shadcnui not response to.

QAQmmttyyy avatar May 11 '24 13:05 QAQmmttyyy

you need to use this version, v1 doesnt work

    "cmdk": "^0.2.1",

or

tailwind.config.ts

  plugins: [
    require("tailwindcss-animate"),
    function ({ addUtilities }) {
      addUtilities({
        ".popover-content-width-same-as-its-trigger": {
          width: "var(--radix-popover-trigger-width)",
          "max-height": "var(--radix-popover-content-available-height)",
        },
      });
    },
  ],

my-combobox.tsx


type MyComboboxProps = {
  label?: string;
  value: string;
  setValue: (value: string) => void;
  values: { label: string; value: string }[];
  inputDisabled?: boolean;
  inputPlaceholder?: string;
};

export const MyCombobox: React.FC<MyComboboxProps> = (props) => {
  const [open, setOpen] = useState(false);

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <div className="flex flex-col w-full">
        {props.label && <Label className="mb-1">{props.label}</Label>}
        <PopoverTrigger asChild>
          <Button
            variant="outline"
            role="combobox"
            aria-expanded={open}
            className=" justify-between"
          >
            {props.value
              ? props.values.find((item) => item.value === props.value)?.label
              : props.label + "..."}
            <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
          </Button>
        </PopoverTrigger>
      </div>
      <PopoverContent
        className={"popover-content-width-same-as-its-trigger p-0"}
      >
        <Command>
          {props.inputDisabled && (
            <CommandInput placeholder={props.inputPlaceholder} />
          )}
          <CommandEmpty>NOTHING_FOUND</CommandEmpty>
          <CommandGroup>
            <CommandList>
              {props.values.map((item) => (
                <CommandItem
                  key={item.value}
                  value={item.value}
                  onSelect={(currentValue) => {
                    props.setValue(
                      currentValue === props.value ? "" : currentValue
                    );
                    setOpen(false);
                  }}
                >
                  <Check
                    className={cn(
                      "mr-2 h-4 w-4",
                      props.value === item.value ? "opacity-100" : "opacity-0"
                    )}
                  />
                  {item.label}
                </CommandItem>
              ))}
            </CommandList>
          </CommandGroup>
        </Command>
      </PopoverContent>
    </Popover>
  );
};

0-don avatar May 11 '24 22:05 0-don

you need to use this version, v1 doesnt work

    "cmdk": "^0.2.1",

Great catch, once downgrading I no longer run into the issue.

Woofer21 avatar May 12 '24 16:05 Woofer21

Just wrap CommandGroup in a CommandList and it will work on latest cmdk. Move the CommandEmpty inside as well.

It should look like this: (just command, popover stuff removed for brevity)

      <Command>
        <CommandInput placeholder="Search ..." />
        <CommandList>
          <CommandEmpty>No option found.</CommandEmpty>
          <CommandGroup>
            {options.map((option) => {
              return (
                <CommandItem
                  key={option.value}
                  value={option.value}
                  onSelect={(currentValue) => {
                    setValue(currentValue === value ? "" : currentValue);
                    setOpen(false);
                    onValueChange(currentValue);
                  }}
                >
                  <CheckIcon
                    className={cn(
                      "mr-2 h-4 w-4",
                      value === option.value ? "opacity-100" : "opacity-0",
                    )}
                  />
                  {option.label}
                </CommandItem>
              );
            })}
          </CommandGroup>
        </CommandList>
      </Command>

HiggsWRX avatar May 12 '24 17:05 HiggsWRX

you need to use this version, v1 doesnt work

    "cmdk": "^0.2.1",

Weekend saved, nice catch

johncarmack1984 avatar May 12 '24 19:05 johncarmack1984

Yo bro, the command box may not be working for 2 reasons.

  1. You didnt wrap the CommantGroup or CommandItem with the CommandList, its required with the current relaease as of this date.
  2. The exact issue i had struggled with for over 6hours. Go to your Command component, and paste the code below to overwrite the default. Or simply replace every className with data-[disabled] to data-[disabled=true].

"use client";

import * as React from "react"; import { type DialogProps } from "@radix-ui/react-dialog"; import { MagnifyingGlassIcon } from "@radix-ui/react-icons"; import { Command as CommandPrimitive } from "cmdk";

import { cn } from "@/lib/utils"; import { Dialog, DialogContent } from "@/components/ui/dialog";

const Command = React.forwardRef< React.ElementRef<typeof CommandPrimitive>, React.ComponentPropsWithoutRef<typeof CommandPrimitive>

(({ className, ...props }, ref) => ( <CommandPrimitive ref={ref} className={cn( "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", className )} {...props} /> )); Command.displayName = CommandPrimitive.displayName;

interface CommandDialogProps extends DialogProps {}

const CommandDialog = ({ children, ...props }: CommandDialogProps) => { return ( <Dialog {...props}> <DialogContent className="overflow-hidden p-0"> <Command className="[&[cmdk-group-heading]]:px-2 [&[cmdk-group-heading]]:font-medium [&[cmdk-group-heading]]:text-muted-foreground [&[cmdk-group]:not([hidden])~[cmdk-group]]:pt-0 [&[cmdk-group]]:px-2 [&[cmdk-input-wrapper]svg]:h-5 [&[cmdk-input-wrapper]svg]:w-5 [&[cmdk-input]]:h-12 [&[cmdk-item]]:px-2 [&[cmdk-item]]:py-3 [&[cmdk-item]svg]:h-5 [&[cmdk-item]_svg]:w-5"> {children} </Command> </DialogContent> </Dialog> ); };

const CommandInput = React.forwardRef< React.ElementRef<typeof CommandPrimitive.Input>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>

(({ className, ...props }, ref) => (

));

CommandInput.displayName = CommandPrimitive.Input.displayName;

const CommandList = React.forwardRef< React.ElementRef<typeof CommandPrimitive.List>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>

(({ className, ...props }, ref) => ( <CommandPrimitive.List ref={ref} className={cn( "max-h-[300px] overflow-y-auto overflow-x-hidden", className )} {...props} /> ));

CommandList.displayName = CommandPrimitive.List.displayName;

const CommandEmpty = React.forwardRef< React.ElementRef<typeof CommandPrimitive.Empty>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>

((props, ref) => ( <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} /> ));

CommandEmpty.displayName = CommandPrimitive.Empty.displayName;

const CommandGroup = React.forwardRef< React.ElementRef<typeof CommandPrimitive.Group>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>

(({ className, ...props }, ref) => ( <CommandPrimitive.Group ref={ref} className={cn( "overflow-hidden p-1 text-foreground [&[cmdk-group-heading]]:px-2 [&[cmdk-group-heading]]:py-1.5 [&[cmdk-group-heading]]:text-xs [&[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", className )} {...props} /> ));

CommandGroup.displayName = CommandPrimitive.Group.displayName;

const CommandSeparator = React.forwardRef< React.ElementRef<typeof CommandPrimitive.Separator>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>

(({ className, ...props }, ref) => ( <CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} /> )); CommandSeparator.displayName = CommandPrimitive.Separator.displayName;

const CommandItem = React.forwardRef< React.ElementRef<typeof CommandPrimitive.Item>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>

(({ className, ...props }, ref) => ( <CommandPrimitive.Item ref={ref} className={cn( "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50", className )} {...props} /> ));

CommandItem.displayName = CommandPrimitive.Item.displayName;

const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { return ( <span className={cn( "ml-auto text-xs tracking-widest text-muted-foreground", className )} {...props} /> ); }; CommandShortcut.displayName = "CommandShortcut";

export { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator, };

DamifeZion avatar May 13 '24 09:05 DamifeZion

Just wrap CommandGroup in a CommandList and it will work on latest cmdk. Move the CommandEmpty inside as well.

It should look like this: (just command, popover stuff removed for brevity)

      <Command>
        <CommandInput placeholder="Search ..." />
        <CommandList>
          <CommandEmpty>No option found.</CommandEmpty>
          <CommandGroup>
            {options.map((option) => {
              return (
                <CommandItem
                  key={option.value}
                  value={option.value}
                  onSelect={(currentValue) => {
                    setValue(currentValue === value ? "" : currentValue);
                    setOpen(false);
                    onValueChange(currentValue);
                  }}
                >
                  <CheckIcon
                    className={cn(
                      "mr-2 h-4 w-4",
                      value === option.value ? "opacity-100" : "opacity-0",
                    )}
                  />
                  {option.label}
                </CommandItem>
              );
            })}
          </CommandGroup>
        </CommandList>
      </Command>

Thanks man.

Olalexy1 avatar May 18 '24 15:05 Olalexy1

For me, the issue still exists. No solution here worked, neither upgrading cmdk nor wrapping CommandGroup in a CommandList. Has anyone another impuls? What about this PR? 🤔

image

qhantom avatar Jul 03 '24 14:07 qhantom

For me, the issue still exists. No solution here worked, neither upgrading cmdk nor wrapping CommandGroup in a CommandList. Has anyone another impuls? What about this PR? 🤔

image

I HAVE FIXED ALL THESE ISSUES... SIMPLY COPY AND PASTE THE BELOW ADVANCED COMBOBOX👇

`"use client"; import { useState, useEffect, useRef } from "react"; import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";

import { cn } from "@/utils/base_utils"; import { Button } from "./button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "./command"; import { Popover, PopoverContent, PopoverTrigger } from "./popover"; import { Drawer, DrawerContent, DrawerTrigger } from "./drawer"; import { useMediaScreen } from "src/hooks/useMediaScreen"; import { ScrollArea } from "./scroll-area";

// Define a function to convert a string or number to an object with label and value function normalizeItem(item) { if (typeof item === "string" || typeof item === "number") { return { label: item.toString(), value: item.toString().toLowerCase() }; } return { label: item.label, value: item.value.toLowerCase() }; }

export function ComboBox({ array: originalArray, //NOTE: The array of items to display in the dropdown defaultValue, //NOTE: This will be used if provided as the initial placeholder, //NOTE: The placeholder text allowSearch = false, //NOTE: Whether to include a search input in the dropdown onValueChange, //NOTE: A callback function that is called when a new value is selected side, triggerClassName, popoverContentClassName, drawerContentClassName, commandItemClassName, iconLeft, iconRight, }) { const array = originalArray.map(normalizeItem); // Convert the array items to objects const isMobile = useMediaScreen({ breakpoint: "md" }); const [open, setOpen] = useState(false); //NOTE: Whether the dropdown is open const popoverTriggerRef = useRef(null); const [currentValue, setCurrentValue] = useState(""); //NOTE: The currently selected value const prevValueRef = useRef(); const defaultValueRef = useRef(defaultValue);

// NOTE: Pass the curent value to be accessible by parent component for use. useEffect(() => { prevValueRef.current = currentValue; });

const prevValue = prevValueRef.current;

useEffect(() => { if (prevValue !== currentValue && onValueChange) { onValueChange(currentValue ? currentValue : ""); } }, [currentValue, onValueChange, prevValue]);

useEffect(() => { defaultValueRef.current = defaultValue; }, [defaultValue]);

useEffect(() => { if (defaultValueRef.current) { // NOTE: If there is a default value we must cross check to make sure the default value is amongst the array as a value, else we throw error const valueExists = array.find( (item) => item.value === defaultValueRef.current );

     if (!valueExists) {
        throw new Error(
           `The default value "${
              defaultValueRef.current
           }" does not exist in the provided array. The array contains the following values: ${array
              .map((item) => item.value)
              .join(", ")}`
        );
     } else {
        setCurrentValue(defaultValueRef.current);
     }
  }

}, [array]);

//NOTE: Render the mobile version of the dropdown if the screen width is small if (isMobile) { return ( <Drawer open={open} onOpenChange={setOpen}> <DrawerTrigger asChild> <Button variant="outline" onClick={(e) => e.stopPropagation()} className={cn("w-full justify-between gap-2", triggerClassName)} > {iconLeft && iconLeft}

              {currentValue ? (
                 array.find((item) => item.value === currentValue)?.label
              ) : (
                 <span className="text-muted-foreground">
                    {placeholder && placeholder}
                 </span>
              )}

              <span className="w-4 h-4 shrink-0 [&_svg]:w-full [&_svg]:h-full">
                 {iconRight ? iconRight : <CaretSortIcon />}
              </span>
           </Button>
        </DrawerTrigger>

        <DrawerContent className={drawerContentClassName}>
           <div id="wrapper" className="mt-4 border-t">
              <SelectList
                 currentValue={currentValue}
                 array={array}
                 placeholder={placeholder}
                 allowSearch={allowSearch}
                 setOpen={setOpen}
                 setSelected={setCurrentValue}
                 commandItemClassName={commandItemClassName}
              />
           </div>
        </DrawerContent>
     </Drawer>
  );

}

//NOTE: Render the desktop version of the dropdown return ( <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> <Button ref={popoverTriggerRef} variant="outline" role="combobox" onClick={(e) => e.stopPropagation()} aria-expanded={open} className={cn("w-full justify-between gap-2", triggerClassName)} > {iconLeft && iconLeft}

           {currentValue ? (
              array.find((item) => item.value === currentValue)?.label
           ) : (
              <span className="text-muted-foreground">
                 {placeholder && placeholder}
              </span>
           )}

           <span className="w-4 h-4 shrink-0 [&_svg]:w-full [&_svg]:h-full">
              {iconRight ? iconRight : <CaretSortIcon />}
           </span>
        </Button>
     </PopoverTrigger>

     <PopoverContent
        side={side}
        align="start"
        style={{
           width: popoverTriggerRef.current
              ? `${popoverTriggerRef.current.offsetWidth}px`
              : "auto",
        }}
        className={cn("p-0", popoverContentClassName)}
     >
        <ScrollArea>
           <SelectList
              currentValue={currentValue}
              array={array}
              placeholder={placeholder}
              allowSearch={allowSearch}
              setOpen={setOpen}
              setSelected={setCurrentValue}
              commandItemClassName={commandItemClassName}
           />
        </ScrollArea>
     </PopoverContent>
  </Popover>

); }

//NOTE: The SelectList component, used to render the list of items in the dropdown const SelectList = ({ currentValue, //NOTE: The currently selected value setOpen, //NOTE: A function to open or close the dropdown setSelected, //NOTE: A function to set the selected value array, //NOTE: The array of items to display in the dropdown allowSearch, //NOTE: Whether to include a search input in the dropdown placeholder, //NOTE: The placeholder text commandItemClassName, renderItem, //NOTE: A function to render each item in the dropdown as jsx }) => { return ( <Command className="w-full"> {allowSearch && ( <> <CommandInput placeholder={placeholder} className="h-9" /> </> )}

     <CommandList>
        <CommandEmpty>No item found.</CommandEmpty>

        <CommandGroup>
           {array.map((item) => (
              <CommandItem
                 key={item.value}
                 value={item.value}
                 onSelect={(value) => {
                    setSelected(currentValue === value ? "" : value);
                    setOpen(false);
                 }}
                 className={cn("py-2 cursor-pointer", commandItemClassName)}
              >
                 {/*NOTE: Use renderItem if it's provided, else use label if it's available, else use value */}
                 {renderItem ? renderItem(item) : item.label || item.value}

                 <CheckIcon
                    className={cn(
                       "ml-auto size-5",
                       currentValue === item.value
                          ? "opacity-100"
                          : "opacity-0"
                    )}
                 />
              </CommandItem>
           ))}
        </CommandGroup>
     </CommandList>
  </Command>

); }; `

DamifeZion avatar Jul 04 '24 10:07 DamifeZion

you need to use this version, v1 doesnt work

    "cmdk": "^0.2.1",

or

tailwind.config.ts

  plugins: [
    require("tailwindcss-animate"),
    function ({ addUtilities }) {
      addUtilities({
        ".popover-content-width-same-as-its-trigger": {
          width: "var(--radix-popover-trigger-width)",
          "max-height": "var(--radix-popover-content-available-height)",
        },
      });
    },
  ],

my-combobox.tsx

type MyComboboxProps = {
  label?: string;
  value: string;
  setValue: (value: string) => void;
  values: { label: string; value: string }[];
  inputDisabled?: boolean;
  inputPlaceholder?: string;
};

export const MyCombobox: React.FC<MyComboboxProps> = (props) => {
  const [open, setOpen] = useState(false);

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <div className="flex flex-col w-full">
        {props.label && <Label className="mb-1">{props.label}</Label>}
        <PopoverTrigger asChild>
          <Button
            variant="outline"
            role="combobox"
            aria-expanded={open}
            className=" justify-between"
          >
            {props.value
              ? props.values.find((item) => item.value === props.value)?.label
              : props.label + "..."}
            <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
          </Button>
        </PopoverTrigger>
      </div>
      <PopoverContent
        className={"popover-content-width-same-as-its-trigger p-0"}
      >
        <Command>
          {props.inputDisabled && (
            <CommandInput placeholder={props.inputPlaceholder} />
          )}
          <CommandEmpty>NOTHING_FOUND</CommandEmpty>
          <CommandGroup>
            <CommandList>
              {props.values.map((item) => (
                <CommandItem
                  key={item.value}
                  value={item.value}
                  onSelect={(currentValue) => {
                    props.setValue(
                      currentValue === props.value ? "" : currentValue
                    );
                    setOpen(false);
                  }}
                >
                  <Check
                    className={cn(
                      "mr-2 h-4 w-4",
                      props.value === item.value ? "opacity-100" : "opacity-0"
                    )}
                  />
                  {item.label}
                </CommandItem>
              ))}
            </CommandList>
          </CommandGroup>
        </Command>
      </PopoverContent>
    </Popover>
  );
};

Nice one. Downgraded the cmdk and it worked. Thank you

AdrianFred avatar Jul 09 '24 12:07 AdrianFred

Downgrading to "cmdk": "^0.2.1"

Worked 👍 Thanks ❤️

qhantom avatar Jul 09 '24 13:07 qhantom