返回 AiToEarn
select.tsx
根目录 / project / aitoearn-web / src / components / ui / select.tsx
1 /**
2 * Select - 选择器组件
3 * 基于 Radix UI Select 封装
4 */
5
6 'use client'
7
8 import * as SelectPrimitive from '@radix-ui/react-select'
9 import { Check, ChevronDown, ChevronUp, X } from 'lucide-react'
10 import * as React from 'react'
11
12 import { useTransClient } from '@/app/i18n/client'
13 import { cn } from '@/utils/className'
14
15 interface SelectProps extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Root> {
16 clearable?: boolean
17 onClear?: () => void
18 clearAriaLabel?: string
19 }
20
21 interface SelectContextValue {
22 clearable?: boolean
23 onClear?: () => void
24 clearAriaLabel?: string
25 }
26
27 const SelectContext = React.createContext<SelectContextValue | null>(null)
28
29 function Select({ clearable, onClear, clearAriaLabel, ...props }: SelectProps) {
30 const contextValue = React.useMemo(
31 () => ({ clearable, onClear, clearAriaLabel }),
32 [clearAriaLabel, clearable, onClear],
33 )
34
35 return (
36 <SelectContext.Provider value={contextValue}>
37 <SelectPrimitive.Root {...props} />
38 </SelectContext.Provider>
39 )
40 }
41
42 const SelectGroup = SelectPrimitive.Group
43
44 const SelectValue = SelectPrimitive.Value
45
46 const SelectTrigger = React.forwardRef<
47 React.ElementRef<typeof SelectPrimitive.Trigger>,
48 React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
49 >(({ className, children, disabled, ...props }, ref) => {
50 const { t } = useTransClient('common')
51 const selectContext = React.useContext(SelectContext)
52 const showClear = !!(selectContext?.clearable && !disabled && selectContext.onClear)
53
54 return (
55 <SelectPrimitive.Trigger
56 ref={ref}
57 className={cn(
58 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
59 className,
60 )}
61 disabled={disabled}
62 {...props}
63 >
64 {children}
65 {showClear
66 ? (
67 <span
68 role="button"
69 aria-label={selectContext.clearAriaLabel ?? t('datePicker.clear')}
70 className="ml-2 shrink-0 opacity-50 hover:opacity-100 transition-opacity"
71 onClick={(e) => {
72 e.stopPropagation()
73 e.preventDefault()
74 selectContext.onClear?.()
75 }}
76 onPointerDown={(e) => {
77 e.stopPropagation()
78 e.preventDefault()
79 }}
80 >
81 <X className="h-4 w-4" />
82 </span>
83 )
84 : (
85 <SelectPrimitive.Icon asChild>
86 <ChevronDown className="h-4 w-4 opacity-50" />
87 </SelectPrimitive.Icon>
88 )}
89 </SelectPrimitive.Trigger>
90 )
91 })
92 SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
93
94 const SelectScrollUpButton = React.forwardRef<
95 React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
96 React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
97 >(({ className, ...props }, ref) => (
98 <SelectPrimitive.ScrollUpButton
99 ref={ref}
100 className={cn('flex cursor-default items-center justify-center py-1', className)}
101 {...props}
102 >
103 <ChevronUp className="h-4 w-4" />
104 </SelectPrimitive.ScrollUpButton>
105 ))
106 SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
107
108 const SelectScrollDownButton = React.forwardRef<
109 React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
110 React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
111 >(({ className, ...props }, ref) => (
112 <SelectPrimitive.ScrollDownButton
113 ref={ref}
114 className={cn('flex cursor-default items-center justify-center py-1', className)}
115 {...props}
116 >
117 <ChevronDown className="h-4 w-4" />
118 </SelectPrimitive.ScrollDownButton>
119 ))
120 SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
121
122 const SelectContent = React.forwardRef<
123 React.ElementRef<typeof SelectPrimitive.Content>,
124 React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
125 >(({ className, children, position = 'popper', ...props }, ref) => (
126 <SelectPrimitive.Portal>
127 <SelectPrimitive.Content
128 ref={ref}
129 className={cn(
130 'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
131 position === 'popper'
132 && 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
133 className,
134 )}
135 position={position}
136 {...props}
137 >
138 <SelectScrollUpButton />
139 <SelectPrimitive.Viewport
140 className={cn(
141 'p-1',
142 position === 'popper' && 'w-full min-w-[var(--radix-select-trigger-width)]',
143 )}
144 >
145 {children}
146 </SelectPrimitive.Viewport>
147 <SelectScrollDownButton />
148 </SelectPrimitive.Content>
149 </SelectPrimitive.Portal>
150 ))
151 SelectContent.displayName = SelectPrimitive.Content.displayName
152
153 const SelectLabel = React.forwardRef<
154 React.ElementRef<typeof SelectPrimitive.Label>,
155 React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
156 >(({ className, ...props }, ref) => (
157 <SelectPrimitive.Label
158 ref={ref}
159 className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
160 {...props}
161 />
162 ))
163 SelectLabel.displayName = SelectPrimitive.Label.displayName
164
165 const SelectItem = React.forwardRef<
166 React.ElementRef<typeof SelectPrimitive.Item>,
167 React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
168 >(({ className, children, ...props }, ref) => (
169 <SelectPrimitive.Item
170 ref={ref}
171 className={cn(
172 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
173 className,
174 )}
175 {...props}
176 >
177 <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
178 <SelectPrimitive.ItemIndicator>
179 <Check className="h-4 w-4" />
180 </SelectPrimitive.ItemIndicator>
181 </span>
182
183 <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
184 </SelectPrimitive.Item>
185 ))
186 SelectItem.displayName = SelectPrimitive.Item.displayName
187
188 const SelectSeparator = React.forwardRef<
189 React.ElementRef<typeof SelectPrimitive.Separator>,
190 React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
191 >(({ className, ...props }, ref) => (
192 <SelectPrimitive.Separator
193 ref={ref}
194 className={cn('-mx-1 my-1 h-px bg-muted', className)}
195 {...props}
196 />
197 ))
198 SelectSeparator.displayName = SelectPrimitive.Separator.displayName
199
200 export {
201 Select,
202 SelectContent,
203 SelectGroup,
204 SelectItem,
205 SelectLabel,
206 SelectScrollDownButton,
207 SelectScrollUpButton,
208 SelectSeparator,
209 SelectTrigger,
210 SelectValue,
211 }
212
212 lines Plain Text