This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
import { useEffect, useRef, useState, type FC, type MouseEvent } from 'react'
|
||||
import { ArrowDown2, CloseCircle, TickSquare } from 'iconsax-react'
|
||||
import { clx } from '../helpers/utils'
|
||||
import type { ItemsSelectType } from './Select'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
items: ItemsSelectType[]
|
||||
value: string[]
|
||||
onChange: (value: string[]) => void
|
||||
onBlur?: () => void
|
||||
error_text?: string
|
||||
placeholder?: string
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const MultiSelect: FC<Props> = ({
|
||||
className,
|
||||
items,
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
error_text,
|
||||
placeholder = 'انتخاب کنید',
|
||||
label,
|
||||
disabled,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||
const selectedValues = value ?? []
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
|
||||
if (isOpen) {
|
||||
setIsOpen(false)
|
||||
onBlur?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [isOpen, onBlur])
|
||||
|
||||
const selectedItems = items.filter((item) =>
|
||||
selectedValues.includes(String(item.value)),
|
||||
)
|
||||
|
||||
const toggleValue = (itemValue: string) => {
|
||||
if (selectedValues.includes(itemValue)) {
|
||||
onChange(selectedValues.filter((v) => v !== itemValue))
|
||||
} else {
|
||||
onChange([...selectedValues, itemValue])
|
||||
}
|
||||
}
|
||||
|
||||
const handleClear = (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
onChange([])
|
||||
onBlur?.()
|
||||
}
|
||||
|
||||
const displayText =
|
||||
selectedItems.length === 0
|
||||
? placeholder
|
||||
: selectedItems.length <= 2
|
||||
? selectedItems.map((item) => item.label).join('، ')
|
||||
: `${selectedItems.length} مورد انتخاب شده`
|
||||
|
||||
return (
|
||||
<div className={clx('w-full', className)} ref={wrapperRef}>
|
||||
{label && (
|
||||
<label className="text-sm text-primary-content">{label}</label>
|
||||
)}
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => setIsOpen((open) => !open)}
|
||||
className={clx(
|
||||
'w-full bg-white border border-border input-surface relative block appearance-none px-2.5 h-10 text-sm rounded-[10px] transition-colors text-right',
|
||||
selectedItems.length === 0 && 'text-[#8c90a3]',
|
||||
selectedItems.length > 0 && 'pl-10',
|
||||
label && 'mt-1',
|
||||
disabled && 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
>
|
||||
<span className="block truncate pr-6">{displayText}</span>
|
||||
<ArrowDown2
|
||||
size={16}
|
||||
color="#8c90a3"
|
||||
className="absolute z-0 top-3 left-2 pointer-events-none"
|
||||
/>
|
||||
</button>
|
||||
{selectedItems.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
className="absolute top-0 bottom-0 my-auto left-8 z-10 flex items-center"
|
||||
aria-label="پاک کردن"
|
||||
>
|
||||
<CloseCircle size={16} color="#8C90A3" />
|
||||
</button>
|
||||
)}
|
||||
{isOpen && (
|
||||
<div className="absolute z-20 top-full left-0 right-0 mt-1 bg-white border border-border rounded-[10px] shadow-lg max-h-60 overflow-auto">
|
||||
{items.length === 0 ? (
|
||||
<div className="px-3 py-2.5 text-xs text-[#8c90a3]">
|
||||
موردی برای انتخاب نیست
|
||||
</div>
|
||||
) : (
|
||||
<ul className="py-1">
|
||||
{items.map((item) => {
|
||||
const itemValue = String(item.value)
|
||||
const isSelected = selectedValues.includes(itemValue)
|
||||
return (
|
||||
<li key={itemValue}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleValue(itemValue)}
|
||||
className={clx(
|
||||
'w-full text-right px-3 py-2 text-sm hover:bg-muted transition-colors flex items-center justify-between gap-2',
|
||||
isSelected && 'bg-blue-50 text-[#0037FF]',
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{item.label}</span>
|
||||
{isSelected && (
|
||||
<TickSquare size={16} color="#0037FF" className="shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{selectedItems.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{selectedItems.map((item) => (
|
||||
<span
|
||||
key={String(item.value)}
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-border bg-[#FBFCFF] px-2 py-1 text-xs text-[#1F2937]"
|
||||
>
|
||||
{item.label}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleValue(String(item.value))}
|
||||
aria-label={`حذف ${item.label}`}
|
||||
className="text-[#8c90a3] hover:text-red-500"
|
||||
>
|
||||
<CloseCircle size={12} color="currentColor" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{error_text ? (
|
||||
<div className="text-xs text-right text-red-600 mt-2 mr-2 font-medium">
|
||||
{error_text}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MultiSelect
|
||||
Reference in New Issue
Block a user