import React, { useCallback, useEffect, useRef } from "react"; export interface SelectableListProps { items: T[]; selectedValue?: T | null; renderLabel: (item: T) => React.ReactNode; onSelect: (item: T) => void; getChildren?: (item: T) => T[] | undefined; } export const SelectableList = ( props: SelectableListProps, ): JSX.Element => { const { items, selectedValue, renderLabel, onSelect, getChildren } = props; const flattenedItems = React.useMemo(() => { const flattened: { item: T; depth: number }[] = []; const walk = (source: T[], depth: number) => { source.forEach((item) => { flattened.push({ item, depth }); const children = getChildren?.(item) ?? []; if (children.length > 0) { walk(children, depth + 1); } }); }; walk(items, 0); return flattened; }, [items, getChildren]); const flatItems = flattenedItems.map((entry) => entry.item); 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 (!flatItems.length) return; const currentIndex = selectedValue ? flatItems.indexOf(selectedValue) : -1; if (e.key === "ArrowDown") { e.preventDefault(); const nextIndex = currentIndex < flatItems.length - 1 ? currentIndex + 1 : 0; onSelect(flatItems[nextIndex]); } if (e.key === "ArrowUp") { e.preventDefault(); const prevIndex = currentIndex > 0 ? currentIndex - 1 : flatItems.length - 1; onSelect(flatItems[prevIndex]); } }, [flatItems, selectedValue, onSelect], ); useEffect(() => { if (!isFocusedRef.current) return; if (!selectedValue) return; const index = flatItems.indexOf(selectedValue); if (index < 0) return; const el = itemRefs.current[index]; if (el) { el.focus({ preventScroll: false }); } }, [flatItems, 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 = flatItems.indexOf(selectedValue); if (index < 0) return; const el = itemRefs.current[index]; if (el) { el.scrollIntoView({ block: "nearest", behavior: "smooth" }); } }, [selectedValue, flatItems]); return (
    (isFocusedRef.current = true)} onBlur={() => (isFocusedRef.current = false)} onKeyDown={handleKeyDown} > {flattenedItems.map(({ item, depth }, 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} style={{ paddingLeft: `${depth * 16}px` }} > {renderLabel(item)}
  • ); })}
); };