Sobes.tech
Middle

Taak 1 We hebben een array van 10.000 objecten (bijvoorbeeld "Medewerkers" of "Producten"). We moeten een component implementeren voor het selecteren van een element dat de browser niet "bevriezen" en gebruiksvriendelijk is. Wat moet worden geïmplementeerd (Vereisten): 1. Typing van gegevens: Maak een Item-interface met velden id: string, label: string en category: string. 2. Zoeken (Input): Bij het invoeren van tekst moet de lijst worden gefilterd. 3. Werken met DOM en gebeurtenissen (Toetsenbordinvoer): * De gebruiker moet door de lijst kunnen navigeren met de pijltjestoetsen Omhoog en Omlaag. 4. Bij het indrukken van Enter wordt het geselecteerde element "bevestigd". 5. Belangrijk punt: Het door pijltjes geselecteerde element moet altijd zichtbaar zijn (programmeer het scrollen van de container als de cursor buiten de grenzen gaat). 6. Optimalisatie (Prestaties): * Aangezien er veel gegevens zijn (10k), bespreek of implementeer een eenvoudige "lazy rendering" (alleen de eerste 20-30 elementen renderen of gebruik IntersectionObserver). Uitgang van de component

sobes.tech AI

Antwoord van AI

Для реализации компонента выбора из большого массива (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 для подгрузки элементов по мере прокрутки.