/
aaronair
/
Test_task_rt_solution
Обзор
Документация
Войти
/
aaronair
/
Test_task_rt_solution
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/components/MultiSelect.tsx
114 строк
4 KB
Ilikedrinkpivo
first_commit
28 май 2026, 14:51
28 май 2026, 14:51
b4498fc
Код
Авторство
О чём код?
import { useEffect, useRef, useState } from "react"; import { cn } from "@/lib/cn"; export interface MultiSelectOption { value: string; label: string; } interface Props { label: string; options: MultiSelectOption[]; selected: string[]; onToggle: (value: string) => void; disabled?: boolean; } export function MultiSelect({ label, options, selected, onToggle, disabled }: Props) { const [open, setOpen] = useState(false); const ref = useRef<HTMLDivElement>(null); useEffect(() => { if (!open) return; function onClick(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) { setOpen(false); } } function onKey(e: KeyboardEvent) { if (e.key === "Escape") setOpen(false); } document.addEventListener("mousedown", onClick); document.addEventListener("keydown", onKey); return () => { document.removeEventListener("mousedown", onClick); document.removeEventListener("keydown", onKey); }; }, [open]); return ( <div ref={ref} className="relative"> <button type="button" disabled={disabled} onClick={() => !disabled && setOpen((p) => !p)} className={cn( "inline-flex items-center gap-2 rounded-lg border bg-surface-card px-3 py-2 text-sm font-medium transition-colors", disabled && "cursor-not-allowed opacity-50", !disabled && selected.length > 0 ? "border-primary-300 text-primary-700 bg-primary-50" : !disabled && "border-ink-300/60 text-ink-700 hover:bg-surface-muted", disabled && "border-ink-300/60 text-ink-500 bg-surface-muted", )} aria-expanded={open} aria-haspopup="listbox" > <span>{label}</span> {selected.length > 0 && ( <span className="inline-flex items-center justify-center rounded-full bg-primary-600 text-white text-xs font-semibold h-5 min-w-5 px-1.5"> {selected.length} </span> )} <svg viewBox="0 0 24 24" className={cn( "h-4 w-4 text-ink-500 transition-transform", open && "rotate-180", )} fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" > <path d="m6 9 6 6 6-6" /> </svg> </button> {open && ( <div role="listbox" aria-label={label} className="absolute z-20 mt-2 w-72 max-h-80 overflow-y-auto rounded-lg border border-ink-300/60 bg-surface-card shadow-cardHover p-1.5" > {options.length === 0 ? ( <div className="px-3 py-2 text-sm text-ink-500">Нет вариантов</div> ) : ( options.map((opt) => { const checked = selected.includes(opt.value); return ( <label key={opt.value} className={cn( "flex items-center gap-2.5 rounded-md px-2.5 py-2 text-sm cursor-pointer", "hover:bg-surface-muted", checked && "bg-primary-50/60", )} > <input type="checkbox" className="h-4 w-4 rounded border-ink-300 text-primary-600 focus:ring-primary-500" checked={checked} onChange={() => onToggle(opt.value)} /> <span className="text-ink-800">{opt.label}</span> </label> ); }) )} </div> )} </div> ); }