Sobes.tech
Middle

Feladat 1 Nekünk van egy 10 000 objektumot tartalmazó tömbünk (például "Alkalmazottak" vagy "Termékek"). Olyan komponens megvalósítása a feladat, amely nem "akad meg" a böngészőben, és kényelmes a felhasználó számára. Mit kell megvalósítani (Elvárások): 1. Adattípusok: Hozzon létre egy Item interfészt, amelynek mezői id: string, label: string és category: string. 2. Keresés (Input): Szöveg beírásakor a lista szűrődjön. 3. DOM és események kezelése (Billentyűzet navigáció): * A felhasználónak lehetősége legyen a lista feletti és alatti nyilakkal való mozgásra. 4. Enter lenyomásakor a kiválasztott elem "megerősítésre" kerül. 5. Fontos szempont: A nyilakkal kiválasztott elem mindig látható legyen (programozottan görgessük a konténert, ha a kurzor elhagyja a határokat). 6. Optimalizáció (Teljesítmény): * Mivel sok adat van (10k), beszéljük meg vagy valósítsuk meg az egyszerű "lusta renderelést" (csak az első 20-30 elemet rendereljük vagy használjunk IntersectionObserver-t). A komponens kimenete

sobes.tech MI

Válasz az MI-től

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