Autocomplete и combobox

Компоненты autocomplete и combobox относятся к категории интерактивных элементов ввода, которые объединяют текстовое поле и выпадающий список вариантов. Основная задача — ускорение ввода и снижение количества ошибок за счёт динамических подсказок.

В терминах UI:

  • Autocomplete — поле ввода с подсказками, которые фильтруются по мере ввода текста
  • Combobox — более строгая форма autocomplete, часто с возможностью выбора только из списка

Библиотека Floating UI предоставляет низкоуровневые инструменты для позиционирования, управления взаимодействиями и доступности таких компонентов, не навязывая конкретную реализацию.


Архитектура решения

Реализация autocomplete/combobox строится из нескольких независимых частей:

  1. Reference element — поле ввода
  2. Floating element — список подсказок
  3. Состояние — открыто/закрыто, текущий ввод, активный элемент
  4. Навигация — клавиатура и мышь
  5. Позиционирование — вычисляется через Floating UI

Ключевой принцип — отделение логики позиционирования от логики взаимодействия.


Базовая настройка Floating UI

Для создания выпадающего списка используется хук useFloating:

import { useFloating, offset, flip, shift } from '@floating-ui/react';

const { refs, floatingStyles, context } = useFloating({
  placement: 'bottom-start',
  middleware: [
    offset(4),
    flip(),
    shift()
  ]
});
  • refs.setReference — привязывается к input
  • refs.setFloating — привязывается к списку
  • floatingStyles — стили позиционирования

Управление открытием списка

Состояние открытия контролируется вручную:

const [open, setOpen] = useState(false);

Часто используется комбинация:

  • открытие при фокусе
  • закрытие при blur или выборе

С помощью useInteractions:

import { useInteractions, useDismiss, useRole } from '@floating-ui/react';

const dismiss = useDismiss(context);
const role = useRole(context, { role: 'listbox' });

const { getReferenceProps, getFloatingProps } = useInteractions([
  dismiss,
  role
]);

Обработка ввода и фильтрация

Autocomplete предполагает динамическую фильтрацию данных:

const [query, setQuery] = useState('');

const filteredItems = items.filter(item =>
  item.toLowerCase().includes(query.toLowerCase())
);

Input связывается с состоянием:

<input
  ref={refs.setReference}
  value={query}
  onCha nge={(e) => {
    setQuery(e.target.value);
    setOpen(true);
  }}
/>

Навигация по списку

Floating UI предоставляет хук useListNavigation для управления активным элементом:

import { useListNavigation } from '@floating-ui/react';

const listRef = useRef([]);
const [activeIndex, setActiveIndex] = useState(null);

const listNavigation = useListNavigation(context, {
  listRef,
  activeIndex,
  onNavigate: setActiveIndex,
  loop: true
});

Особенности:

  • стрелки вверх/вниз перемещают активный элемент
  • loop позволяет зацикливать список
  • поддержка Home/End

Выбор элемента

Выбор можно реализовать через клик или клавишу Enter:

const handleSelect = (index) => {
  setQuery(filteredItems[index]);
  setOpen(false);
};

Интеграция с клавиатурой:

import { useTypeahead } from '@floating-ui/react';

const typeahead = useTypeahead(context, {
  listRef,
  activeIndex,
  onMatch: setActiveIndex
});

Рендер списка

{open && (
  <ul
    ref={refs.setFloating}
    style={floatingStyles}
    {...getFloatingProps()}
  >
    {filteredItems.map((item, index) => (
      <li
        key={item}
        ref={(node) => (listRef.current[index] = node)}
        onCl ick={() => handleSelect(index)}
        style={{
          background: index === activeIndex ? '#eee' : 'white'
        }}
      >
        {item}
      </li>
    ))}
  </ul>
)}

Роль и доступность (ARIA)

Combobox требует строгого соблюдения ARIA-спецификации:

Input

<input
  role="combobox"
  aria-expanded={open}
  aria-controls="listbox-id"
  aria-autocomplete="list"
/>

Список

<ul role="listbox">

Элемент списка

<li role="option" aria-selected={active}>

Floating UI автоматически помогает с ролями через useRole.


Управление фокусом

Для корректной работы клавиатуры используется FloatingFocusManager:

import { FloatingFocusManager } from '@floating-ui/react';

{open && (
  <FloatingFocusManager context={context} modal={false}>
    <ul>...</ul>
  </FloatingFocusManager>
)}

Особенности:

  • предотвращает потерю фокуса
  • позволяет навигацию внутри списка
  • modal={false} — не блокирует остальной интерфейс

Обработка кликов вне компонента

useDismiss автоматически закрывает список при клике вне:

const dismiss = useDismiss(context, {
  outsidePress: true
});

Дополнительно:

  • Escape закрывает список
  • Blur можно контролировать вручную

Продвинутые сценарии

Асинхронные данные

useEffect(() => {
  if (!query) return;

  fetch(`/api?q=${query}`)
    .then(res => res.json())
    .then(setItems);
}, [query]);

Важно учитывать:

  • debounce
  • отмену предыдущих запросов
  • состояние загрузки

Виртуализация списка

При большом количестве элементов:

  • используется react-virtual
  • список рендерит только видимые элементы
  • Floating UI продолжает корректно позиционировать контейнер

Кастомные элементы

Autocomplete не ограничен текстом:

<li>
  <img src={avatar} />
  <span>{name}</span>
</li>

Важно:

  • сохранять listRef
  • корректно обрабатывать focus/hover

Множественный выбор

Combobox может поддерживать multiple selection:

  • выбранные элементы отображаются как теги
  • input очищается после выбора
  • список остаётся открытым

Оптимизация производительности

Ключевые моменты:

  • мемоизация фильтрации (useMemo)
  • минимизация перерендеров списка
  • использование React.memo для элементов

Типичные ошибки

1. Потеря фокуса при клике

  • причина: blur срабатывает раньше click
  • решение: использовать mousedown или FloatingFocusManager

2. Некорректная навигация

  • забытый listRef
  • несинхронизированный activeIndex

3. Проблемы с позиционированием

  • отсутствует shift или flip
  • контейнер с overflow:hidden

Сравнение Autocomplete и Combobox

Характеристика Autocomplete Combobox
Свободный ввод Да Ограничен
Строгий выбор Нет Да
ARIA требования Умеренные Строгие
Использование Поиск, фильтрация Формы, селекты

Интеграция с формами

Combobox часто используется как замена <select>:

  • значение хранится в состоянии
  • синхронизируется с form libraries (Formik, React Hook Form)
  • возможна сериализация выбранного элемента

Расширяемость

Floating UI не ограничивает реализацию:

  • можно добавлять группы элементов
  • поддержка вложенных списков
  • кастомные стратегии фильтрации (fuzzy search)

Вывод ключевых принципов

  • позиционирование полностью отделено от логики
  • управление состоянием остаётся на стороне разработчика
  • доступность достигается через ARIA + хуки
  • гибкость позволяет строить как простые, так и сложные интерфейсы