I have a React component which displays a list of "autocomplete" recommendations as the user is typing into the search bar. I am trying to allow for the user to also navigate the list by using the arrow keys and enter key.

When the user presses the down key for the first time, the focus variable increases by one and the HTML focus goes to the first recommendation. If they press it again, it goes to the focus variable increases by one again, and the HTML focus moves to the second recommendation, and so on. Similarly, when the user presses the up key it decreases the focus by one and moves the HTML focus to the previous recommendation. In addition, there are measures to ensure the focus index does not go outside of the possible range as well as resetting the focus index to zero when the value of the search bar changes.
I am encountering this strange error that when overriding the focus index value, it will function properly only when using the down key, and then when I press the up key it will change to the previous value form before the override. In some case this can cause the focus index to go outside of the range and then crash, since when you type longer searches it narrows down the list of recommendations.
Here are some screen shots of the console output for the focus index when searching only one letter:

Now I'll show what happens when I narrow the search and then navigate the recommendations:

Here is a working codesandbox link:
https://codesandbox.io/s/jolly-tu-7itsm?fontsize=14&hidenavigation=1&theme=dark
Ok, it is exactly as I had suspected. You've enclosed some focus value in the "keyup" event handler that is mutated independently of the focus being reset each render cycle in the main component body and useEffect hook when the dependencies update.
There is even a react lint warning in your codesandbox to this effect:
Assignments to the 'focus' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property.
useEffect hook to manage the "keyup" event listener and cleanup function to remove it when the component unmounts. This will also keep your effect from added a new listener every time the effect callback triggers (your code added a listener each render!!)."autocomplete__container" container as direct DOM queries and manipulation are considered anti-pattern in React.Code:
function App() {
const [value, setValue] = useState("");
const [items, setItems] = useState([]);
const [query, setQuery] = useState([]);
const [formSubmit, setFormSubmit] = useState(false);
const [completions] = useAutocomplete(value, autocompleteValues);
const autocompleteContainer = useRef(); // <-- ref to get DOMNode
const focusRef = useRef(-1); // <-- ref to store stable focus value
...
// Effect hook to manage keyup event handler and cleanup
useEffect(() => {
const handleKeypress = (event) => {
if (event.keyCode === 38 && focusRef.current >= 1) { // <-- focus.current
focusRef.current -= 1;
autocompleteContainer.current.childNodes[focusRef.current].focus(); // <-- autocompleteContainer.current
} else if (
event.keyCode === 40 &&
focusRef.current < autocompleteContainer.current.childElementCount - 1
) {
focusRef.current += 1;
autocompleteContainer.current.childNodes[focusRef.current].focus();
} else if (
event.keyCode === 13 &&
document.activeElement.className !== "form-control"
) {
setValue(document.activeElement.innerHTML);
setQuery(document.activeElement.innerHTML);
document.getElementsByClassName(
"autocomplete__container"
)[0].style.display = "none";
setFormSubmit(true);
}
};
window.addEventListener("keyup", handleKeypress); // <-- add listener
return () => window.removeEventListener("keyup", handleKeypress); // <-- return cleanup function to remove listener
}, []);
// Effect hook to reset focus value when list updates
useEffect(() => {
focusRef.current = -1;
console.log("focus reset", focusRef.current);
}, [value, items]);
return (
<div className="App">
<Form id="search__form" onSubmit={handleSubmit}>
<Form.Group>
<InputGroup className="mb-3">
...
<div
ref={autocompleteContainer} // <-- attach DOM ref here
className="autocomplete__container"
>
{completions.map((val, index) => (
<p
tabIndex={index + 10}
key={index}
onClick={() => updateInput({ val })}
>
{val}
</p>
))}
</div>
</InputGroup>
</Form.Group>
</Form>
<p>{focusRef.current}</p>
</div>
);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With