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.

pure.tsx 4.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  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. }
  41. const PureSelect = ({
  42. options,
  43. value,
  44. onChange,
  45. containerProps,
  46. triggerProps,
  47. popupProps,
  48. }: PureSelectProps) => {
  49. const { t } = useTranslation()
  50. const {
  51. open,
  52. onOpenChange,
  53. placement,
  54. offset,
  55. } = containerProps || {}
  56. const {
  57. className: triggerClassName,
  58. } = triggerProps || {}
  59. const {
  60. wrapperClassName: popupWrapperClassName,
  61. className: popupClassName,
  62. itemClassName: popupItemClassName,
  63. title: popupTitle,
  64. } = popupProps || {}
  65. const [localOpen, setLocalOpen] = useState(false)
  66. const mergedOpen = open ?? localOpen
  67. const handleOpenChange = useCallback((openValue: boolean) => {
  68. onOpenChange?.(openValue)
  69. setLocalOpen(openValue)
  70. }, [onOpenChange])
  71. const selectedOption = options.find(option => option.value === value)
  72. const triggerText = selectedOption?.label || t('common.placeholder.select')
  73. return (
  74. <PortalToFollowElem
  75. placement={placement || 'bottom-start'}
  76. offset={offset || 4}
  77. open={mergedOpen}
  78. onOpenChange={handleOpenChange}
  79. >
  80. <PortalToFollowElemTrigger
  81. onClick={() => handleOpenChange(!mergedOpen)}
  82. asChild
  83. >
  84. <div
  85. className={cn(
  86. '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',
  87. mergedOpen && 'bg-state-base-hover-alt',
  88. triggerClassName,
  89. )}
  90. >
  91. <div
  92. className='grow'
  93. title={triggerText}
  94. >
  95. {triggerText}
  96. </div>
  97. <RiArrowDownSLine
  98. className={cn(
  99. 'h-4 w-4 shrink-0 text-text-quaternary group-hover:text-text-secondary',
  100. mergedOpen && 'text-text-secondary',
  101. )}
  102. />
  103. </div>
  104. </PortalToFollowElemTrigger>
  105. <PortalToFollowElemContent className={cn(
  106. 'z-10',
  107. popupWrapperClassName,
  108. )}>
  109. <div
  110. className={cn(
  111. 'rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg',
  112. popupClassName,
  113. )}
  114. >
  115. {
  116. popupTitle && (
  117. <div className='system-xs-medium-uppercase flex h-[22px] items-center px-3 text-text-tertiary'>
  118. {popupTitle}
  119. </div>
  120. )
  121. }
  122. {
  123. options.map(option => (
  124. <div
  125. key={option.value}
  126. className={cn(
  127. 'system-sm-medium flex h-8 cursor-pointer items-center rounded-lg px-2 text-text-secondary hover:bg-state-base-hover',
  128. popupItemClassName,
  129. )}
  130. title={option.label}
  131. onClick={() => {
  132. onChange?.(option.value)
  133. handleOpenChange(false)
  134. }}
  135. >
  136. <div className='mr-1 grow truncate px-1'>
  137. {option.label}
  138. </div>
  139. {
  140. value === option.value && <RiCheckLine className='h-4 w-4 shrink-0 text-text-accent' />
  141. }
  142. </div>
  143. ))
  144. }
  145. </div>
  146. </PortalToFollowElemContent>
  147. </PortalToFollowElem>
  148. )
  149. }
  150. export default PureSelect