/
kan64
/
spreadsheet-lab
Обзор
Документация
Войти
/
kan64
/
spreadsheet-lab
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
client/src/components/SelectDropdown.tsx
150 строк
5 KB
Anton Kravchenkov
feat(ui): Select integration
04 мар 2026, 22:37
04 мар 2026, 22:37
6ec6b1b
Код
Авторство
О чём код?
import React, { useState, useCallback, useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; export interface SelectDropdownOption<T = string> { value: T; label: string; } export interface SelectDropdownProps<T = string> { value: T; onChange: (value: T) => void; options: SelectDropdownOption<T>[]; placeholder?: string; disabled?: boolean; className?: string; } export function SelectDropdown<T = string>({ value, onChange, options, placeholder = 'Выберите...', disabled = false, className = '', }: SelectDropdownProps<T>) { const [showDropdown, setShowDropdown] = useState(false); const [highlightIdx, setHighlightIdx] = useState(-1); const [dropdownPos, setDropdownPos] = useState<{ top: number; left: number; width: number } | null>(null); const wrapperRef = useRef<HTMLDivElement>(null); const triggerRef = useRef<HTMLButtonElement>(null); const dropdownRef = useRef<HTMLDivElement>(null); const selectedOption = options.find((o) => o.value === value); const displayLabel = selectedOption?.label ?? placeholder; const updateDropdownPos = useCallback(() => { if (triggerRef.current) { const rect = triggerRef.current.getBoundingClientRect(); setDropdownPos({ top: rect.bottom + 4, left: rect.left, width: rect.width }); } }, []); const openDropdown = useCallback(() => { updateDropdownPos(); setShowDropdown(true); const idx = options.findIndex((o) => o.value === value); setHighlightIdx(idx >= 0 ? idx : 0); }, [updateDropdownPos, options, value]); const closeDropdown = useCallback(() => { setShowDropdown(false); }, []); useEffect(() => { if (!showDropdown) return; const handleClickOutside = (e: MouseEvent) => { const target = e.target as Node; const inWrapper = wrapperRef.current?.contains(target); const inDropdown = dropdownRef.current?.contains(target); if (!inWrapper && !inDropdown) { closeDropdown(); } }; const handleScroll = () => updateDropdownPos(); document.addEventListener('mousedown', handleClickOutside, true); window.addEventListener('scroll', handleScroll, true); return () => { document.removeEventListener('mousedown', handleClickOutside, true); window.removeEventListener('scroll', handleScroll, true); }; }, [showDropdown, closeDropdown, updateDropdownPos]); const handleSelect = (opt: SelectDropdownOption<T>) => { onChange(opt.value); closeDropdown(); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (showDropdown) { if (e.key === 'ArrowDown') { e.preventDefault(); setHighlightIdx((prev) => (prev < options.length - 1 ? prev + 1 : 0)); } else if (e.key === 'ArrowUp') { e.preventDefault(); setHighlightIdx((prev) => (prev > 0 ? prev - 1 : options.length - 1)); } else if (e.key === 'Enter' && highlightIdx >= 0) { e.preventDefault(); handleSelect(options[highlightIdx]); return; } else if (e.key === 'Escape') { closeDropdown(); return; } } else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openDropdown(); } }; const dropdownPortal = showDropdown && dropdownPos ? createPortal( <div ref={dropdownRef} className="searchable-dropdown-list" style={{ top: dropdownPos.top, left: dropdownPos.left, width: dropdownPos.width }} > {options.map((opt, idx) => ( <button key={idx} className={`searchable-dropdown-item${idx === highlightIdx ? ' highlighted' : ''}`} onMouseDown={(e) => { e.preventDefault(); handleSelect(opt); }} onMouseEnter={() => setHighlightIdx(idx)} > {opt.label} </button> ))} </div>, document.body, ) : null; return ( <div ref={wrapperRef} className={`select-dropdown-wrapper ${className}`}> <button ref={triggerRef} type="button" className="select-dropdown-trigger" onClick={openDropdown} onKeyDown={handleKeyDown} disabled={disabled} > <span className={selectedOption ? '' : 'select-dropdown-placeholder'}> {displayLabel} </span> </button> <button type="button" className={`searchable-dropdown-chevron${showDropdown ? ' open' : ''}`} onMouseDown={(e) => { e.preventDefault(); showDropdown ? closeDropdown() : openDropdown(); }} tabIndex={-1} disabled={disabled} > <svg width="16" height="16" viewBox="0 0 16 16" fill="none"> <path d="M4 6L8 10L12 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> </button> {dropdownPortal} </div> ); }