combobox.tsx raw

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