Reputation: 945
I'm trying to implement keyboard navigation for a list using react-hooks.
important - This list can shrink/grow based on a search.
My issue is around the Enter
key which should run some callback.
The activeCursor
does not change, which I understand since its not in the useEffect
array, but how can I get the current state inside handleKeyPress
without having to re-run useEffect
?
Also, I would ideally want to run my useEffect
only on mount ([]
) but since the filteredMessages
changes I must re-call it, which is also something I find strange since its eventListeners
so I'm not even sure what happens to them each time...
const useKeyboardNavigation = (filteredMessages, onMessageSelection) => {
const [activeCursor, setActiveCursor] = React.useState(0);
const size = filteredMessages.length
const handleKeyPress = (event) => {
if (event.key === 'ArrowDown') {
setActiveCursor(prev => prev < size ? prev + 1 : 0)
}
else if (event.key === 'ArrowUp') {
setActiveCursor(prev => prev > 0 ? prev - 1 : size)
}
else if (event.key === 'Enter') {
const msg = filteredMessages[activeCursor] // ??? Always 0
onMessageSelection(msg)
}
};
React.useEffect(
() => {
// Each time the list changes I reset the cursor
setActiveCursor(0)
document.addEventListener('keydown', handleKeyPress);
return () => document.removeEventListener('keydown', handleKeyPress);
},
[filteredMessages]
);
return [activeCursor, setActiveCursor];
}
Upvotes: 2
Views: 2818
Reputation: 945
Going to post my solution, ended up using a mixture of state and refs. Would still be open to hearing a solution with better performance for large lists.
const useKeyboardNavigation = (size: number) => {
const [activeCursor, setActiveCursor] = React.useState(0);
const handleKeyPress = event => {
if (event.key === 'ArrowDown') {
setActiveCursor(prev => (prev < size ? prev + 1 : 0));
} else if (event.key === 'ArrowUp') {
setActiveCursor(prev => (prev > 0 ? prev - 1 : size));
}
};
// Reset when size changes
React.useEffect(() => setActiveCursor(0), [size]);
React.useEffect(
() => {
document.addEventListener('keydown', handleKeyPress);
return () => document.removeEventListener('keydown', handleKeyPress);
},
[size, activeCursor]
);
return [activeCursor, setActiveCursor];
};
Usage:
const [activeCursor, setActiveCursor] = useKeyboardNavigation(messages.length);
Then when I render each ListItem (using messages.map
):
const useFocus = (isActive: boolean) => {
const itemRef = React.useRef<HTMLDivElement>(null);
React.useEffect(
() => {
isActive && itemRef && itemRef.current && itemRef.current.focus();
},
[isActive]
);
return itemRef;
};
Usage:
const myRef = useFocus(activeCursor === index);
<li ref={myRef} ...>
Upvotes: 2