| '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) |
|
|