You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import {
  2. useCallback,
  3. useState,
  4. } from 'react'
  5. import { useTranslation } from 'react-i18next'
  6. import {
  7. RiArrowDownSLine,
  8. RiCheckLine,
  9. } from '@remixicon/react'
  10. import {
  11. PortalToFollowElem,
  12. PortalToFollowElemContent,
  13. PortalToFollowElemTrigger,
  14. } from '@/app/components/base/portal-to-follow-elem'
  15. import type {
  16. PortalToFollowElemOptions,
  17. } from '@/app/components/base/portal-to-follow-elem'
  18. import cn from '@/utils/classnames'
  19. type Option = {
  20. label: string
  21. value: string
  22. }
  23. type PureSelectProps = {
  24. options: Option[]
  25. value?: string
  26. onChange?: (value: string) => void
  27. containerProps?: PortalToFollowElemOptions & {
  28. open?: boolean
  29. onOpenChange?: (open: boolean) => void
  30. }
  31. triggerProps?: {
  32. className?: string
  33. },
  34. popupProps?: {
  35. wrapperClassName?: string
  36. className?: string
  37. itemClassName?: string
  38. title?: string
  39. },
  40. placeholder?: string
  41. disabled?: boolean
  42. triggerPopupSameWidth?: boolean
  43. }
  44. const PureSelect = ({
  45. options,
  46. value,
  47. onChange,
  48. containerProps,
  49. triggerProps,
  50. popupProps,
  51. placeholder,
  52. disabled,
  53. triggerPopupSameWidth,
  54. }: PureSelectProps) => {
  55. const { t } = useTranslation()
  56. const {
  57. open,
  58. onOpenChange,
  59. placement,
  60. offset,
  61. } = containerProps || {}
  62. const {
  63. className: triggerClassName,
  64. } = triggerProps || {}
  65. const {
  66. wrapperClassName: popupWrapperClassName,
  67. className: popupClassName,
  68. itemClassName: popupItemClassName,
  69. title: popupTitle,
  70. } = popupProps || {}
  71. const [localOpen, setLocalOpen] = useState(false)
  72. const mergedOpen = open ?? localOpen
  73. const handleOpenChange = useCallback((openValue: boolean) => {
  74. onOpenChange?.(openValue)
  75. setLocalOpen(openValue)
  76. }, [onOpenChange])
  77. const selectedOption = options.find(option => option.value === value)
  78. const triggerText = selectedOption?.label || placeholder || t('common.placeholder.select')
  79. return (
  80. <PortalToFollowElem
  81. placement={placement || 'bottom-start'}
  82. offset={offset || 4}
  83. open={mergedOpen}
  84. onOpenChange={handleOpenChange}
  85. triggerPopupSameWidth={triggerPopupSameWidth}
  86. >
  87. <PortalToFollowElemTrigger
  88. onClick={() => handleOpenChange(!mergedOpen)}
  89. asChild
  90. >
  91. <div
  92. className={cn(
  93. 'system-sm-regular group flex h-8 cursor-pointer items-center rounded-lg bg-components-input-bg-normal px-2 text-components-input-text-filled hover:bg-state-base-hover-alt',
  94. mergedOpen && 'bg-state-base-hover-alt',
  95. triggerClassName,
  96. )}
  97. >
  98. <div
  99. className='grow'
  100. title={triggerText}
  101. >
  102. {triggerText}
  103. </div>
  104. <RiArrowDownSLine
  105. className={cn(
  106. 'h-4 w-4 shrink-0 text-text-quaternary group-hover:text-text-secondary',
  107. mergedOpen && 'text-text-secondary',
  108. )}
  109. />
  110. </div>
  111. </PortalToFollowElemTrigger>
  112. <PortalToFollowElemContent className={cn(
  113. 'z-10',
  114. popupWrapperClassName,
  115. )}>
  116. <div
  117. className={cn(
  118. 'rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg',
  119. popupClassName,
  120. )}
  121. >
  122. {
  123. popupTitle && (
  124. <div className='system-xs-medium-uppercase flex h-[22px] items-center px-3 text-text-tertiary'>
  125. {popupTitle}
  126. </div>
  127. )
  128. }
  129. {
  130. options.map(option => (
  131. <div
  132. key={option.value}
  133. className={cn(
  134. 'system-sm-medium flex h-8 cursor-pointer items-center rounded-lg px-2 text-text-secondary hover:bg-state-base-hover',
  135. popupItemClassName,
  136. )}
  137. title={option.label}
  138. onClick={() => {
  139. if (disabled) return
  140. onChange?.(option.value)
  141. handleOpenChange(false)
  142. }}
  143. >
  144. <div className='mr-1 grow truncate px-1'>
  145. {option.label}
  146. </div>
  147. {
  148. value === option.value && <RiCheckLine className='h-4 w-4 shrink-0 text-text-accent' />
  149. }
  150. </div>
  151. ))
  152. }
  153. </div>
  154. </PortalToFollowElemContent>
  155. </PortalToFollowElem>
  156. )
  157. }
  158. export default PureSelect