useListNavigation
Hooks

useListNavigation

Arrow-key navigation state for menus and listboxes.

Signature

useListNavigation returns the active index and a keyboard handler for a vertical list. The application renders the highlight, manages DOM focus, and handles selection.

SignatureTypeScript
function useListNavigation(options: {
  count: number;
  enabled?: boolean;
  /** Wrap from last to first. */
  loop?: boolean;
  initialIndex?: number;
  onSelect?: (index: number) => void;
  /** Container to keep the active row scrolled into view. */
  scrollRef?: { current: HTMLElement | null };
}): {
  activeIndex: number;
  setActiveIndex: (index: number) => void;
  move: (delta: number) => void;
  onKeyDown: (e: KeyboardEvent) => void;
};

Usage

ExampleTSX
import { useId, useRef } from "react";
import { useListNavigation } from "@solarius/onset";

type Item = { id: string; label: string };

function Options({
  items,
  onPick,
}: {
  items: Item[];
  onPick: (index: number) => void;
}) {
  const id = useId();
  const scrollRef = useRef<HTMLUListElement>(null);
  const { activeIndex, setActiveIndex, onKeyDown } = useListNavigation({
    count: items.length,
    onSelect: onPick,
    loop: true,
    scrollRef,
  });

  return (
    <ul
      ref={scrollRef}
      role="listbox"
      aria-label="Available options"
      aria-activedescendant={
        activeIndex >= 0 ? `${id}-${activeIndex}` : undefined
      }
      onKeyDown={onKeyDown}
      tabIndex={0}
      style={{ maxHeight: 240, overflowY: "auto" }}
    >
      {items.map((item, index) => (
        <li
          key={item.id}
          id={`${id}-${index}`}
          data-index={index}
          role="option"
          onMouseEnter={() => setActiveIndex(index)}
          onClick={() => onPick(index)}
          aria-selected={index === activeIndex}
        >
          {item.label}
        </li>
      ))}
    </ul>
  );
}