"use client"; import clsx from "clsx"; import { motion, Variants } from "framer-motion"; import { Icon, SearchNormal } from "iconsax-react"; import { ChevronDown } from "lucide-react"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; export interface ComboboxOption { id: string; title: string; icon?: Icon; imagePath?: string; label?: string; } type Props = { title: string; options: Array; expanded?: boolean; selectedId: string; searchable?: boolean; placeholder?: string; icon?: React.ElementType; onSelectionChange: ( e: React.MouseEvent, index: number, ) => void; } & React.HTMLAttributes; type DropdownPosition = { top: number; left: number; width: number; }; function Combobox({ title, options, placeholder, icon: Icon, expanded, selectedId, searchable = true, onSelectionChange, className, ...props }: Props) { const [expand, setExpand] = useState(expanded); const [searchValue, setSearchValue] = useState(""); const [dropdownPosition, setDropdownPosition] = useState(null); const boxRef = useRef(null); const triggerRef = useRef(null); const dropdownRef = useRef(null); const updateDropdownPosition = useCallback(() => { if (!triggerRef.current) return; const rect = triggerRef.current.getBoundingClientRect(); setDropdownPosition({ top: rect.bottom + 4, left: rect.left, width: rect.width, }); }, []); useEffect(() => { if (!expand) { setDropdownPosition(null); return; } updateDropdownPosition(); window.addEventListener("scroll", updateDropdownPosition, true); window.addEventListener("resize", updateDropdownPosition); return () => { window.removeEventListener("scroll", updateDropdownPosition, true); window.removeEventListener("resize", updateDropdownPosition); }; }, [expand, updateDropdownPosition]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const target = event.target as Node; if ( boxRef.current?.contains(target) || dropdownRef.current?.contains(target) ) { return; } setExpand(false); }; if (expand) { document.addEventListener("mousedown", handleClickOutside); } return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [expand]); const toggleExpand = () => { setExpand((prev) => !prev); setSearchValue(""); }; const searchChanged = (e: React.ChangeEvent) => { setSearchValue(e.target.value); }; const setSelection = ( e: React.MouseEvent, index: number, ) => { e.stopPropagation(); onSelectionChange(e, index); setExpand(false); }; const selectedOption = options.find((x) => x.id === selectedId); const filteredOptions = options.filter((v) => v.title?.includes(searchValue)); const activeIcon = selectedOption?.icon && React.createElement(selectedOption.icon, { size: 16, color: "currentColor", className: "inline-block mr-1 text-primary dark:text-foreground", }); const activeImage = selectedOption?.imagePath && ( ); const variants: Variants = { collapse: { opacity: 0, scale: 0.95, pointerEvents: "none", transition: { duration: 0.1, ease: "easeInOut", }, }, expand: { opacity: 1, scale: 1, pointerEvents: "auto", transition: { duration: 0.1, ease: "easeInOut", }, }, }; const dropdown = expand && dropdownPosition && typeof document !== "undefined" ? createPortal( e.stopPropagation()} initial="collapse" animate="expand" variants={variants} > {searchable && (
)}
{filteredOptions.map((v) => { const originalIndex = options.findIndex((o) => o.id === v.id); return (
setSelection(e, originalIndex)} key={v.id} data-selected={v.id === selectedId} className="flex gap-3 items-center justify-end px-2 py-2 cursor-pointer hover:bg-current/10 rounded-md last:rounded-b-xl" tabIndex={0} role="option" aria-selected={v.id === selectedId} > {v?.imagePath ? ( ) : ( v?.icon && React.createElement(v.icon, { size: 16, color: "currentColor", className: "inline-block shrink-0 text-primary dark:text-foreground", }) )} {v.title}
); })}
, document.body, ) : null; return (
{activeImage ? activeImage : activeIcon ? activeIcon : Icon && ( )} {selectedOption?.title ?? placeholder}
{dropdown}
); } export default Combobox;