import React, { useCallback, useEffect, useRef } from "react"; export interface SelectableListProps { items: T[]; selectedValue?: T | null; renderLabel: (item: T) => React.ReactNode; onSelect: (item: T) => void; } export const SelectableList = ( props: SelectableListProps, ): JSX.Element => { const { items, selectedValue, renderLabel, onSelect } = props; const listRef = useRef(null); const isFocusedRef = useRef(false); const prevSelectedValueRef = useRef(selectedValue); const itemRefs = useRef<(HTMLLIElement | null)[]>([]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (!isFocusedRef.current) return; if (!items.length) return; const currentIndex = selectedValue ? items.indexOf(selectedValue) : -1; if (e.key === "ArrowDown") { e.preventDefault(); const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0; onSelect(items[nextIndex]); } if (e.key === "ArrowUp") { e.preventDefault(); const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1; onSelect(items[prevIndex]); } }, [items, selectedValue, onSelect], ); useEffect(() => { if (!isFocusedRef.current) return; if (!selectedValue) return; const index = items.indexOf(selectedValue); if (index < 0) return; const el = itemRefs.current[index]; if (el) { el.focus({ preventScroll: false }); } }, [items, selectedValue]); // Separate effect for scrolling - only when selection changes useEffect(() => { // Only scroll if the selected value actually changed if (prevSelectedValueRef.current === selectedValue) return; prevSelectedValueRef.current = selectedValue; if (!selectedValue) return; const index = items.indexOf(selectedValue); if (index < 0) return; const el = itemRefs.current[index]; if (el) { el.scrollIntoView({ block: "nearest", behavior: "smooth" }); } }, [selectedValue, items]); return (
    (isFocusedRef.current = true)} onBlur={() => (isFocusedRef.current = false)} onKeyDown={handleKeyDown} > {items.map((item, index) => { const isSelected = selectedValue === item; const className = isSelected ? "selected" : ""; return (
  • (itemRefs.current[index] = el)} tabIndex={isSelected ? 0 : -1} role="option" aria-selected={isSelected} onClick={() => onSelect(item)} className={className} > {renderLabel(item)}
  • ); })}
); };