Pastikan Anda telah menyiapkan proyek Next.js. Jika tidak, buatlah:
npx buat-aplikasi-berikutnya-aplikasi-saya --typescriptcd aplikasi-saya
Instal komponen shadcn yang diperlukan:
npx shadcn@init terbaru npx shadcn@latest tambahkan lencana pemisah tombol popover perintah
Buat multi-select.tsx
di direktori components
Anda:
// src/components/multi-select.tsximport * sebagai React dari "react";import { cva, ketik VariantProps } dari "class-variance-authority";import { PeriksaIkon, lingkaran X, ChevronTurun, XIkon, WandSparkles,} dari "lucide-react";import { cn } dari "@/lib/utils";import { Separator } dari "@/components/ui/separator";import { Button } dari "@/components/ui/ tombol";import { Lencana } dari "@/components/ui/badge";import { popover, Konten Popover, PopoverTrigger,} dari "@/components/ui/popover";import { Memerintah, PerintahKosong, Grup Komando, Masukan Perintah, Item Perintah, Daftar Perintah, CommandSeparator,} dari "@/components/ui/command";/** * Varian untuk komponen multi-pilih untuk menangani gaya yang berbeda. * Menggunakan otoritas varians kelas (cva) untuk menentukan gaya berbeda berdasarkan prop "varian". */const multiSelectVariant = cva( "m-1 transisi kemudahan penundaan masuk-keluar-150 hover:-translate-y-1 hover:skala-110 durasi-300", {varian: { varian: {default: "border-foreground/10 text-foreground bg-card hover:bg-card/80",secondary: "border-foreground/10 bg-secondary text-secondary-foreground hover:bg- sekunder/80",destruktif: "batas-transparan bg-destruktif teks-destruktif-latar depan hover:bg-destructive/80",terbalik: "terbalik", },},defaultVariants: { varian: "default",}, });/** * Props untuk komponen MultiSelect */interface MultiSelectProps extends React.ButtonHTMLAttributes<HTMLButtonElement>,VariantProps<typeof multiSelectVariants> { /** * Array objek opsi yang akan ditampilkan dalam komponen multi-pilih. * Setiap objek opsi memiliki label, nilai, dan ikon opsional. */ options: {/** Teks yang akan ditampilkan untuk opsi. */label: string;/** Nilai unik yang terkait dengan opsi. */value: string;/** Komponen ikon opsional untuk ditampilkan bersama opsi. */icon?: React.ComponentType<{ className?: string }>; }[]; /** * Fungsi panggilan balik dipicu ketika nilai yang dipilih berubah. * Menerima array nilai baru yang dipilih. */ onValueChange: (nilai: string[]) => batal; /** Nilai default yang dipilih saat komponen dipasang. */ Nilai default?: string[]; /** * Teks placeholder yang akan ditampilkan ketika tidak ada nilai yang dipilih. * Opsional, defaultnya adalah "Pilih opsi". */ pengganti?: string; /** * Durasi animasi dalam hitungan detik untuk efek visual (misal, lencana memantul). * Opsional, defaultnya adalah 0 (tanpa animasi). */ animasi?: nomor; /** * Jumlah maksimum item yang akan ditampilkan. Item tambahan yang dipilih akan diringkas. * Opsional, defaultnya adalah 3. */ maxCount?: nomor; /** * Modalitas popover. Jika disetel ke true, interaksi dengan elemen luar * akan dinonaktifkan dan hanya konten popover yang akan terlihat oleh pembaca layar. * Opsional, defaultnya adalah false. */ modalPopover?: boolean; /** * Jika benar, menjadikan komponen multi-pilih sebagai anak dari komponen lain. * Opsional, defaultnya adalah false. */ asChild?: boolean; /** * Nama kelas tambahan untuk menerapkan gaya khusus ke komponen multi-pilih. * Opsional, dapat digunakan untuk menambahkan gaya khusus. */ className?: string;}ekspor const MultiSelect = React.forwardRef< HTMLButtonElement, Alat Peraga MultiPilih>( ({ opsi, onValueChange, varian, defaultValue = [], placeholder = "Pilih opsi", animasi = 0, maxCount = 3, modalPopover = false, asChild = false, className, ...props},ref ) => {const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue);const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);const [isAnimating, setIsAnimating] = React.useState(false) ;const handleInputKeyDown = ( kejadian: React.KeyboardEvent<HTMLInputElement>) => { if (event.key === "Enter") {setIsPopoverOpen(true); } else if (event.key === "Backspace" && !event.currentTarget.value) {const newSelectedValues = [...selectedValues];newSelectedValues.pop();setSelectedValues(newSelectedValues);onValueChange(newSelectedValues); }};const toggleOption = (pilihan: string) => { const newSelectedValues = SelectValues.includes(pilihan)? selectValues.filter((value) => value !== pilihan): [...selectedValues, pilihan]; setSelectedValues(newSelectedValues); onValueChange(newSelectedValues);};const handleClear = () => { setSelectedValues([]); onValueChange([]);};const handleTogglePopover = () => { setIsPopoverOpen((prev) => !prev);};const clearExtraOptions = () => { const newSelectedValues = SelectValues.slice(0, maxCount); setSelectedValues(newSelectedValues); onValueChange(newSelectedValues);};const toggleAll = () => { if (selectedValues.length === options.length) {handleClear(); } else {const allValues = options.map((option) => option.value);setSelectedValues(allValues);onValueChange(allValues); }};return ( <Popoveropen={isPopoverOpen}onOpenChange={setIsPopoverOpen}modal={modalPopover} ><PopoverTrigger asChild> <Buttonref={ref}{...props}onClick={handleTogglePopover}className={cn( "flex w-penuh p-1 batas bulat-md min-h-10 h-pusat item otomatis justifikasi-antara bg-inherit hover:bg-inherit", className)} >{selectedValues.length > 0 ? ( <div className="flex justify-between items-center w-full"><div className="flex-wrap item-fleksibel- center"> {selectedValues.slice(0, maxCount).map((value) => {const option = options.find((o) => o.value === nilai);const IconComponent = opsi?.icon;return ( <Badgekey={value}className={cn( isAnimating ? "animate-bounce" : "", multiSelectVariants({ varian }))}style={{ animationDuration: ` ${animation}s` }} >{IconComponent && ( <IconComponent className="h-4 w-4 mr-2" />)}{option?.label<XCircle className="ml-2 h-4 w-4 penunjuk kursor" onClick={(event) => {event.stopPropagation();toggleOption(value }} /> </Badge>); })} {selectedValues.length > maxCount && (<Badge className={cn("bg-teks transparan di latar depan perbatasan-latar depan/1 hover:bg-transparan",isAnimating ? "animate-bounce" : "",multiSelectVariants({ varian }) )} style={{ animationDuration: `${animation}s` }}> {`+ ${selectedValues.length - maxCount} more`} <XCircleclassName= "ml-2 h-4 w-4 penunjuk kursor"onClick={(event) => { event.stopPropagation(); clearExtraOptions();}} /></Badge> )}</div><div className="flex items-center justify-between"> <XIconclassName="h-4 mx-2 kursor-penunjuk teks-dibungkam-latar depan "onClick={(acara) => { acara.stopPropagation(); handleClear();}} /> <Separatororientation="vertical"className="flex min-h-6 h-full" /> <ChevronDown className="h-4 mx-2 kursor-penunjuk teks-dibungkam-latar depan" / ></div> </div>) : ( <div className="flex items-center justify-between w-full mx-auto"><span className="text-sm text-muted-foreground mx-3"> {placeholder</span><ChevronDown className="h-4 kursor-pointer text-muted-foreground mx-2" /> </div>)} </Button>< /PopoverTrigger><PopoverContent className="w-auto p-0" align="start" onEscapeKeyDown={() => setIsPopoverOpen(false)}> <Command><CommandInput placeholder="Search..." onKeyDown={handleInputKeyDown}/><CommandList> <CommandEmpty>Tidak ada hasil yang ditemukan.</CommandEmpty> <CommandGroup><CommandItem key="all" onSelect={toggleAll} className ="penunjuk-kursor"> <divclassName={cn( "mr-2 flex h-4 w-4 items-center justify-center round-sm border border-primary",selectValues.length === options.length? "bg-primary text-primary-foreground": "opacity-50 [&_svg]:invisible")} >< CheckIcon className="h-4 w-4" /> </div> <span>(Pilih Semua)</span></CommandItem>{options.map((option) => { const isSelected = SelectValues.includes(option.value); kembali (<CommandItem key={option.value} onSelect={() => toggleOption(option.value)} className="cursor-pointer"> <divclassName={cn( "mr-2 flex h-4 w-4 item-pusat justify-center round-sm border border-primary", isSelected? "bg-primary text-primary-foreground": "opacity-50 [&_svg]:tidak terlihat")} ><CheckIcon className="h-4 w-4" /> </div> {option.icon && (<option.icon className="mr-2 h-4 w-4 teks -muted-foreground" /> )} <span>{option.label</span></CommandItem> );})} </CommandGroup> <CommandSeparator /> <CommandGroup><div className="flex items-center justify-between"> {selectedValues.length > 0 && (<> <CommandItemonSelect={handleClear}className="flex-1 justify-center kursor-pointer" >Hapus </ CommandItem> <Separatororientation="vertical"className="flex min-h-6 h-full" /></> )} <CommandItemonSelect={() => setIsPopoverOpen(false)}className="flex-1 justify-center kursor-pointer max-w-full" >Tutup </CommandItem></div> </CommandGroup></CommandList> < /Command></PopoverContent>{animasi > 0 &&selectValues.length > 0 && ( <WandSparklesclassName={cn( "penunjuk kursor my-2 teks-latar depan bg-latar belakang w-3 h-3", isAnimating ? "" : "text-muted-foreground")}onClick={() => setIsAnimating(!isAnimating)} />)} </Popover>); });MultiSelect.displayName =</sp