infoalloggi-monorepo/apps/infoalloggi/src/components/custom_ui/multiselect.tsx

294 lines
7.1 KiB
TypeScript

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<ListOption>
>,
) => {
const rows = props.children as JSX.Element[];
const parentRef = useRef<HTMLDivElement>(null);
const count = rows.length;
const virtualizer = useVirtualizer({
count,
getScrollElement: () => parentRef.current,
estimateSize: () => 45,
});
const items = virtualizer.getVirtualItems();
return (
<div
ref={parentRef}
className="List"
style={{
height: `${virtualizer.getTotalSize()}px`,
maxHeight: "300px",
width: "auto",
overflowY: "auto",
contain: "strict",
}}
>
<div
style={{
height: virtualizer.getTotalSize(),
width: "100%",
position: "relative",
}}
>
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${items[0]?.start ?? 0}px)`,
}}
>
{items.map((virtualRow) => (
<div
key={virtualRow.key}
data-index={virtualRow.index}
ref={virtualizer.measureElement}
className={virtualRow.index % 2 ? "ListItemOdd" : "ListItemEven"}
>
{rows[virtualRow.index]}
</div>
))}
</div>
</div>
</div>
);
};
/**
*
* @example
* <MultiSelect
* elementId="select-naz"
* elementName="nazioneNascita"
* options={UnpackOptions({ options: nazioni_options })}
* defaultValue={RepackValues({
* options: nazioni_options,
* values: field.value,
* })}
* onValueChange={(value) => {
* 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 (
<ReactSelect
menuPlacement={menuPlacement}
components={{ MenuList: VirtualizedMenuList }}
inputId={elementId}
isMulti={isMulti}
isDisabled={disabled}
instanceId={elementId}
name={elementName}
options={options}
onChange={(values) => {
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: <intended>
label: options[Number.parseInt(v)]!,
value: v,
}));
}
return {
// biome-ignore lint/style/noNonNullAssertion: <intended>
label: options[Number.parseInt(values)]!,
value: values,
};
};