change searchable select for shadcn combobox

This commit is contained in:
Victor Oliva
2024-11-08 07:07:06 +03:00
parent 041e5bb6d5
commit 13490134a6
2 changed files with 85 additions and 60 deletions

View File

@@ -1,13 +1,15 @@
import { cn } from "@/utils/cn"
import { Check, ChevronsUpDown } from "lucide-react"
import { useState } from "react"
import { twMerge as clsx } from "tailwind-merge"
import { ChevronDown, CheckIcon } from "lucide-react"
import { Button } from "./ui/button"
import {
Combobox,
ComboboxInput,
ComboboxOption,
ComboboxOptions,
ComboboxButton,
} from "@headlessui/react"
Command,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "./ui/command"
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"
export const SearchableSelect = <T,>({
value,
@@ -18,60 +20,54 @@ export const SearchableSelect = <T,>({
setValue: (val: T | null) => void
options: { value: T; text: string }[]
}) => {
const [query, setQuery] = useState("")
const filteredOptions =
query === ""
? options
: options.filter((option) => {
return option.text.toLowerCase().includes(query.toLowerCase())
})
const [open, setOpen] = useState(false)
return (
<>
<Combobox
value={value}
onChange={(value) => setValue(value)}
onClose={() => setQuery("")}
immediate
>
<div className="flex flex-row px-4 py-2 border border-slate-500 text-white bg-transparent">
<ComboboxInput
className="text-ellipsis overflow-hidden whitespace-nowrap focus:outline-none select-all"
displayValue={(option: T) =>
options.find((_option) => _option.value === option)?.text ?? ""
}
onChange={(event) => setQuery(event.target.value)}
placeholder="Select"
autoComplete="off"
/>
<ComboboxButton className="group">
<ChevronDown className="size-4 " />
</ComboboxButton>
</div>
<ComboboxOptions
anchor="bottom start"
transition
className={clsx(
"max-h-96 rounded bg-black p-1 empty:invisible mt-2 -ml-4",
"transition duration-100 ease-in data-[leave]:data-[closed]:opacity-0",
)}
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="flex w-52 justify-between overflow-hidden"
>
{filteredOptions
.sort((a, b) => a.text.localeCompare(b.text))
.map((option, idx) => (
<ComboboxOption
key={idx}
value={option.value}
className="group flex cursor-default items-center gap-2 rounded py-1.5 px-3 select-none data-[focus]:bg-white/10"
>
<CheckIcon className="invisible size-4 group-data-[selected]:visible" />
<div className="text-sm text-white">{option.text}</div>
</ComboboxOption>
))}
</ComboboxOptions>
</Combobox>
</>
{value ? (
<span className="text-ellipsis overflow-hidden">
{options.find((option) => option.value === value)?.text}
</span>
) : (
<span className="opacity-80">Select</span>
)}
<ChevronsUpDown className="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Filter…" />
<CommandList>
<CommandGroup>
{options.map((option, i) => (
<CommandItem
key={i}
value={option.text}
onSelect={() => {
setValue(option.value)
setOpen(false)
}}
>
{option.text}
<Check
className={cn(
"ml-auto",
value === option.value ? "opacity-100" : "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}

View File

@@ -0,0 +1,29 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border border-neutral-200 bg-white p-4 text-neutral-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }