combobox.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. 'use client';
  2. import * as React from 'react';
  3. import { Combobox as ComboboxPrimitive } from '@base-ui/react';
  4. import { cn } from '@/lib/utils';
  5. import { Button } from '@/components/ui/button';
  6. import {
  7. InputGroup,
  8. InputGroupAddon,
  9. InputGroupButton,
  10. InputGroupInput,
  11. } from '@/components/ui/input-group';
  12. import { ChevronDownIcon, XIcon, CheckIcon } from 'lucide-react';
  13. const Combobox = ComboboxPrimitive.Root;
  14. function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
  15. return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;
  16. }
  17. function ComboboxTrigger({
  18. className,
  19. children,
  20. ...props
  21. }: ComboboxPrimitive.Trigger.Props) {
  22. return (
  23. <ComboboxPrimitive.Trigger
  24. data-slot="combobox-trigger"
  25. className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
  26. {...props}
  27. >
  28. {children}
  29. <ChevronDownIcon className="text-muted-foreground size-4 pointer-events-none" />
  30. </ComboboxPrimitive.Trigger>
  31. );
  32. }
  33. function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
  34. return (
  35. <ComboboxPrimitive.Clear
  36. data-slot="combobox-clear"
  37. render={<InputGroupButton variant="ghost" size="icon-xs" />}
  38. className={cn(className)}
  39. {...props}
  40. >
  41. <XIcon className="pointer-events-none" />
  42. </ComboboxPrimitive.Clear>
  43. );
  44. }
  45. function ComboboxInput({
  46. className,
  47. children,
  48. disabled = false,
  49. showTrigger = true,
  50. showClear = false,
  51. ...props
  52. }: ComboboxPrimitive.Input.Props & {
  53. showTrigger?: boolean;
  54. showClear?: boolean;
  55. }) {
  56. return (
  57. <InputGroup className={cn('w-auto', className)}>
  58. <ComboboxPrimitive.Input
  59. render={<InputGroupInput disabled={disabled} />}
  60. {...props}
  61. />
  62. <InputGroupAddon align="inline-end">
  63. {showTrigger && (
  64. <InputGroupButton
  65. size="icon-xs"
  66. variant="ghost"
  67. render={<ComboboxTrigger />}
  68. data-slot="input-group-button"
  69. className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
  70. disabled={disabled}
  71. />
  72. )}
  73. {showClear && <ComboboxClear disabled={disabled} />}
  74. </InputGroupAddon>
  75. {children}
  76. </InputGroup>
  77. );
  78. }
  79. function ComboboxContent({
  80. className,
  81. side = 'bottom',
  82. sideOffset = 6,
  83. align = 'start',
  84. alignOffset = 0,
  85. anchor,
  86. ...props
  87. }: ComboboxPrimitive.Popup.Props &
  88. Pick<
  89. ComboboxPrimitive.Positioner.Props,
  90. 'side' | 'align' | 'sideOffset' | 'alignOffset' | 'anchor'
  91. >) {
  92. return (
  93. <ComboboxPrimitive.Portal>
  94. <ComboboxPrimitive.Positioner
  95. side={side}
  96. sideOffset={sideOffset}
  97. align={align}
  98. alignOffset={alignOffset}
  99. anchor={anchor}
  100. className="isolate z-50"
  101. >
  102. <ComboboxPrimitive.Popup
  103. data-slot="combobox-content"
  104. data-chips={!!anchor}
  105. className={cn(
  106. 'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:border-input/30 overflow-hidden rounded-lg shadow-md ring-1 duration-100 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:shadow-none data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 cn-menu-target cn-menu-translucent group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) data-[chips=true]:min-w-(--anchor-width)',
  107. className,
  108. )}
  109. {...props}
  110. />
  111. </ComboboxPrimitive.Positioner>
  112. </ComboboxPrimitive.Portal>
  113. );
  114. }
  115. function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
  116. return (
  117. <ComboboxPrimitive.List
  118. data-slot="combobox-list"
  119. className={cn(
  120. 'no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 p-1 data-empty:p-0 overflow-y-auto overscroll-contain',
  121. className,
  122. )}
  123. {...props}
  124. />
  125. );
  126. }
  127. function ComboboxItem({
  128. className,
  129. children,
  130. ...props
  131. }: ComboboxPrimitive.Item.Props) {
  132. return (
  133. <ComboboxPrimitive.Item
  134. data-slot="combobox-item"
  135. className={cn(
  136. "data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
  137. className,
  138. )}
  139. {...props}
  140. >
  141. {children}
  142. <ComboboxPrimitive.ItemIndicator
  143. render={
  144. <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
  145. }
  146. >
  147. <CheckIcon className="pointer-events-none" />
  148. </ComboboxPrimitive.ItemIndicator>
  149. </ComboboxPrimitive.Item>
  150. );
  151. }
  152. function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
  153. return (
  154. <ComboboxPrimitive.Group
  155. data-slot="combobox-group"
  156. className={cn(className)}
  157. {...props}
  158. />
  159. );
  160. }
  161. function ComboboxLabel({
  162. className,
  163. ...props
  164. }: ComboboxPrimitive.GroupLabel.Props) {
  165. return (
  166. <ComboboxPrimitive.GroupLabel
  167. data-slot="combobox-label"
  168. className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
  169. {...props}
  170. />
  171. );
  172. }
  173. function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
  174. return (
  175. <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
  176. );
  177. }
  178. function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
  179. return (
  180. <ComboboxPrimitive.Empty
  181. data-slot="combobox-empty"
  182. className={cn(
  183. 'text-muted-foreground hidden w-full justify-center py-2 text-center text-sm group-data-empty/combobox-content:flex',
  184. className,
  185. )}
  186. {...props}
  187. />
  188. );
  189. }
  190. function ComboboxSeparator({
  191. className,
  192. ...props
  193. }: ComboboxPrimitive.Separator.Props) {
  194. return (
  195. <ComboboxPrimitive.Separator
  196. data-slot="combobox-separator"
  197. className={cn('bg-border -mx-1 my-1 h-px', className)}
  198. {...props}
  199. />
  200. );
  201. }
  202. function ComboboxChips({
  203. className,
  204. ...props
  205. }: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
  206. ComboboxPrimitive.Chips.Props) {
  207. return (
  208. <ComboboxPrimitive.Chips
  209. data-slot="combobox-chips"
  210. className={cn(
  211. 'dark:bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/50 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive dark:has-aria-invalid:border-destructive/50 flex min-h-8 flex-wrap items-center gap-1 rounded-lg border bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:ring-3 has-aria-invalid:ring-3 has-data-[slot=combobox-chip]:px-1',
  212. className,
  213. )}
  214. {...props}
  215. />
  216. );
  217. }
  218. function ComboboxChip({
  219. className,
  220. children,
  221. showRemove = true,
  222. ...props
  223. }: ComboboxPrimitive.Chip.Props & {
  224. showRemove?: boolean;
  225. }) {
  226. return (
  227. <ComboboxPrimitive.Chip
  228. data-slot="combobox-chip"
  229. className={cn(
  230. 'bg-muted text-foreground flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm px-1.5 text-xs font-medium whitespace-nowrap has-data-[slot=combobox-chip-remove]:pr-0 has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50',
  231. className,
  232. )}
  233. {...props}
  234. >
  235. {children}
  236. {showRemove && (
  237. <ComboboxPrimitive.ChipRemove
  238. render={<Button variant="ghost" size="icon-xs" />}
  239. className="-ml-1 opacity-50 hover:opacity-100"
  240. data-slot="combobox-chip-remove"
  241. >
  242. <XIcon className="pointer-events-none" />
  243. </ComboboxPrimitive.ChipRemove>
  244. )}
  245. </ComboboxPrimitive.Chip>
  246. );
  247. }
  248. function ComboboxChipsInput({
  249. className,
  250. ...props
  251. }: ComboboxPrimitive.Input.Props) {
  252. return (
  253. <ComboboxPrimitive.Input
  254. data-slot="combobox-chip-input"
  255. className={cn('min-w-16 flex-1 outline-none', className)}
  256. {...props}
  257. />
  258. );
  259. }
  260. function useComboboxAnchor() {
  261. return React.useRef<HTMLDivElement | null>(null);
  262. }
  263. export {
  264. Combobox,
  265. ComboboxInput,
  266. ComboboxContent,
  267. ComboboxList,
  268. ComboboxItem,
  269. ComboboxGroup,
  270. ComboboxLabel,
  271. ComboboxCollection,
  272. ComboboxEmpty,
  273. ComboboxSeparator,
  274. ComboboxChips,
  275. ComboboxChip,
  276. ComboboxChipsInput,
  277. ComboboxTrigger,
  278. ComboboxValue,
  279. useComboboxAnchor,
  280. };