Поиск по вводу

Поиск по вводу — один из наиболее распространённых паттернов пользовательского интерфейса. Он применяется в автодополнении, выборе элементов из большого списка, фильтрации данных, командных палитрах, навигации и формах. Основная задача — позволить пользователю вводить текст и получать релевантные результаты в реальном времени.

В библиотеке Radix UI подобная функциональность не представлена в виде одного универсального компонента. Вместо этого она строится из набора примитивов:

  • Combobox-подобные интерфейсы реализуются через Popover, Command, Select или DropdownMenu
  • Фильтрация списка выполняется на уровне логики React
  • Управление фокусом и доступностью обеспечивается компонентами Radix

Такой подход соответствует философии Radix UI: библиотека предоставляет низкоуровневые доступные примитивы, а сложные интерфейсные паттерны собираются из них.


Основная архитектура поиска

Поиск по вводу обычно состоит из нескольких элементов:

  1. Поле ввода (input)
  2. Контейнер результатов
  3. Список элементов
  4. Логика фильтрации
  5. Управление состоянием

Типичная схема взаимодействия:

Input → изменение значения → фильтрация данных → обновление списка

В React состояние поиска чаще всего хранится в useState.

Простейшая структура:

const [query, setQuery] = useState("");
const filteredItems = items.filter(item =>
  item.toLowerCase().includes(query.toLowerCase())
);

Radix UI используется для создания интерфейса списка, навигации по элементам и доступного взаимодействия с клавиатурой.


Построение поиска через Popover

Комбинация Popover и поля ввода позволяет создать интерфейс автодополнения.

Базовая структура

import * as Popover from "@radix-ui/react-popover";
import { useState } from "react";

function SearchBox({ items }) {
  const [query, setQuery] = useState("");

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

  return (
    <Popover.Root>
      <Popover.Trigger asChild>
        <input
          value={query}
          onCha nge={(e) => setQuery(e.target.value)}
          placeholder="Поиск..."
        />
      </Popover.Trigger>

      <Popover.Content>
        <ul>
          {filtered.map(item => (
            <li key={item}>{item}</li>
          ))}
        </ul>
      </Popover.Content>
    </Popover.Root>
  );
}

В этой архитектуре:

  • поле ввода выступает триггером
  • список результатов отображается в Popover
  • фильтрация происходит при каждом изменении ввода

Доступность поиска

Radix UI уделяет большое внимание доступности (accessibility). При реализации поиска необходимо учитывать несколько аспектов:

Навигация с клавиатуры

Пользователь должен иметь возможность:

  • перемещаться по результатам стрелками
  • подтверждать выбор клавишей Enter
  • закрывать список клавишей Escape

Radix решает это через систему roving focus и управление фокусом внутри компонентов.

ARIA-атрибуты

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

role="combobox"
aria-expanded
aria-controls
aria-activedescendant

При использовании компонентов Radix значительная часть этих атрибутов добавляется автоматически.


Использование Command для поиска

Одним из наиболее удобных способов реализации поиска является компонент Command. Он используется для командных палитр и поиска элементов.

Структура:

import * as Command from "cmdk";

function SearchCommand({ items }) {
  return (
    <Command>
      <Command.Input placeholder="Поиск..." />
      <Command.List>
        {items.map(item => (
          <Command.Item key={item}>
            {item}
          </Command.Item>
        ))}
      </Command.List>
    </Command>
  );
}

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

  • встроенная фильтрация
  • навигация клавиатурой
  • управление фокусом
  • оптимизированная производительность

Command фактически реализует готовый интерфейс поиска.


Реализация автодополнения

Автодополнение — частный случай поиска по вводу, при котором предлагаются варианты завершения текста.

Алгоритм

  1. Пользователь вводит часть строки
  2. Система сравнивает ввод с базой данных
  3. Отображаются совпадения

Пример логики:

const suggestions = data.filter(item =>
  item.startsWith(query)
);

Для нечувствительности к регистру:

item.toLowerCase().startsWith(query.toLowerCase())

Фильтрация больших списков

При работе с большими массивами данных (тысячи элементов) простая фильтрация может стать медленной. Применяются следующие техники:

Дебаунс ввода

Дебаунс ограничивает количество вычислений.

import { useDebounce } from "use-debounce";

const [debouncedQuery] = useDebounce(query, 300);

Фильтрация выполняется только после паузы ввода.


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

Если список содержит много элементов, используется виртуализация.

Популярные библиотеки:

  • react-virtual
  • react-window
  • react-virtuoso

Radix UI хорошо интегрируется с ними, так как его компоненты не навязывают структуру DOM.


Асинхронный поиск

Часто данные загружаются с сервера. В этом случае поиск выполняется через API.

Типичный алгоритм:

  1. Пользователь вводит текст
  2. Отправляется HTTP-запрос
  3. Сервер возвращает результаты
  4. Интерфейс обновляется

Пример:

useEffect(() => {
  async function fetchData() {
    const res = await fetch(`/api/search?q=${query}`);
    const data = await res.json();
    setResults(data);
  }

  if (query.length > 2) {
    fetchData();
  }
}, [query]);

Ограничение длины запроса уменьшает нагрузку на сервер.


