An autocomplete combines an editable input with a list of suggestions. The input keeps focus while the active suggestion changes, so keyboard users can continue typing or accept a choice without leaving the field. Expose the popup state and the active option to assistive technology; a highlighted background alone does not describe the selection.
You'll search fictional route labels. Match case-insensitively, move to the first suggestion with ArrowDown, accept it with Enter, and dismiss the popup with Escape. A search with no matches needs an explicit status. Separate the text, open state, and active index so changing one does not corrupt the others. The hidden fixture changes the labels, so the component must filter the supplied rows rather than return a fixed route.
function App(){const [value,setValue]=useState(''),[open,setOpen]=useState(false),[active,setActive]=useState(-1);const matches=f.rows.filter(r=>r.label.toLowerCase().includes(value.toLowerCase()));function keyboard(e){if(e.key==='ArrowDown'){e.preventDefault();setOpen(true);setActive(a=>Math.min(a+1,matches.length-1))}if(e.key==='ArrowUp'){e.preventDefault();setActive(a=>Math.max(0,a-1))}if(e.key==='Enter'&&open&&active>=0&&matches[active]){e.preventDefault();setValue(matches[active].label);setOpen(false)}if(e.key==='Escape')setOpen(false)}return <><h1>Route search</h1><label>Route<input role="combobox" aria-autocomplete="list" aria-expanded={open} aria-controls="routes" aria-activedescendant={open&&active>=0?'route-'+matches[active]?.id:undefined} value={value} onChange={e=>{setValue(e.target.value);setOpen(true);setActive(-1)}} onKeyDown={keyboard}/></label>{open&&<ul role="listbox" id="routes">{matches.map((r,i)=><li role="option" id={'route-'+r.id} aria-selected={i===active} key={r.id} onClick={()=>{setValue(r.label);setOpen(false)}}>{r.label}</li>)}</ul>}{open&&!matches.length&&<p role="status">No routes</p>}</>}
Trace the control, state change and accessible output in the completed example.