Sobes.tech
Middle

Exercice 1 Nous avons un tableau de 10 000 objets (par exemple, "Employés" ou "Produits"). Nous devons implémenter un composant pour sélectionner un élément qui ne "bloquera" pas le navigateur et sera pratique pour l'utilisateur. Ce qu'il faut implémenter (Exigences): 1. Typage des données : Créer une interface Item avec les champs id : string, label : string et category : string. 2. Recherche (Input) : Lors de la saisie de texte, la liste doit être filtrée. 3. Travail avec le DOM et les événements (Navigation au clavier) : * L'utilisateur doit pouvoir se déplacer dans la liste avec les flèches Haut et Bas. 4. En appuyant sur Entrée, l'élément sélectionné est "confirmé". 5. Point important : L'élément sélectionné par les flèches doit toujours être visible (faire défiler le conteneur si le curseur sort des limites). 6. Optimisation (Performance) : * Étant donné qu'il y a beaucoup de données (10k), discuter ou implémenter un "rendering paresseux" simple (ne rendre que les 20-30 premiers éléments ou utiliser IntersectionObserver). Sortie du composant

sobes.tech IA

Réponse de l'IA

Для реализации компонента выбора из большого массива (10 000 элементов) с поиском и навигацией клавиатурой можно сделать так:

interface Item {
  id: string;
  label: string;
  category: string;
}

const ITEMS_PER_PAGE = 30;

function ItemSelector({ items }: { items: Item[] }) {
  const [query, setQuery] = React.useState('');
  const [filteredItems, setFilteredItems] = React.useState<Item[]>(items);
  const [highlightedIndex, setHighlightedIndex] = React.useState(0);
  const containerRef = React.useRef<HTMLDivElement>(null);

  React.useEffect(() => {
    const filtered = items.filter(item =>
      item.label.toLowerCase().includes(query.toLowerCase())
    );
    setFilteredItems(filtered);
    setHighlightedIndex(0);
  }, [query, items]);

  // Обработка клавиш
  function onKeyDown(e: React.KeyboardEvent) {
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      setHighlightedIndex(i => Math.min(i + 1, filteredItems.length - 1));
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      setHighlightedIndex(i => Math.max(i - 1, 0));
    } else if (e.key === 'Enter') {
      e.preventDefault();
      const selected = filteredItems[highlightedIndex];
      if (selected) alert(`Выбран: ${selected.label}`);
    }
  }

  // Скролл к выделенному элементу
  React.useEffect(() => {
    const container = containerRef.current;
    if (!container) return;
    const itemElements = container.querySelectorAll('.item');
    const current = itemElements[highlightedIndex] as HTMLElement | undefined;
    if (current) {
      const containerTop = container.scrollTop;
      const containerBottom = containerTop + container.clientHeight;
      const elemTop = current.offsetTop;
      const elemBottom = elemTop + current.offsetHeight;

      if (elemTop < containerTop) {
        container.scrollTop = elemTop;
      } else if (elemBottom > containerBottom) {
        container.scrollTop = elemBottom - container.clientHeight;
      }
    }
  }, [highlightedIndex]);

  // Ленивый рендеринг первых ITEMS_PER_PAGE элементов
  const visibleItems = filteredItems.slice(0, ITEMS_PER_PAGE);

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={e => setQuery(e.target.value)}
        onKeyDown={onKeyDown}
        placeholder="Поиск..."
      />
      <div
        ref={containerRef}
        style={{ maxHeight: 300, overflowY: 'auto', border: '1px solid #ccc' }}
        tabIndex={0}
        onKeyDown={onKeyDown}
      >
        {visibleItems.map((item, index) => (
          <div
            key={item.id}
            className="item"
            style={{
              padding: '4px 8px',
              backgroundColor: index === highlightedIndex ? '#bde4ff' : 'transparent',
            }}
          >
            {item.label} ({item.category})
          </div>
        ))}
        {filteredItems.length > ITEMS_PER_PAGE && (
          <div style={{ padding: 8, fontStyle: 'italic' }}>
            Показано {ITEMS_PER_PAGE} из {filteredItems.length} элементов
          </div>
        )}
      </div>
    </div>
  );
}

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

  • Типизация через интерфейс Item.
  • Фильтрация по вводу пользователя.
  • Навигация стрелками и подтверждение Enter.
  • Скролл к выделенному элементу, чтобы он всегда был виден.
  • Ленивый рендеринг — показываем только первые 30 элементов, чтобы не перегружать DOM.

Для более продвинутой оптимизации можно использовать виртуализацию (например, react-window) или IntersectionObserver для подгрузки элементов по мере прокрутки.