ui icon indicating copy to clipboard operation
ui copied to clipboard

QUESTION: Change PopoverTrigger to Input or CommandInput

Open datnmqti opened this issue 2 years ago • 5 comments

I'm using Combobox, currently trigger is a button, i want replace trigger element with CommandInput or custom Input. Moving CommandInput out of Command seem like doesn't work.

Expection: My expectation is to have a component similar to Select, click on Input and enter and then there will be a suggestion option, it's similar to AutoComplete

import * as React from "react"
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
} from "@/components/ui/command"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

const frameworks = [
  {
    value: "next.js",
    label: "Next.js",
  },
  {
    value: "sveltekit",
    label: "SvelteKit",
  },
  {
    value: "nuxt.js",
    label: "Nuxt.js",
  },
  {
    value: "remix",
    label: "Remix",
  },
  {
    value: "astro",
    label: "Astro",
  },
]

export function ComboboxDemo() {
  const [open, setOpen] = React.useState(false)
  const [value, setValue] = React.useState("")

  return (
    <Popover open={open} onOpenChange={setOpen}>
      {/* <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          className="w-[200px] justify-between"
        >
          {value
            ? frameworks.find((framework) => framework.value === value)?.label
            : "Select framework..."}
          <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
        </Button> */}
        {/* Replace with Input or CommandInput */}
        <CommandInput placeholder="Search framework..." className="h-9" />
      </PopoverTrigger>
      <PopoverContent className="w-[200px] p-0">
        <Command>
          <CommandEmpty>No framework found.</CommandEmpty>
          <CommandGroup>
            {frameworks.map((framework) => (
              <CommandItem
                key={framework.value}
                onSelect={(currentValue) => {
                  setValue(currentValue === value ? "" : currentValue)
                  setOpen(false)
                }}
              >
                {framework.label}
                <CheckIcon
                  className={cn(
                    "ml-auto h-4 w-4",
                    value === framework.value ? "opacity-100" : "opacity-0"
                  )}
                />
              </CommandItem>
            ))}
          </CommandGroup>
        </Command>
      </PopoverContent>
    </Popover>
  )
}

datnmqti avatar Jul 29 '23 03:07 datnmqti

Hey, you can find an example here 🤌 I hope it will helps you

https://www.armand-salle.fr/post/autocomplete-select-shadcn-ui

armandsalle avatar Jul 30 '23 15:07 armandsalle

@armandsalle thank you, it's useful

datnmqti avatar Aug 05 '23 03:08 datnmqti

@armandsalle been searching for this everywhere. Thank you so much for saving me hours of work ahah

Filsommer avatar Oct 06 '23 17:10 Filsommer

Hey, you can find an example here 🤌 I hope it will helps you

https://www.armand-salle.fr/post/autocomplete-select-shadcn-ui

Originally posted by @armandsalle in https://github.com/shadcn-ui/ui/issues/1069#issuecomment-1657198760

eternity4318 avatar Dec 05 '23 02:12 eternity4318

@datnmqti were you able to move CommandInput under PopoverTrigger while using popover for multiselect. If so can you please share your code how you did that?

zahidiqbalnbs avatar May 16 '24 14:05 zahidiqbalnbs

This issue has been automatically closed because it received no activity for a while. If you think it was closed by accident, please leave a comment. Thank you.

shadcn avatar Jun 20 '24 23:06 shadcn

@zahidiqbalnbs FYI, I managed to use a normal Input outside a Popover and "bind" it to the Command & its CommandInput which lives inside the Popover.

The key is that the Input is inside a PopoverAnchor (not a PopoverTrigger) and that it relays all onKeyDown events, as well as its input to the Command & CommandInput (which is hidden in my case).

The onKeyDown & value relays are necessary for up/down navigation & filters of the Command to work.

I needed to wrap the Command inside a Popover b/c @armandsalle solution outlined above was giving me problems when being used inside a Sheet or Dialog. E.g. pressing Esc with the command open & focused, caused the whole dialog to close instead of just the command. Those issues seem to be solved by wrapping the Command inside a Popover.

My solution looks something like this:

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

type AutocompleteProps = {
  options: Option[];
  openSuggestionsAtInputLength?: number;
};

export const Autocomplete: React.FC<AutocompleteProps> = ({ options, openSuggestionsAtInputLength = 3 }) => {
  const [input, setInput] = useState('');
  const cmdInputRef = useRef<HTMLInputElement>(null);

  const [popoverIsOpen, setPopoverIsOpen] = useState(false);

  const onInputValueChange = (e: ChangeEvent<HTMLInputElement>): void => {
    const value = e.target.value;
    setInput(value);
    if (!popoverIsOpen && value.length >= openSuggestionsAtInputLength) {
      setPopoverIsOpen(true);
    }
  };

  /**
   * Pass all keydown events from the input to the `CommandInput` to provide navigation using up/down arrow keys etc.
   */
  const relayInputKeyDownToCommand = (e: React.KeyboardEvent<HTMLInputElement>): void => {
    const { key, code, bubbles } = e;
    cmdInputRef.current?.dispatchEvent(new KeyboardEvent('keydown', { key, code, bubbles }));

    if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
      e.preventDefault();
    }
  };

  return (
    <>
      <Popover open={popoverIsOpen} onOpenChange={setPopoverIsOpen}>
        <PopoverAnchor>
          <Input onKeyDown={relayInputKeyDownToCommand} value={input} onChange={onInputValueChange} />
        </PopoverAnchor>

        <PopoverContent onOpenAutoFocus={e => e.preventDefault()} className="p-1">
          <Command>
            <CommandInput ref={cmdInputRef} value={input} wrapperClassName={tw`hidden`} />
            <CommandList>
              <CommandEmpty>{`No suggetsions...`}</CommandEmpty>
              <CommandGroup>
                {options.map(x => (
                  <CommandItem
                    key={x.value}
                    onSelect={() => { /* TODO */ }}
                  >
                    {x.label}
                  </CommandItem>
                ))}
              </CommandGroup>
            </CommandList>
          </Command>
        </PopoverContent>
      </Popover>
    </>
  );
};

FYI, this is just a POC implementation which seems to be working just fine for my use cases but it might need some more adjustments down the road.

Also, @zahidiqbalnbs, I'm not sure whether this actually matches your requirements or not. E.g. you mentioned something about multi select which is no use case for me.

CombeeMike avatar Nov 28 '24 11:11 CombeeMike

hey all and @CombeeMike I find an easier solution to this problem by using Aria Kit https://ariakit.org/examples/combobox-radix with Radix 👍 works very well and need a lot less code

armandsalle avatar Nov 28 '24 13:11 armandsalle