File size: 4,697 Bytes
f0743f4 | 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 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | import React, { useRef } from 'react';
import {
Select,
SelectArrow,
SelectItem,
SelectItemCheck,
SelectLabel,
SelectPopover,
SelectProvider,
} from '@ariakit/react';
import './AnimatePopover.css';
import { cn } from '~/utils';
interface MultiSelectProps<T extends string> {
items: T[];
label?: string;
placeholder?: string;
onSelectedValuesChange?: (values: T[]) => void;
renderSelectedValues?: (values: T[], placeholder?: string) => React.ReactNode;
className?: string;
itemClassName?: string;
labelClassName?: string;
selectClassName?: string;
selectIcon?: React.ReactNode;
popoverClassName?: string;
selectItemsClassName?: string;
selectedValues: T[];
setSelectedValues: (values: T[]) => void;
renderItemContent?: (
value: T,
defaultContent: React.ReactNode,
isSelected: boolean,
) => React.ReactNode;
}
function defaultRender<T extends string>(values: T[], placeholder?: string) {
if (values.length === 0) {
return placeholder || 'Select...';
}
if (values.length === 1) {
return values[0];
}
return `${values.length} items selected`;
}
export default function MultiSelect<T extends string>({
items,
label,
placeholder = 'Select...',
onSelectedValuesChange,
renderSelectedValues = defaultRender,
className,
selectIcon,
itemClassName,
labelClassName,
selectClassName,
popoverClassName,
selectItemsClassName,
selectedValues = [],
setSelectedValues,
renderItemContent,
}: MultiSelectProps<T>) {
const selectRef = useRef<HTMLButtonElement>(null);
const handleValueChange = (values: T[]) => {
setSelectedValues(values);
if (onSelectedValuesChange) {
onSelectedValuesChange(values);
}
};
return (
<div className={className}>
<SelectProvider value={selectedValues} setValue={handleValueChange}>
{label && (
<SelectLabel className={cn('mb-1 block text-sm text-text-primary', labelClassName)}>
{label}
</SelectLabel>
)}
<Select
ref={selectRef}
className={cn(
'flex items-center justify-between gap-2 rounded-xl px-3 py-2 text-sm',
'bg-surface-tertiary text-text-primary shadow-sm hover:cursor-pointer hover:bg-surface-hover',
'outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75',
selectClassName,
selectedValues.length > 0 && selectItemsClassName != null && selectItemsClassName,
)}
onChange={(e) => e.stopPropagation()}
>
{selectIcon && <span>{selectIcon as React.JSX.Element}</span>}
<span className="mr-auto hidden truncate md:block">
{renderSelectedValues(selectedValues, placeholder)}
</span>
<SelectArrow className="ml-1 hidden stroke-1 text-base opacity-75 md:block" />
</Select>
<SelectPopover
gutter={4}
sameWidth
modal
unmountOnHide
finalFocus={selectRef}
className={cn(
'animate-popover z-50 flex max-h-[300px]',
'flex-col overflow-auto overscroll-contain rounded-xl',
'bg-surface-secondary px-1.5 py-1 text-text-primary shadow-lg',
'border border-border-light',
'outline-none',
popoverClassName,
)}
>
{items.map((value) => {
const defaultContent = (
<>
<SelectItemCheck className="mr-0.5 text-primary" />
<span className="truncate">{value}</span>
</>
);
const isCurrentItemSelected = selectedValues.includes(value);
return (
<SelectItem
key={value}
value={value}
className={cn(
'flex items-center gap-2 rounded-lg px-2 py-1.5 hover:cursor-pointer',
'scroll-m-1 outline-none transition-colors',
'hover:bg-black/[0.075] dark:hover:bg-white/10',
'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10',
'w-full min-w-0 text-sm',
itemClassName,
)}
>
{renderItemContent
? (renderItemContent(
value,
defaultContent,
isCurrentItemSelected,
) as React.JSX.Element)
: (defaultContent as React.JSX.Element)}
</SelectItem>
);
})}
</SelectPopover>
</SelectProvider>
</div>
);
}
|