/* Copyright 2026 Marimo. All rights reserved. */ import { type JSX, useId, useMemo, useState } from "react"; import { Virtuoso } from "react-virtuoso"; import { Combobox, ComboboxItem } from "../../components/ui/combobox"; import { cn } from "../../utils/cn"; import { Labeled } from "./common/labeled"; import { multiselectFilterFn } from "./multiselectFilterFn"; interface SearchableSelectProps { options: string[]; value: string | null; setValue: (value: string | null) => void; label: string | null; allowSelectNone: boolean; fullWidth: boolean; } const NONE_KEY = "__none__"; export const SearchableSelect = (props: SearchableSelectProps): JSX.Element => { const { options, value, setValue, label, allowSelectNone, fullWidth } = props; const id = useId(); const [searchQuery, setSearchQuery] = useState(""); const filteredOptions = useMemo(() => { if (!searchQuery) { return options; } return options.filter( (option) => multiselectFilterFn(option, searchQuery) === 1, ); }, [options, searchQuery]); const handleValueChange = (newValue: string | null) => { if (newValue == null) { return; } if (newValue === NONE_KEY) { setValue(null); } else { setValue(newValue); } }; const renderList = () => { const extraOptions = allowSelectNone ? ( -- ) : null; if (filteredOptions.length > 200) { return ( { const option = filteredOptions[i]; const comboboxItem = ( {option} ); if (i === 0) { return ( <> {extraOptions} {comboboxItem} ); } return comboboxItem; }} /> ); } const list = filteredOptions.map((option) => ( {option} )); return ( <> {extraOptions} {list} ); }; return ( displayValue={(option) => { if (option === NONE_KEY) { return "--"; } return option; }} placeholder="Select..." multiple={false} className={cn({ "w-full": fullWidth, })} value={value ?? NONE_KEY} onValueChange={handleValueChange} shouldFilter={false} search={searchQuery} onSearchChange={setSearchQuery} data-testid="marimo-plugin-searchable-dropdown" > {renderList()} ); };