/* Copyright 2026 Marimo. All rights reserved. */ import { useControllableState } from "@radix-ui/react-use-controllable-state"; import { Check, ChevronDownIcon, XCircle } from "lucide-react"; import React, { createContext } from "react"; import { cn } from "../../utils/cn"; import { Functions } from "../../utils/functions"; import { Badge } from "./badge"; import { Command, CommandEmpty, CommandInput, CommandItem, CommandList, } from "./command"; import { Popover, PopoverContent, PopoverTrigger } from "./popover"; interface ComboboxContextValue { isSelected: (value: unknown) => boolean; onSelect: (value: unknown) => void; } export const ComboboxContext = createContext({ isSelected: () => false, onSelect: Functions.NOOP, }); interface ComboboxCommonProps { children: React.ReactNode; displayValue?: (item: TValue) => string; placeholder?: string; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; inputPlaceholder?: string; search?: string; onSearchChange?: (search: string) => void; emptyState?: React.ReactNode; className?: string; id?: string; keepPopoverOpenOnSelect?: boolean; } type ComboboxFilterProps = | { shouldFilter?: true; filterFn?: React.ComponentProps["filter"]; } | { shouldFilter: false; filterFn?: never; }; type ComboboxValueProps = | { multiple?: false; chips?: false; chipsClassName?: never; value?: TValue | null; defaultValue?: TValue | null; onValueChange?: (value: TValue | null) => void; } | { multiple: true; chips?: boolean; chipsClassName?: string; value?: TValue[] | null; defaultValue?: TValue[] | null; onValueChange?: (value: TValue[] | null) => void; }; export type ComboboxProps = ComboboxCommonProps & ComboboxValueProps & ComboboxFilterProps; export const Combobox = ({ children, displayValue, className, placeholder, value: valueProp, defaultValue, onValueChange, multiple = false, shouldFilter = true, filterFn, open: openProp, defaultOpen, onOpenChange, inputPlaceholder = "Search...", search, onSearchChange, emptyState = "Nothing found.", chips = false, chipsClassName, keepPopoverOpenOnSelect, id, ...rest }: ComboboxProps) => { const [open = false, setOpen] = useControllableState({ prop: openProp, defaultProp: defaultOpen ?? false, onChange: onOpenChange, }); const [value, setValue] = useControllableState({ prop: valueProp, defaultProp: defaultValue, onChange: (state) => { onValueChange?.(state as unknown as TValue & TValue[]); }, }); const isSelected = (selectedValue: unknown) => { if (Array.isArray(value)) { return value.includes(selectedValue as TValue); } return value === selectedValue; }; const handleSelect = (selectedValue: unknown) => { let newValue: TValue | TValue[] | null = selectedValue as TValue; if (multiple) { if (Array.isArray(value)) { if (value.includes(newValue)) { const newArr = value.filter((val) => val !== selectedValue); newValue = newArr.length > 0 ? newArr : []; } else { newValue = [...value, newValue]; } } else { newValue = [newValue]; } } else if (value === selectedValue) { newValue = null; } setValue(newValue); const keepOpen = keepPopoverOpenOnSelect ?? multiple; if (!keepOpen) { setOpen(false); } }; const renderValue = (): string => { // If we show chips, we don't want to change the placeholder if (multiple && chips && placeholder) { return placeholder; } if (value != null) { if (Array.isArray(value)) { if (value.length === 0) { return placeholder ?? "--"; } if (value.length === 1 && displayValue !== undefined) { return displayValue(value[0]); } return `${value.length} selected`; } if (displayValue !== undefined) { return displayValue(value as unknown as TValue); } return placeholder ?? "--"; } return placeholder ?? "--"; }; return (
{renderValue()}
{emptyState} {children}
{multiple && chips && (
{Array.isArray(value) && value.map((val) => { if (val == null) { return null; } return ( {displayValue?.(val) ?? String(val)} { handleSelect(val); }} className="w-3 h-3 opacity-50 hover:opacity-100 ml-1 cursor-pointer" /> ); })}
)}
); }; interface ComboboxItemOptions { value: TValue; } export interface ComboboxItemProps extends ComboboxItemOptions, Omit< React.ComponentProps, keyof ComboboxItemOptions | "onSelect" | "role" > { onSelect?: (value: TValue) => void; } export const ComboboxItem = React.forwardRef( ( { children, className, value, onSelect, disabled, }: ComboboxItemProps, ref: React.Ref, ) => { const valueAsString = typeof value === "object" && "value" in value ? value.value : String(value); const context = React.use(ComboboxContext); return ( { context.onSelect(value); onSelect?.(value); }} > {context.isSelected(value) && ( )} {children} ); }, ); ComboboxItem.displayName = "ComboboxItem";