go-chi-oapi-codegen-todolist/frontend/components/multi-select.tsx
2025-04-20 15:58:52 +07:00

122 lines
4.5 KiB
TypeScript

"use client"
import * as React from "react"
import { X } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Command, CommandGroup, CommandItem } from "@/components/ui/command"
import { Command as CommandPrimitive } from "cmdk"
interface MultiSelectProps {
options: { label: string; value: string }[]
selected: string[]
onChange: (selected: string[]) => void
placeholder?: string
}
export function MultiSelect({ options, selected, onChange, placeholder = "Select options" }: MultiSelectProps) {
const inputRef = React.useRef<HTMLInputElement>(null)
const [open, setOpen] = React.useState(false)
const [inputValue, setInputValue] = React.useState("")
const handleUnselect = (value: string) => {
onChange(selected.filter((s) => s !== value))
}
const handleSelect = (value: string) => {
if (selected.includes(value)) {
onChange(selected.filter((s) => s !== value))
} else {
onChange([...selected, value])
}
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
const input = inputRef.current
if (input) {
if (e.key === "Delete" || e.key === "Backspace") {
if (input.value === "" && selected.length > 0) {
onChange(selected.slice(0, -1))
}
}
if (e.key === "Escape") {
input.blur()
}
}
}
const selectedOptions = options.filter((option) => selected.includes(option.value))
return (
<Command onKeyDown={handleKeyDown} className="overflow-visible bg-transparent">
<div className="group border rounded-md px-3 py-2 text-sm ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
<div className="flex flex-wrap gap-1">
{selectedOptions.map((option) => (
<Badge key={option.value} variant="secondary" className="rounded-sm">
{option.label}
<button
className="ml-1 rounded-full outline-none ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-2"
onKeyDown={(e) => {
if (e.key === "Enter") {
handleUnselect(option.value)
}
}}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onClick={() => handleUnselect(option.value)}
>
<X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
</button>
</Badge>
))}
<CommandPrimitive.Input
ref={inputRef}
value={inputValue}
onValueChange={setInputValue}
onBlur={() => setOpen(false)}
onFocus={() => setOpen(true)}
placeholder={selected.length === 0 ? placeholder : undefined}
className="ml-2 flex-1 bg-transparent outline-none placeholder:text-muted-foreground"
/>
</div>
</div>
<div className="relative mt-2">
{open && options.length > 0 && (
<div className="absolute top-0 z-10 w-full rounded-md border bg-popover text-popover-foreground shadow-md outline-none animate-in">
<CommandGroup className="h-full overflow-auto max-h-[200px]">
{options.map((option) => {
const isSelected = selected.includes(option.value)
return (
<CommandItem
key={option.value}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onSelect={() => handleSelect(option.value)}
className={`flex items-center gap-2 ${isSelected ? "bg-muted" : ""}`}
>
<div
className={`border mr-2 flex h-4 w-4 items-center justify-center rounded-sm ${
isSelected
? "bg-primary border-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible"
}`}
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
<span>{option.label}</span>
</CommandItem>
)
})}
</CommandGroup>
</div>
)}
</div>
</Command>
)
}