import { useVirtualizer } from "@tanstack/react-virtual"; import { type JSX, useRef, useState } from "react"; import ReactSelect, { type CSSObjectWithLabel, type GroupBase, type MenuListProps, } from "react-select"; import { cn } from "~/lib/utils"; export type ListOption = { label: string; value: string; }; type BaseProps = { elementId: string; elementName: string; options: ListOption[]; placeholder: string; disabled?: boolean; menuPlacement?: "auto" | "bottom" | "top"; defaultValue?: ListOption | ListOption[]; }; type MultiProps = { isMulti: true; onValueChange: (value: ListOption[]) => void; }; type SigletProps = { isMulti: false; onValueChange: (value: ListOption) => void; }; type MultiSelectProps = BaseProps & (MultiProps | SigletProps); const VirtualizedMenuList = ( props: MenuListProps< ListOption, MultiSelectProps["isMulti"], GroupBase >, ) => { const rows = props.children as JSX.Element[]; const parentRef = useRef(null); const count = rows.length; const virtualizer = useVirtualizer({ count, getScrollElement: () => parentRef.current, estimateSize: () => 45, }); const items = virtualizer.getVirtualItems(); return (
{items.map((virtualRow) => (
{rows[virtualRow.index]}
))}
); }; /** * * @example * { * field.onChange(value.value); * //if isMulti * field.onChange(value.map((v) => v.value)); * }} * placeholder="Seleziona..." * isMulti={false} * /> */ export const MultiSelect = ({ elementId, defaultValue, elementName, isMulti, onValueChange, options, placeholder, disabled, menuPlacement, }: MultiSelectProps) => { const [menuOpen, setMenuOpen] = useState(false); return ( { if (isMulti) { const valore = values as ListOption[]; onValueChange(valore); } else { const valore = values as ListOption; onValueChange(valore); } }} defaultValue={defaultValue} placeholder={placeholder} onMenuOpen={() => setMenuOpen(true)} onMenuClose={() => setMenuOpen(false)} styles={{ container: (base, state) => ({ ...base, ":hover .select__indicator-separator": { backgroundColor: state.isFocused ? "rgb(161,161,170)" : "rgb(161,161,170)", }, ":hover .select__dropdown-indicator": { color: state.isFocused ? "rgb(161,161,170)" : "rgb(161,161,170)", }, }) as CSSObjectWithLabel, control: (base, state) => ({ ...base, minHeight: "42px", borderRadius: "0.5rem", boxShadow: undefined, border: state.isFocused ? "1px solid rgb(161,161,170)" : "1px solid rgb(212 ,212 ,216)", "&:hover": { borderColor: state.isFocused ? "rgb(161,161,170)" : "rgb(161 ,161 ,170)", }, "& .select__indicator-separator": { backgroundColor: state.isFocused ? "rgb(161,161,170)" : "rgb(212 ,212 ,216)", }, "& .select__dropdown-indicator": { color: state.isFocused ? "rgb(161,161,170)" : "rgb(212 ,212 ,216)", transition: "none", }, }) as CSSObjectWithLabel, multiValue: (base) => ({ ...base, backgroundColor: undefined, borderRadius: "0.5rem", }) as CSSObjectWithLabel, multiValueLabel: (base) => ({ ...base, borderRadius: "0.5rem", }) as CSSObjectWithLabel, menu: (base) => ({ ...base, borderRadius: "0.5rem", }) as CSSObjectWithLabel, option: (base, state) => ({ ...base, backgroundColor: undefined, color: state.isSelected ? undefined : undefined, "&:hover": { backgroundColor: undefined, color: state.isSelected ? undefined : undefined, }, "&:active": { backgroundColor: "rgb(161, 161, 170)", color: "rgb(255, 255, 255)", }, "&:focus": { backgroundColor: undefined, }, }) as CSSObjectWithLabel, }} classNames={{ valueContainer: () => cn("min-h-[38px]"), control: () => cn( "mt-2 w-full rounded-lg h-min-[42px] dark:bg-primary bg-white shadow-xs outline-hidden text-foreground dark:text-white", ), option: () => cn( "dark:bg-black text-foreground bg-white dark:text-white border-b border-neutral-300 dark:border-foreground shadow-xs outline-hidden hover:text-primary hover:bg-neutral-200 dark:hover:bg-neutral-700", ), menu: () => cn( `dark:bg-black text-foreground bg-white dark:text-white border shadow-xs outline-hidden dark:border-foreground border-neutral-300 overflow-hidden`, menuOpen ? "animate-in fade-in-0 zoom-in-90" : "animate-out fade-out-0 zoom-out-90", ), multiValueLabel: () => cn( "dark:bg-black rounded-lg text-primary bg-white dark:text-white outline-hidden", ), multiValue: () => cn( "overflow-hidden dark:bg-black rounded-lg text-primary bg-white dark:text-white border border-neutral-300 dark:border-foreground shadow-xs outline-hidden", ), multiValueRemove: () => cn("rounded-s-none rounded-e-md"), input: () => cn("*:ring-0 focus:*:ring-0"), singleValue: () => cn("text-primary font-medium"), }} classNamePrefix="select" /> ); }; type UnpackOptions = { options: string[]; }; export const UnpackOptions = ({ options }: UnpackOptions) => { return options.map((v, idx) => ({ value: idx.toString(), label: v })); }; type RepackValues = { values: string | string[]; options: string[]; }; export const RepackValues = ({ values, options }: RepackValues) => { if (Array.isArray(values)) { return values.map((v) => ({ // biome-ignore lint/style/noNonNullAssertion: label: options[Number.parseInt(v)]!, value: v, })); } return { // biome-ignore lint/style/noNonNullAssertion: label: options[Number.parseInt(values)]!, value: values, }; };