132 lines
3.7 KiB
TypeScript

import {
Field,
Label,
Combobox as HeadlessCombobox,
ComboboxInput,
ComboboxButton,
ComboboxOptions,
ComboboxOption,
} from '@headlessui/react'
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
import { useState, type ComponentProps, type ReactNode } from 'react'
import {
get,
type FieldError,
type FieldValues,
type Path,
type RegisterOptions,
Controller,
} from 'react-hook-form'
import { useRemixFormContext } from 'remix-hook-form'
import { twMerge } from 'tailwind-merge'
type TComboboxOption = {
code: string
name: string
id: string
}
type TComboboxProperties<T extends FieldValues> = ComponentProps<
typeof HeadlessCombobox
> & {
id: string
label?: ReactNode
name: Path<T>
rules?: RegisterOptions
placeholder?: string
options?: TComboboxOption[]
labelClassName?: string
containerClassName?: string
}
export const Combobox = <TFormValues extends Record<string, unknown>>(
properties: TComboboxProperties<TFormValues>,
) => {
const {
id,
label,
name,
rules,
disabled,
placeholder,
options,
className,
labelClassName,
containerClassName,
...restProperties
} = properties
const {
control,
formState: { errors },
} = useRemixFormContext()
const [query, setQuery] = useState('')
const filteredOptions =
query === ''
? options
: options?.filter((option) =>
option.name.toLowerCase().includes(query.toLowerCase()),
)
const error: FieldError = get(errors, name)
return (
<Field
className={twMerge('relative', containerClassName)}
disabled={disabled}
id={id}
>
<Label className={twMerge('mb-1 block text-gray-700', labelClassName)}>
{label} {error && <span className="text-red-500">{error.message}</span>}
</Label>
<Controller
name={name}
control={control}
rules={rules}
render={({ field }) => (
<HeadlessCombobox
value={field.value}
onChange={field.onChange}
disabled={disabled}
immediate
{...restProperties}
>
<div className="relative">
<ComboboxInput
placeholder={placeholder}
displayValue={(option: TComboboxOption) => option?.name}
onChange={(event) => setQuery(event.target.value)}
className={twMerge(
'focus:inheriten h-[42px] w-full rounded-md border border-[#DFDFDF] p-2',
className,
)}
/>
<ComboboxButton className="group absolute inset-y-0 right-0 px-2.5">
<ChevronDownIcon className="size-4 fill-gray-500" />
</ComboboxButton>
</div>
<ComboboxOptions
anchor={{ to: 'bottom', gap: '8px' }}
transition
className={twMerge(
'w-[var(--input-width)] rounded-md border border-[#DFDFDF] p-1 empty:invisible',
'bg-white transition duration-100 ease-in data-[leave]:data-[closed]:opacity-0',
)}
>
{filteredOptions?.map((option) => (
<ComboboxOption
key={option.id}
value={option}
className="group flex cursor-default items-center gap-2 rounded-lg px-3 py-1.5 select-none data-[focus]:bg-white/10"
>
<CheckIcon className="invisible size-4 group-data-[selected]:visible" />
<div className="text-sm/6">{option.name}</div>
</ComboboxOption>
))}
</ComboboxOptions>
</HeadlessCombobox>
)}
/>
</Field>
)
}