Files
gitea-mirror/src/components/ui/multi-select.tsx
Arunavo Ray 5f45a9a03d updates
2025-07-26 22:06:29 +05:30

137 lines
4.3 KiB
TypeScript

import * as React from "react"
import { X } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
interface MultiSelectProps {
options: { label: string; value: string }[]
selected: string[]
onChange: (selected: string[]) => void
placeholder?: string
className?: string
}
export function MultiSelect({
options,
selected,
onChange,
placeholder = "Select items...",
className,
}: MultiSelectProps) {
const [open, setOpen] = React.useState(false)
const handleUnselect = (item: string) => {
onChange(selected.filter((i) => i !== item))
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={`w-full justify-between ${selected.length > 0 ? "h-full" : ""} ${className}`}
>
<div className="flex gap-1 flex-wrap">
{selected.length > 0 ? (
selected.map((item) => (
<Badge
variant="secondary"
key={item}
className="mr-1 mb-1"
onClick={(e) => {
e.stopPropagation()
handleUnselect(item)
}}
>
{options.find((option) => option.value === item)?.label || item}
<button
className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
onKeyDown={(e) => {
if (e.key === "Enter") {
handleUnselect(item)
}
}}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleUnselect(item)
}}
>
<X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
</button>
</Badge>
))
) : (
<span className="text-muted-foreground">{placeholder}</span>
)}
</div>
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command className={className}>
<CommandInput placeholder="Search..." />
<CommandList>
<CommandEmpty>No item found.</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
onSelect={() => {
onChange(
selected.includes(option.value)
? selected.filter((item) => item !== option.value)
: [...selected, option.value]
)
setOpen(true)
}}
>
<div
className={`mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary ${
selected.includes(option.value)
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible"
}`}
>
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<span>{option.label}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}