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.
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
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>
);
}