Индикация загрузки

При асинхронном поиске важно показывать состояние загрузки.

{loading && <div>Загрузка...</div>}

В интерфейсе поиска это обычно реализуется внутри списка результатов.


Обработка пустых результатов

Если совпадений нет, интерфейс должен явно сообщать об этом.

Нет результатов

Пример:

{filtered.length === 0 && (
  <div className="empty">
    Ничего не найдено
  </div>
)}

Подсветка совпадений

Для улучшения UX совпадающая часть текста может подсвечиваться.

Алгоритм:

  1. Найти позицию совпадения
  2. Разделить строку на части
  3. выделить совпадающий фрагмент

Пример:

function highlight(text, query) {
  const index = text.toLowerCase().indexOf(query.toLowerCase());

  if (index === -1) return text;

  const before = text.slice(0, index);
  const match = text.slice(index, index + query.length);
  const after = text.slice(index + query.length);

  return (
    <>
      {before}
      <strong>{match}</strong>
      {after}
    </>
  );
}

Закрытие результатов поиска

Результаты поиска должны скрываться в нескольких ситуациях:

  • потеря фокуса
  • нажатие Escape
  • выбор элемента
  • очистка поля

Radix автоматически управляет этим поведением через Popover или Dialog.


Очистка поля поиска

Поле поиска часто содержит кнопку очистки.

<button onCl ick={() => setQuery("")}>
  ×
</button>

После очистки обычно:

  • скрываются результаты
  • восстанавливается исходный список

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

Фокус — ключевой элемент доступности интерфейса поиска.

Radix решает несколько задач:

  • перенос фокуса в список
  • возврат фокуса в input
  • правильная навигация стрелками

Это достигается благодаря внутренним механизмам управления фокусом.


Интеграция поиска в Select

Компонент Select может быть расширен возможностью поиска.

Структура:

Sel ect
 ├ Trigger
 └ Content
      ├ Input (поиск)
      └ Items

Пример:

<Select.Content>
  <input
    placeholder="Фильтр..."
    onCha nge={(e) => setQuery(e.target.value)}
  />

  {filteredItems.map(item => (
    <Select.Item key={item} value={item}>
      {item}
    </Select.Item>
  ))}
</Select.Content>

Поиск в DropdownMenu

Dropdown-меню может содержать поле фильтрации.

<DropdownMenu.Content>
  <input
    placeholder="Поиск..."
    onCha nge={(e) => setQuery(e.target.value)}
  />

  {filtered.map(item => (
    <DropdownMenu.Item key={item}>
      {item}
    </DropdownMenu.Item>
  ))}
</DropdownMenu.Content>

Такой интерфейс используется в:

  • настройках
  • списках команд
  • меню действий

Управление состоянием выбранного элемента

После выбора элемента необходимо обновить состояние.

const [selected, setSelected] = useState(null);

При выборе:

onSel ect={(value) => setSelected(value)}

Выбранное значение может:

  • подставляться в поле ввода
  • сохраняться в форме
  • использоваться для навигации

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

При реализации поиска важны несколько факторов:

Мемоизация

const filtered = useMemo(() => {
  return items.filter(...)
}, [items, query]);

Это предотвращает лишние вычисления.


Ограничение количества результатов

Часто выводится только часть совпадений:

максимум 10–20 элементов
filtered.slice(0, 10)

Предварительная индексация

Для больших наборов данных используется:

  • Fuse.js (fuzzy search)
  • Lunr.js
  • Elasticlunr

Они позволяют выполнять быстрый поиск по сложным условиям.


Нечёткий поиск позволяет находить результаты даже при ошибках в написании.

Пример:

"react" → "reaact"

Библиотека Fuse.js используется следующим образом:

import Fuse fr om "fuse.js";

const fuse = new Fuse(items, {
  keys: ["name"]
});

const results = fuse.search(query);

Структура сложного поискового интерфейса

Крупные приложения используют более сложную архитектуру:

SearchProvider
 ├ SearchInput
 ├ SearchResults
 ├ SearchItem
 └ SearchEmptyState

Такой подход обеспечивает:

  • повторное использование компонентов
  • изоляцию логики
  • масштабируемость

Командная палитра

Одним из популярных интерфейсов поиска является command palette, аналогичная интерфейсу редакторов кода.

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

  • поиск по действиям
  • мгновенная навигация
  • управление клавиатурой

Radix и cmdk часто используются вместе для реализации такого интерфейса.


Расширенные возможности поиска

В сложных системах поиск может включать:

  • категории результатов
  • недавние запросы
  • историю поиска
  • подсказки
  • поиск по нескольким полям

Пример структуры:

Results
 ├ Recent
 ├ Pages
 ├ Commands
 └ Settings

Каждая категория может иметь собственную фильтрацию.


Поведение при вводе

Современные интерфейсы поиска учитывают ряд UX-паттернов:

  • результаты появляются после 1–2 символов
  • список ограничен по высоте
  • активный элемент подсвечен
  • выбор выполняется клавишей Enter

Radix UI обеспечивает инфраструктуру для реализации всех этих сценариев через доступные примитивы управления фокусом, меню, списками и всплывающими контейнерами.