index.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. 'use client'
  2. import type { FC } from 'react'
  3. import React, { Fragment, useEffect, useState } from 'react'
  4. import { Combobox, Listbox, Transition } from '@headlessui/react'
  5. import classNames from 'classnames'
  6. import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/20/solid'
  7. import { useTranslation } from 'react-i18next'
  8. const defaultItems = [
  9. { value: 1, name: 'option1' },
  10. { value: 2, name: 'option2' },
  11. { value: 3, name: 'option3' },
  12. { value: 4, name: 'option4' },
  13. { value: 5, name: 'option5' },
  14. { value: 6, name: 'option6' },
  15. { value: 7, name: 'option7' },
  16. ]
  17. export type Item = {
  18. value: number | string
  19. name: string
  20. }
  21. export type ISelectProps = {
  22. className?: string
  23. wrapperClassName?: string
  24. items?: Item[]
  25. defaultValue?: number | string
  26. disabled?: boolean
  27. onSelect: (value: Item) => void
  28. allowSearch?: boolean
  29. bgClassName?: string
  30. placeholder?: string
  31. overlayClassName?: string
  32. }
  33. const Select: FC<ISelectProps> = ({
  34. className,
  35. items = defaultItems,
  36. defaultValue = 1,
  37. disabled = false,
  38. onSelect,
  39. allowSearch = true,
  40. bgClassName = 'bg-gray-100',
  41. overlayClassName,
  42. }) => {
  43. const [query, setQuery] = useState('')
  44. const [open, setOpen] = useState(false)
  45. const [selectedItem, setSelectedItem] = useState<Item | null>(null)
  46. useEffect(() => {
  47. let defaultSelect = null
  48. const existed = items.find((item: Item) => item.value === defaultValue)
  49. if (existed)
  50. defaultSelect = existed
  51. setSelectedItem(defaultSelect)
  52. }, [defaultValue])
  53. const filteredItems: Item[]
  54. = query === ''
  55. ? items
  56. : items.filter((item) => {
  57. return item.name.toLowerCase().includes(query.toLowerCase())
  58. })
  59. return (
  60. <Combobox
  61. as="div"
  62. disabled={disabled}
  63. value={selectedItem}
  64. className={className}
  65. onChange={(value: Item) => {
  66. if (!disabled) {
  67. setSelectedItem(value)
  68. setOpen(false)
  69. onSelect(value)
  70. }
  71. }}>
  72. <div className={classNames('relative')}>
  73. <div className='group text-gray-800'>
  74. {allowSearch
  75. ? <Combobox.Input
  76. 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`}
  77. onChange={(event) => {
  78. if (!disabled)
  79. setQuery(event.target.value)
  80. }}
  81. displayValue={(item: Item) => item?.name}
  82. />
  83. : <Combobox.Button onClick={
  84. () => {
  85. if (!disabled)
  86. setOpen(!open)
  87. }
  88. } 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`}>
  89. {selectedItem?.name}
  90. </Combobox.Button>}
  91. <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={
  92. () => {
  93. if (!disabled)
  94. setOpen(!open)
  95. }
  96. }>
  97. {open ? <ChevronUpIcon className="h-5 w-5" /> : <ChevronDownIcon className="h-5 w-5" />}
  98. </Combobox.Button>
  99. </div>
  100. {filteredItems.length > 0 && (
  101. <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 ${overlayClassName}`}>
  102. {filteredItems.map((item: Item) => (
  103. <Combobox.Option
  104. key={item.value}
  105. value={item}
  106. className={({ active }: { active: boolean }) =>
  107. classNames(
  108. 'relative cursor-default select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700',
  109. active ? 'bg-gray-100' : '',
  110. )
  111. }
  112. >
  113. {({ /* active, */ selected }) => (
  114. <>
  115. <span className={classNames('block truncate', selected && 'font-normal')}>{item.name}</span>
  116. {selected && (
  117. <span
  118. className={classNames(
  119. 'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
  120. )}
  121. >
  122. <CheckIcon className="h-5 w-5" aria-hidden="true" />
  123. </span>
  124. )}
  125. </>
  126. )}
  127. </Combobox.Option>
  128. ))}
  129. </Combobox.Options>
  130. )}
  131. </div>
  132. </Combobox >
  133. )
  134. }
  135. const SimpleSelect: FC<ISelectProps> = ({
  136. className,
  137. wrapperClassName,
  138. items = defaultItems,
  139. defaultValue = 1,
  140. disabled = false,
  141. onSelect,
  142. placeholder,
  143. }) => {
  144. const { t } = useTranslation()
  145. const localPlaceholder = placeholder || t('common.placeholder.select')
  146. const [selectedItem, setSelectedItem] = useState<Item | null>(null)
  147. useEffect(() => {
  148. let defaultSelect = null
  149. const existed = items.find((item: Item) => item.value === defaultValue)
  150. if (existed)
  151. defaultSelect = existed
  152. setSelectedItem(defaultSelect)
  153. }, [defaultValue])
  154. return (
  155. <Listbox
  156. value={selectedItem}
  157. onChange={(value: Item) => {
  158. if (!disabled) {
  159. setSelectedItem(value)
  160. onSelect(value)
  161. }
  162. }}
  163. >
  164. <div className={`relative h-9 ${wrapperClassName}`}>
  165. <Listbox.Button className={`w-full h-full rounded-lg border-0 bg-gray-100 py-1.5 pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-pointer ${className}`}>
  166. <span className={classNames('block truncate text-left', !selectedItem?.name && 'text-gray-400')}>{selectedItem?.name ?? localPlaceholder}</span>
  167. <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
  168. <ChevronDownIcon
  169. className="h-5 w-5 text-gray-400"
  170. aria-hidden="true"
  171. />
  172. </span>
  173. </Listbox.Button>
  174. <Transition
  175. as={Fragment}
  176. leave="transition ease-in duration-100"
  177. leaveFrom="opacity-100"
  178. leaveTo="opacity-0"
  179. >
  180. <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">
  181. {items.map((item: Item) => (
  182. <Listbox.Option
  183. key={item.value}
  184. className={({ active }) =>
  185. `relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : ''
  186. }`
  187. }
  188. value={item}
  189. disabled={disabled}
  190. >
  191. {({ /* active, */ selected }) => (
  192. <>
  193. <span className={classNames('block truncate', selected && 'font-normal')}>{item.name}</span>
  194. {selected && (
  195. <span
  196. className={classNames(
  197. 'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
  198. )}
  199. >
  200. <CheckIcon className="h-5 w-5" aria-hidden="true" />
  201. </span>
  202. )}
  203. </>
  204. )}
  205. </Listbox.Option>
  206. ))}
  207. </Listbox.Options>
  208. </Transition>
  209. </div>
  210. </Listbox>
  211. )
  212. }
  213. export { SimpleSelect }
  214. export default React.memo(Select)