combobox.tsx 8.8 KB

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