mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-02-14 04:11:11 -05:00
128 lines
3.8 KiB
TypeScript
128 lines
3.8 KiB
TypeScript
'use client';
|
|
|
|
import { Button } from '@workspace/ui/components/button';
|
|
import {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
} from '@workspace/ui/components/command';
|
|
import { Popover, PopoverContent, PopoverTrigger } from '@workspace/ui/components/popover';
|
|
import { cn } from '@workspace/ui/lib/utils';
|
|
import { BoxIcon, CheckIcon, ChevronsUpDownIcon } from 'lucide-react';
|
|
import * as React from 'react';
|
|
|
|
export type Option<T = string> = {
|
|
value: T;
|
|
label: string;
|
|
children?: React.ReactNode;
|
|
};
|
|
|
|
// Conditional types to determine the value type for onChange
|
|
type OnChangeType<T, M extends boolean> = M extends true ? T[] : T;
|
|
|
|
type ComboboxProps<T = string, M extends boolean = false> = {
|
|
multiple?: M;
|
|
options?: Option<T>[];
|
|
placeholder?: string;
|
|
value?: OnChangeType<T, M>;
|
|
onChange: (value: OnChangeType<T, M>) => void;
|
|
className?: string;
|
|
};
|
|
|
|
export function Combobox<T, M extends boolean = false>({
|
|
multiple = false as M,
|
|
options = [],
|
|
placeholder = 'Select...',
|
|
value,
|
|
onChange,
|
|
className,
|
|
}: ComboboxProps<T, M>) {
|
|
const [open, setOpen] = React.useState(false);
|
|
|
|
const handleSelect = (selectedValue: T) => {
|
|
if (multiple) {
|
|
const newValue = Array.isArray(value) ? [...value] : [];
|
|
|
|
if (newValue.includes(selectedValue)) {
|
|
newValue.splice(newValue.indexOf(selectedValue), 1);
|
|
onChange(newValue as OnChangeType<T, M>);
|
|
} else {
|
|
onChange([...newValue, selectedValue] as OnChangeType<T, M>);
|
|
}
|
|
} else {
|
|
const newValue = selectedValue === value ? ('' as T) : selectedValue;
|
|
onChange(newValue as OnChangeType<T, M>);
|
|
setOpen(false);
|
|
}
|
|
};
|
|
|
|
const renderButtonLabel = () => {
|
|
if (multiple && Array.isArray(value) && value.length > 0) {
|
|
const selectedLabels = options
|
|
.filter((option) => value.includes(option.value))
|
|
.map((option) => option.label)
|
|
.join(', ');
|
|
|
|
return selectedLabels;
|
|
} else if (!multiple) {
|
|
const selectedOption = options.find((option) => option.value === value);
|
|
|
|
return selectedOption ? selectedOption.label : placeholder;
|
|
}
|
|
|
|
return placeholder;
|
|
};
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant='outline'
|
|
role='combobox'
|
|
aria-expanded={open}
|
|
className={cn('w-full items-center justify-between', className)}
|
|
>
|
|
<span className='truncate'>{renderButtonLabel()}</span>
|
|
<ChevronsUpDownIcon className='ml-2 size-4 shrink-0 opacity-50' />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className='w-fit p-0' align='start'>
|
|
<Command>
|
|
<CommandInput placeholder='Search...' className='h-9' />
|
|
<CommandEmpty>
|
|
<BoxIcon className='inline-block text-slate-500' />
|
|
</CommandEmpty>
|
|
<CommandGroup>
|
|
<CommandList>
|
|
{options.map((option) => (
|
|
<CommandItem
|
|
key={String(option.label + option.value)}
|
|
value={option.label + option.value}
|
|
onSelect={() => handleSelect(option.value)}
|
|
>
|
|
{option.children || option.label}
|
|
<CheckIcon
|
|
className={cn(
|
|
'ml-auto h-4 w-4',
|
|
multiple
|
|
? Array.isArray(value) && value.includes(option.value)
|
|
? 'opacity-100'
|
|
: 'opacity-0'
|
|
: value === option.value
|
|
? 'opacity-100'
|
|
: 'opacity-0',
|
|
)}
|
|
/>
|
|
</CommandItem>
|
|
))}
|
|
</CommandList>
|
|
</CommandGroup>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
}
|