← Назад к вопросам

Как настраивал кастомный select?

1.3 Junior🔥 71 комментариев
#Soft Skills и рабочие процессы

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Архитектура кастомного select

Кастомный select требует управления состоянием, видимостью выпадающего списка, обработки клавиатурных событий и фильтрации опций. Самый надёжный подход — разделить на три компонента: Select (контейнер), SelectTrigger (кнопка открытия) и SelectContent (список опций).

Реализация с React Hooks

import { useState, useRef, useEffect } from 'react';

interface SelectProps {
  value: string;
  onChange: (value: string) => void;
  options: Array<{ value: string; label: string }>;
  placeholder?: string;
}

export function Select({ value, onChange, options, placeholder = 'Выберите...' }: SelectProps) {
  const [isOpen, setIsOpen] = useState(false);
  const [searchTerm, setSearchTerm] = useState('');
  const containerRef = useRef<HTMLDivElement>(null);

  const filteredOptions = options.filter(opt =>
    opt.label.toLowerCase().includes(searchTerm.toLowerCase())
  );

  // Закрытие при клике вне компонента
  useEffect(() => {
    function handleClickOutside(e: MouseEvent) {
      if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
        setIsOpen(false);
      }
    }
    
    document.addEventListener('mousedown', handleClickOutside);
    return () => document.removeEventListener('mousedown', handleClickOutside);
  }, []);

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Escape') setIsOpen(false);
    if (e.key === 'Enter' && filteredOptions.length > 0) {
      onChange(filteredOptions[0].value);
      setIsOpen(false);
    }
  };

  return (
    <div ref={containerRef} className="relative w-full">
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="w-full px-4 py-2 border rounded-lg text-left"
      >
        {value ? options.find(o => o.value === value)?.label : placeholder}
      </button>

      {isOpen && (
        <div className="absolute top-full mt-1 w-full border rounded-lg bg-white shadow-lg z-10">
          <input
            type="text"
            placeholder="Поиск..."
            value={searchTerm}
            onChange={(e) => setSearchTerm(e.target.value)}
            onKeyDown={handleKeyDown}
            className="w-full px-4 py-2 border-b"
          />
          <ul className="max-h-48 overflow-y-auto">
            {filteredOptions.map(opt => (
              <li
                key={opt.value}
                onClick={() => {
                  onChange(opt.value);
                  setIsOpen(false);
                  setSearchTerm('');
                }}
                className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
              >
                {opt.label}
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
}

Ключевые особенности

Управление фокусом: Автоматическое закрытие при клике вне элемента через useEffect и useRef. Это стандартная практика для всех выпадающих меню.

Фильтрация опций: Поле поиска внутри select значительно улучшает UX при большом количестве опций. Фильтрация работает независимо от основного значения.

Доступность клавиатуры: Клавиша Escape закрывает список, Enter выбирает первую опцию. Это критично для пользователей, работающих только с клавиатурой.

Performance: Не переиспользуется состояние для каждой опции — список рендерится один раз при изменении filteredOptions.

Расширение функционала

Для более сложных случаев добавляешь:

  • Virtual scrolling (если 1000+ опций)
  • Группировку опций по категориям
  • Иконки рядом с опциями
  • Асинхронную загрузку опций

Главное — держать компонент простым и композируемым. Это позволяет легко адаптировать его под конкретные требования проекта.

Как настраивал кастомный select? | PrepBro