Welcome to Chapter 5!
In the previous chapter, Chapter 4: Input Controller, we built the bridge between your keyboard and the application. We know when the user presses "Down Arrow," but we haven't actually moved anything yet.
The Input Controller just shouts "Move Down!" It is up to the Navigation Engine to actually calculate where "Down" is.
Imagine you are browsing a library of 1,000 movies on your TV.
When you are at the 5th movie and press "Right," the screen must scroll to show the 6th movie. The first movie disappears off the left side.
This logicβtracking where you are and sliding the "camera" to keep you in viewβis the job of the Navigation Engine. Without it, you would highlight an item that is off the screen, and the user would be flying blind.
Our engine, useSelectNavigation, manages two specific things:
This is simple. It tracks the value of the item currently highlighted.
focusedValue: "pepperoni"This is a sliding window. It tracks the start and end indices of what is visible.
visibleFromIndex: 10visibleToIndex: 15Using this hook is like hiring a camera operator. You tell them your list of actors (options) and how big the lens is (visible count).
// Inside your component
const navigation = useSelectNavigation({
options: allToppings, // Array of 50 items
visibleOptionCount: 5 // Only show 5 at a time
});
The hook returns the calculated data needed to draw the screen:
// What the hook gives back:
console.log(navigation.focusedValue); // "pepperoni"
console.log(navigation.visibleOptions); // Only 5 items!
Note: In Chapter 1: Multi-Select Container, we looped over navigation.visibleOptions to render our list. This is why! We never render the full list of 50 items, only the 5 the engine gives us.
The engine uses a pattern called a Reducer. Think of a Reducer as a "State Machine." It takes the current situation, applies an action, and returns the new situation.
Let's see what happens when the Input Controller triggers focusNextOption().
Let's look at the heart of the engine in use-select-navigation.ts. We will simplify the code to understand the logic of "Scrolling."
We store the viewport boundaries in the state.
// The internal memory of the engine
type State = {
focusedValue: string;
visibleFromIndex: number; // e.g., 0
visibleToIndex: number; // e.g., 5
// ...
}
When the action focus-next-option comes in, we run this calculation:
// Inside the reducer function
case 'focus-next-option': {
// 1. Find the next item in the list
const nextItem = currentItem.next;
// 2. Check if we need to scroll
// If the next item's index is greater than what we can see...
const needsToScroll = nextItem.index >= state.visibleToIndex;
if (!needsToScroll) {
// Simple case: Just highlight the new item
return { ...state, focusedValue: nextItem.value };
}
// ...
If needsToScroll is true, we have to "push" the window down.
// ... continued from above
// Shift the window down by 1
const newEnd = state.visibleToIndex + 1;
const newStart = newEnd - state.visibleOptionCount;
return {
...state,
focusedValue: nextItem.value,
visibleFromIndex: newStart, // The window moved!
visibleToIndex: newEnd
};
}
This ensures that the focusedValue is always inside the range of visibleFromIndex and visibleToIndex.
The engine also handles Page Down and Page Up. The logic is similar, but instead of moving by 1, it jumps by the visibleOptionCount.
case 'focus-next-page': {
// Jump ahead by 5 items (or whatever the view count is)
const targetIndex = currentItem.index + state.visibleOptionCount;
// Recalculate the viewport to surround this new index
// ... logic to shift window ...
}
Finally, the hook exposes visibleOptions. This isn't stored in state; it is calculated on the fly (derived) to ensure it is always perfectly in sync with the indices.
// use-select-navigation.ts
const visibleOptions = useMemo(() => {
// Take the full list
return options
// Add index numbers for reference
.map((opt, i) => ({ ...opt, index: i }))
// Slice it using our calculated window!
.slice(state.visibleFromIndex, state.visibleToIndex);
}, [options, state.visibleFromIndex, state.visibleToIndex]);
This visibleOptions array is exactly what Chapter 1: Multi-Select Container uses to draw the UI.
The Navigation Engine is the GPS and Camera Operator of our application.
focusedValue).
You might have noticed a property called .next in the code examples above (currentItem.next). How do we know what comes next? Do we loop through the array every time to find the neighbor? That would be slow!
Instead, we convert our options array into a smart Linked List structure to make finding neighbors instant.
Next Chapter: Linked Option Data Structure
Generated by Code IQ