File size: 5,190 Bytes
101ebaa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6593c28
 
 
101ebaa
 
 
 
 
 
 
 
 
 
6593c28
 
 
101ebaa
 
6593c28
 
 
101ebaa
 
6593c28
101ebaa
 
 
 
 
 
 
6593c28
101ebaa
 
46d4f6c
101ebaa
 
 
 
 
 
 
 
 
 
 
46d4f6c
101ebaa
 
 
 
 
 
 
 
 
6593c28
 
 
 
 
 
 
 
101ebaa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6593c28
 
 
 
 
101ebaa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
"use client";

import * as React from "react";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
    Popover,
    PopoverContent,
    PopoverTrigger,
} from "@/components/ui/popover";
import { Input } from "@/components/ui/input";

interface ComboboxProps {
    options: { id: string; name: string }[];
    value: string;
    onValueChange: (value: string) => void;
    placeholder?: string;
    searchPlaceholder?: string;
    emptyMessage?: string;
    loading?: boolean;
    searchValue?: string;
    onSearchValueChange?: (value: string) => void;
    disableLocalFilter?: boolean;
}

export function Combobox({
    options,
    value,
    onValueChange,
    placeholder = "Select option...",
    searchPlaceholder = "Search...",
    emptyMessage = "No results found.",
    loading = false,
    searchValue,
    onSearchValueChange,
    disableLocalFilter = false,
}: ComboboxProps) {
    const [open, setOpen] = React.useState(false);
    const [internalSearch, setInternalSearch] = React.useState("");

    const search = searchValue ?? internalSearch;

    const filteredOptions = React.useMemo(() => {
        if (disableLocalFilter) return options;
        if (!search) return options;
        const lower = search.toLowerCase();
        return options.filter(
            (option) =>
                option.name.toLowerCase().includes(lower) ||
                option.id.toLowerCase().includes(lower)
        );
    }, [disableLocalFilter, options, search]);

    const selectedOption = options.find((opt) => opt.id === value);
    const selectedLabel = selectedOption?.name ?? (value ? value : placeholder);

    return (
        <Popover open={open} onOpenChange={setOpen}>
            <PopoverTrigger asChild>
                <Button
                    variant="outline"
                    role="combobox"
                    aria-expanded={open}
                    className="w-full justify-between font-normal"
                >
                    <span className="truncate">
                        {selectedLabel}
                    </span>
                    <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
                </Button>
            </PopoverTrigger>
            <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0" align="start">
                <div className="p-2">
                    <Input
                        placeholder={searchPlaceholder}
                        value={search}
                        onChange={(e) => {
                            const next = e.target.value;
                            if (onSearchValueChange) {
                                onSearchValueChange(next);
                            } else {
                                setInternalSearch(next);
                            }
                        }}
                        className="h-9"
                    />
                </div>
                <div className="max-h-[300px] overflow-y-auto">
                    {loading ? (
                        <div className="py-6 text-center text-sm text-muted-foreground">
                            Loading...
                        </div>
                    ) : filteredOptions.length === 0 ? (
                        <div className="py-6 text-center text-sm text-muted-foreground">
                            {emptyMessage}
                        </div>
                    ) : (
                        <div className="p-1">
                            {filteredOptions.map((option) => (
                                <button
                                    key={option.id}
                                    onClick={() => {
                                        onValueChange(option.id === value ? "" : option.id);
                                        setOpen(false);
                                        if (onSearchValueChange) {
                                            onSearchValueChange("");
                                        } else {
                                            setInternalSearch("");
                                        }
                                    }}
                                    className={cn(
                                        "relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground",
                                        value === option.id && "bg-accent"
                                    )}
                                >
                                    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
                                        {value === option.id && <Check className="h-4 w-4" />}
                                    </span>
                                    <span className="truncate">{option.name}</span>
                                </button>
                            ))}
                        </div>
                    )}
                </div>
            </PopoverContent>
        </Popover>
    );
}