|
'use client' |
|
import type { FC } from 'react' |
|
import React, { Fragment, useEffect, useState } from 'react' |
|
import { Combobox, Listbox, Transition } from '@headlessui/react' |
|
import classNames from 'classnames' |
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/20/solid' |
|
|
|
const defaultItems = [ |
|
{ value: 1, name: 'option1' }, |
|
{ value: 2, name: 'option2' }, |
|
{ value: 3, name: 'option3' }, |
|
{ value: 4, name: 'option4' }, |
|
{ value: 5, name: 'option5' }, |
|
{ value: 6, name: 'option6' }, |
|
{ value: 7, name: 'option7' }, |
|
] |
|
|
|
export type Item = { |
|
value: number | string |
|
name: string |
|
} |
|
|
|
export type ISelectProps = { |
|
className?: string |
|
items?: Item[] |
|
defaultValue?: number | string |
|
disabled?: boolean |
|
onSelect: (value: Item) => void |
|
allowSearch?: boolean |
|
bgClassName?: string |
|
} |
|
const Select: FC<ISelectProps> = ({ |
|
className, |
|
items = defaultItems, |
|
defaultValue = 1, |
|
disabled = false, |
|
onSelect, |
|
allowSearch = true, |
|
bgClassName = 'bg-gray-100', |
|
}) => { |
|
const [query, setQuery] = useState('') |
|
const [open, setOpen] = useState(false) |
|
|
|
const [selectedItem, setSelectedItem] = useState<Item | null>(null) |
|
useEffect(() => { |
|
let defaultSelect = null |
|
const existed = items.find((item: Item) => item.value === defaultValue) |
|
if (existed) |
|
defaultSelect = existed |
|
|
|
setSelectedItem(defaultSelect) |
|
}, [defaultValue]) |
|
|
|
const filteredItems: Item[] |
|
= query === '' |
|
? items |
|
: items.filter((item) => { |
|
return item.name.toLowerCase().includes(query.toLowerCase()) |
|
}) |
|
|
|
return ( |
|
<Combobox |
|
as="div" |
|
disabled={disabled} |
|
value={selectedItem} |
|
className={className} |
|
onChange={(value: Item) => { |
|
if (!disabled) { |
|
setSelectedItem(value) |
|
setOpen(false) |
|
onSelect(value) |
|
} |
|
}}> |
|
<div className={classNames('relative')}> |
|
<div className='group text-gray-800'> |
|
{allowSearch |
|
? <Combobox.Input |
|
className={`w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-not-allowed`} |
|
onChange={(event) => { |
|
if (!disabled) |
|
setQuery(event.target.value) |
|
}} |
|
displayValue={(item: Item) => item?.name} |
|
/> |
|
: <Combobox.Button onClick={ |
|
() => { |
|
if (!disabled) |
|
setOpen(!open) |
|
} |
|
} className={`flex items-center h-9 w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200`}> |
|
{selectedItem?.name} |
|
</Combobox.Button>} |
|
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none group-hover:bg-gray-200" onClick={ |
|
() => { |
|
if (!disabled) |
|
setOpen(!open) |
|
} |
|
}> |
|
{open ? <ChevronUpIcon className="h-5 w-5" /> : <ChevronDownIcon className="h-5 w-5" />} |
|
</Combobox.Button> |
|
</div> |
|
|
|
{filteredItems.length > 0 && ( |
|
<Combobox.Options className="absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm"> |
|
{filteredItems.map((item: Item) => ( |
|
<Combobox.Option |
|
key={item.value} |
|
value={item} |
|
className={({ active }: { active: boolean }) => |
|
classNames( |
|
'relative cursor-default select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700', |
|
active ? 'bg-gray-100' : '', |
|
) |
|
} |
|
> |
|
{({ /* active, */ selected }) => ( |
|
<> |
|
<span className={classNames('block truncate', selected && 'font-normal')}>{item.name}</span> |
|
{selected && ( |
|
<span |
|
className={classNames( |
|
'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700', |
|
)} |
|
> |
|
<CheckIcon className="h-5 w-5" aria-hidden="true" /> |
|
</span> |
|
)} |
|
</> |
|
)} |
|
</Combobox.Option> |
|
))} |
|
</Combobox.Options> |
|
)} |
|
</div> |
|
</Combobox > |
|
) |
|
} |
|
|
|
const SimpleSelect: FC<ISelectProps> = ({ |
|
className, |
|
items = defaultItems, |
|
defaultValue = 1, |
|
disabled = false, |
|
onSelect, |
|
}) => { |
|
const [selectedItem, setSelectedItem] = useState<Item | null>(null) |
|
useEffect(() => { |
|
let defaultSelect = null |
|
const existed = items.find((item: Item) => item.value === defaultValue) |
|
if (existed) |
|
defaultSelect = existed |
|
|
|
setSelectedItem(defaultSelect) |
|
}, [defaultValue]) |
|
|
|
return ( |
|
<Listbox |
|
value={selectedItem} |
|
onChange={(value: Item) => { |
|
if (!disabled) { |
|
setSelectedItem(value) |
|
onSelect(value) |
|
} |
|
}} |
|
> |
|
<div className="relative h-9"> |
|
<Listbox.Button className={`w-full h-full rounded-lg border-0 bg-gray-100 py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-pointer ${className}`}> |
|
<span className="block truncate text-left">{selectedItem?.name}</span> |
|
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> |
|
<ChevronDownIcon |
|
className="h-5 w-5 text-gray-400" |
|
aria-hidden="true" |
|
/> |
|
</span> |
|
</Listbox.Button> |
|
<Transition |
|
as={Fragment} |
|
leave="transition ease-in duration-100" |
|
leaveFrom="opacity-100" |
|
leaveTo="opacity-0" |
|
> |
|
<Listbox.Options className="absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm"> |
|
{items.map((item: Item) => ( |
|
<Listbox.Option |
|
key={item.value} |
|
className={({ active }) => |
|
`relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : '' |
|
}` |
|
} |
|
value={item} |
|
disabled={disabled} |
|
> |
|
{({ /* active, */ selected }) => ( |
|
<> |
|
<span className={classNames('block truncate', selected && 'font-normal')}>{item.name}</span> |
|
{selected && ( |
|
<span |
|
className={classNames( |
|
'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700', |
|
)} |
|
> |
|
<CheckIcon className="h-5 w-5" aria-hidden="true" /> |
|
</span> |
|
)} |
|
</> |
|
)} |
|
</Listbox.Option> |
|
))} |
|
</Listbox.Options> |
|
</Transition> |
|
</div> |
|
</Listbox> |
|
) |
|
} |
|
export { SimpleSelect } |
|
export default React.memo(Select) |
|
|