Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

custom.tsx 4.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  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. export type Option = {
  20. label: string
  21. value: string
  22. }
  23. export type CustomSelectProps<T extends Option> = {
  24. options: T[]
  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. CustomTrigger?: (option: T | undefined, open: boolean) => React.JSX.Element
  41. CustomOption?: (option: T, selected: boolean) => React.JSX.Element
  42. }
  43. const CustomSelect = <T extends Option>({
  44. options,
  45. value,
  46. onChange,
  47. containerProps,
  48. triggerProps,
  49. popupProps,
  50. CustomTrigger,
  51. CustomOption,
  52. }: CustomSelectProps<T>) => {
  53. const { t } = useTranslation()
  54. const {
  55. open,
  56. onOpenChange,
  57. placement,
  58. offset,
  59. } = containerProps || {}
  60. const {
  61. className: triggerClassName,
  62. } = triggerProps || {}
  63. const {
  64. wrapperClassName: popupWrapperClassName,
  65. className: popupClassName,
  66. itemClassName: popupItemClassName,
  67. } = popupProps || {}
  68. const [localOpen, setLocalOpen] = useState(false)
  69. const mergedOpen = open ?? localOpen
  70. const handleOpenChange = useCallback((openValue: boolean) => {
  71. onOpenChange?.(openValue)
  72. setLocalOpen(openValue)
  73. }, [onOpenChange])
  74. const selectedOption = options.find(option => option.value === value)
  75. const triggerText = selectedOption?.label || t('common.placeholder.select')
  76. return (
  77. <PortalToFollowElem
  78. placement={placement || 'bottom-start'}
  79. offset={offset || 4}
  80. open={mergedOpen}
  81. onOpenChange={handleOpenChange}
  82. >
  83. <PortalToFollowElemTrigger
  84. onClick={() => handleOpenChange(!mergedOpen)}
  85. asChild
  86. >
  87. <div
  88. className={cn(
  89. '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',
  90. mergedOpen && 'bg-state-base-hover-alt',
  91. triggerClassName,
  92. )}
  93. >
  94. {CustomTrigger ? CustomTrigger(selectedOption, mergedOpen) : (
  95. <>
  96. <div
  97. className='grow'
  98. title={triggerText}
  99. >
  100. {triggerText}
  101. </div>
  102. <RiArrowDownSLine
  103. className={cn(
  104. 'h-4 w-4 shrink-0 text-text-quaternary group-hover:text-text-secondary',
  105. mergedOpen && 'text-text-secondary',
  106. )}
  107. />
  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 shadow-shadow-shadow-5',
  119. popupClassName,
  120. )}
  121. >
  122. {
  123. options.map((option) => {
  124. const selected = value === option.value
  125. return (
  126. <div
  127. key={option.value}
  128. className={cn(
  129. 'system-sm-medium flex h-8 cursor-pointer items-center rounded-lg px-2 text-text-secondary hover:bg-state-base-hover',
  130. popupItemClassName,
  131. )}
  132. title={option.label}
  133. onClick={() => {
  134. onChange?.(option.value)
  135. handleOpenChange(false)
  136. }}
  137. >
  138. {CustomOption ? CustomOption(option, selected) : (
  139. <>
  140. <div className='mr-1 grow truncate px-1'>
  141. {option.label}
  142. </div>
  143. {
  144. selected && <RiCheckLine className='h-4 w-4 shrink-0 text-text-accent' />
  145. }
  146. </>
  147. )}
  148. </div>
  149. )
  150. })
  151. }
  152. </div>
  153. </PortalToFollowElemContent>
  154. </PortalToFollowElem>
  155. )
  156. }
  157. export default CustomSelect