ui icon indicating copy to clipboard operation
ui copied to clipboard

We need <AutoComplete /> like MUI and other UI libraries.

Open omborda2002 opened this issue 1 year ago • 1 comments

Need of AutoComplete

omborda2002 avatar Jan 26 '24 05:01 omborda2002

Hi, you can build one with Combobox element. When you say AutoComplete you mean with server-side fetches right?

Here's mine for reference:

'use client';

import * as React from 'react';
import { Check, ChevronsUpDown } from 'lucide-react';
import { useDebouncedCallback } from 'use-debounce';

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';
import { ScrollArea } from './scroll-area';

export type ComboBoxItemType = {
  value: string;
  label: string;
};

type ComboboxProps = {
  value?: string;
  onSelect: (value: string | undefined) => void;
  items: ComboBoxItemType[];
  searchPlaceholder?: string;
  noResultsMsg?: string;
  selectItemMsg?: string;
  className?: string;
  unselect?: boolean;
  unselectMsg?: string;
  onSearchChange?: (e: string) => void;
  disabled?: boolean;
};

const popOverStyles = {
  width: 'var(--radix-popover-trigger-width)',
};

export function Combobox({
  value,
  onSelect,
  items,
  searchPlaceholder = 'Pesquisar item...',
  noResultsMsg = 'Nenhum item encontrado',
  selectItemMsg = 'Selecione um item',
  className,
  unselect = false,
  unselectMsg = 'Nenhum',
  onSearchChange,
  disabled = false,
}: ComboboxProps) {
  const [open, setOpen] = React.useState(false);

  const handleOnSearchChange = useDebouncedCallback((e: string) => {
    if (onSearchChange) {
      onSearchChange(e);
    }
  }, 300);

  return (
    <Popover open={open} onOpenChange={setOpen} modal={true}>
      <PopoverTrigger disabled={disabled} asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          className={cn('justify-between', className)}
        >
          {value
            ? items.find((item) => item.value === value)?.label
            : selectItemMsg}
          <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
        </Button>
      </PopoverTrigger>
      <PopoverContent
        style={popOverStyles}
        className="popover-content-width-same-as-its-trigger p-0"
      >
        <Command>
          <CommandInput
            placeholder={searchPlaceholder}
            onValueChange={handleOnSearchChange}
          />
          <ScrollArea className="max-h-[220px] overflow-auto">
            <CommandEmpty>{noResultsMsg}</CommandEmpty>
            <CommandGroup>
              {unselect && (
                <CommandItem
                  key="unselect"
                  value=""
                  onSelect={() => {
                    onSelect(undefined);
                    setOpen(false);
                  }}
                >
                  <Check
                    className={cn(
                      'mr-2 h-4 w-4',
                      value === '' ? 'opacity-100' : 'opacity-0',
                    )}
                  />
                  {unselectMsg}
                </CommandItem>
              )}
              {items.map((item) => (
                <CommandItem
                  key={item.value}
                  value={item.label}
                  onSelect={(currentValue) => {
                    onSelect(
                      currentValue === item.label.toLowerCase()
                        ? item.value
                        : '',
                    );
                    setOpen(false);
                  }}
                >
                  <Check
                    className={cn(
                      'mr-2 h-4 w-4',
                      value === item.value ? 'opacity-100' : 'opacity-0',
                    )}
                  />
                  {item.label}
                </CommandItem>
              ))}
            </CommandGroup>
          </ScrollArea>
        </Command>
      </PopoverContent>
    </Popover>
  );
}

And then you can use it like this if you like: PS: In my case, I extracted to a component because I use it multiple forms...

'use client';

import { list } from '@/actions/clients';
import { toaster } from '@/components/form/toaster';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { ComboBoxItemType, Combobox } from '../ui/combobox';
import { Label } from '../ui/label';

type ClientAutocompleteProps = {
  value?: string;
  disabled?: boolean;
  setClientId: Dispatch<SetStateAction<string | undefined>>;
  required?: boolean;
};

export default function ClientAutocomplete({
  value,
  disabled,
  setClientId,
  required = false,
}: ClientAutocompleteProps) {
  const [clients, setClients] = useState<ComboBoxItemType[]>([]);

  const handleClientSearchChanged = async (value: string) => {
    if (value === '' || value.length < 2) {
      return;
    }

    const response = await list(1, value);

    if (response.type === 'error') {
      toaster.send(response);
      return;
    }

    setClients(
      response.data?.data.map((client) => ({
        value: client.id,
        label: client.nomeFantasia,
      })) || [],
    );
  };

  useEffect(() => {
    if (value) {
      handleClientSearchChanged(value);
    }
  }, [value]);

  return (
    <>
      <Label required={required}>Cliente</Label>
      <Combobox
        disabled={disabled}
        value={value}
        items={clients}
        onSelect={(value) => setClientId(value)}
        selectItemMsg="Pesquise pelo cliente"
        searchPlaceholder="Pesquisar cliente..."
        onSearchChange={handleClientSearchChanged}
        {...(!required && { unselect: true, unselectMsg: 'Sem cliente' })}
      />
    </>
  );
}

mtnoronha avatar Jan 26 '24 10:01 mtnoronha

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 Feb 17 '24 23:02 shadcn

I agree, including an AutoComplete component would be great to have. The problem with the ComboBox solution is that you can only use existing items and can't add new items via an input field.

Thanks for all the great work!

dseipp avatar Feb 20 '24 14:02 dseipp

@dseipp You can make an option for like Add +whatever is in the text box which updates as the user types. If the add option is selected it can happen in the handler.

mi-na-bot avatar Mar 20 '24 23:03 mi-na-bot

I agree

samuelkarani avatar Apr 05 '24 16:04 samuelkarani

Please add the autocomplete to shadcn

romanticsoul avatar Apr 07 '24 15:04 romanticsoul

I suspect the timeline/existence of autocomplete in shadcn ui is closely related to this feature in radix https://github.com/radix-ui/primitives/issues/1342

mi-na-bot avatar Apr 08 '24 10:04 mi-na-bot

A combobox and autocomplete are 2 different types of components. Maybe @shadcn assumes they are the same thing, and thats why autocomplete does not exist in shadcn ui.

Using examples from mantine ui: https://mantine.dev/core/autocomplete https://mantine.dev/core/combobox

samuelkarani avatar Apr 08 '24 14:04 samuelkarani

This is not necessarily the case. They chose to split as two components, probably to reduce complexity, but there is only one Combobox aria pattern officialy: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/

This pattern covers both autocomplete and autosuggest combobox types.

In our Combobox, we have just a prop to allow the user "free typing" (not restricted only to select one/multiple value in the menu), which turns it into an "autosuggest": https://sparkui.vercel.app/?path=/docs/components-combobox--docs#custom-value-entry

Powerplex avatar May 24 '24 09:05 Powerplex