| 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> |
| ); |
| } |
|
|