import { createSignal, For, onCleanup, onMount, Show, splitProps } from "solid-js"; import type { Component } from "solid-js"; import { Motion, Presence } from "solid-motionone"; export type DropdownOption = { value: string; label: string; disabled?: boolean }; type DropdownProps = { options: DropdownOption[]; value?: string | string[]; onChange?: (value: string | string[]) => void; placeholder?: string; multiple?: boolean; searchable?: boolean; disabled?: boolean; class?: string; }; const ChevronIcon: Component<{ open: boolean }> = (props) => ( ); export const Dropdown: Component = (props) => { const [local, _others] = splitProps(props, [ "options", "value", "onChange", "placeholder", "multiple", "searchable", "disabled", "class", ]); const [open, setOpen] = createSignal(false); const [search, setSearch] = createSignal(""); const [focusIndex, setFocusIndex] = createSignal(-1); let containerRef: HTMLDivElement | undefined; let inputRef: HTMLInputElement | undefined; const selectedValues = (): string[] => { if (!local.value) return []; return Array.isArray(local.value) ? local.value : [local.value]; }; const filteredOptions = () => { const q = search().toLowerCase(); if (!q) return local.options; return local.options.filter((o) => o.label.toLowerCase().includes(q)); }; const displayLabel = () => { const vals = selectedValues(); if (vals.length === 0) return local.placeholder || "Select..."; if (vals.length === 1) { return local.options.find((o) => o.value === vals[0])?.label || vals[0]; } return `${vals.length} selected`; }; const toggleOption = (value: string) => { if (local.multiple) { const current = selectedValues(); const next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value]; local.onChange?.(next); } else { local.onChange?.(value); setOpen(false); } setSearch(""); }; const handleClickOutside = (e: MouseEvent) => { if (containerRef && !containerRef.contains(e.target as Node)) { setOpen(false); } }; const handleKeyDown = (e: KeyboardEvent) => { const opts = filteredOptions(); if (e.key === "Escape") { setOpen(false); } else if (e.key === "ArrowDown") { e.preventDefault(); setFocusIndex((prev) => Math.min(prev + 1, opts.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setFocusIndex((prev) => Math.max(prev - 1, 0)); } else if (e.key === "Enter" && focusIndex() >= 0) { e.preventDefault(); const opt = opts[focusIndex()]; if (opt && !opt.disabled) toggleOption(opt.value); } }; onMount(() => { document.addEventListener("click", handleClickOutside); }); onCleanup(() => { document.removeEventListener("click", handleClickOutside); }); return (
{/* Trigger */} {/* Dropdown */} {/* Search */}
{ setSearch(e.currentTarget.value); setFocusIndex(0); }} placeholder="Search..." class="w-full px-3 py-1.5 bg-gray-900 border border-gray-700 rounded text-sm text-white placeholder-gray-500 focus:outline-none focus:border-blue-500" />
{/* Options */}
    {(option, index) => { const isSelected = () => selectedValues().includes(option.value); const isFocused = () => focusIndex() === index(); return (
  • !option.disabled && toggleOption(option.value)} onMouseEnter={() => setFocusIndex(index())} class={`flex items-center gap-2 px-4 py-2 text-sm cursor-pointer transition-colors ${option.disabled ? "opacity-50 cursor-not-allowed" : ""} ${isFocused() ? "bg-gray-700" : ""} ${isSelected() ? "text-blue-400" : "text-gray-300"} `}> {option.label}
  • ); }}
  • No options found
); };