Welcome to the second chapter of the Hierarchical Tree Selector tutorial!
In the previous chapter, Hierarchical Tree Selector, we learned how to structure nested data and render a basic component. However, a tree is useless if you can't open or close folders!
This chapter focuses on the Brain of the component: how it interprets keyboard inputs to navigate deep hierarchies without using a mouse.
Terminal lists are traditionally 1D (One-Dimensional). You press Up to go to the previous item, and Down to go to the next.
But a Tree is 2D:
If we only used Up and Down, navigating a huge folder structure would be a nightmare. We need a strategy to handle that "Horizontal" movement.
We will implement a standard interaction model used in file explorers (like VS Code or Finder):
Before we can expand anything, we need to remember what is currently open. We don't change the original data; instead, we keep a separate "list of open IDs."
We use a JavaScript Set for this. Think of it as a VIP Guest List. If a folder's ID is on the list, it's allowed to show its children.
// Inside TreeSelect.tsx component
// A Set acts like a unique checklist of IDs: {'src', 'components'}
const [internalExpandedIds, setInternalExpandedIds] = React.useState(
new Set()
);
To check if a folder is open, we just ask the set:
// Returns true if 'src' is in the Set
const isFolderOpen = internalExpandedIds.has('src');
We need a helper function to add or remove IDs from this list. This function effectively "opens" or "closes" a folder.
const toggleExpand = (nodeId, shouldExpand) => {
setInternalExpandedIds(prev => {
const newSet = new Set(prev); // Copy the old list
if (shouldExpand) {
newSet.add(nodeId); // Add ID to open it
} else {
newSet.delete(nodeId); // Remove ID to close it
}
return newSet;
});
};
Why copy the Set? In React, we must create a new object/set to trigger a re-render. We can't just modify the old one.
This is where the magic happens. We listen for key presses on the focused item.
Normally, pressing Right in a text box moves the cursor. In our list, we want to prevent that default behavior and run our logic instead.
Let's see what happens when a user presses the Right Arrow on a folder named src:
handleKeyDown Function
Let's break down the actual code inside TreeSelect.tsx. We look at the event e.key.
If the user presses Right, and the item has stuff inside it, we open it.
// Inside handleKeyDown
if (e.key === "right" && flatNode.hasChildren) {
// Stop the terminal from doing weird scrolling stuff
e.preventDefault();
// Call our toggle helper to ADD to the set
toggleExpand(focusNodeId, true);
}
If the user presses Left, and the folder is currently open, we close it.
// Inside handleKeyDown
else if (e.key === "left") {
// Check if it's a folder AND it's currently open
if (flatNode.hasChildren && flatNode.isExpanded) {
e.preventDefault();
// Call our toggle helper to REMOVE from the set
toggleExpand(focusNodeId, false);
}
// ... (See Scenario C below)
}
This is a subtle but powerful feature.
Button.tsx (a child).Button.tsx (it's a file, it can't close). You want to jump focus back to the folder that contains it (components).// Inside the same 'left' block as above
else {
// If we are a child (we have a parentId)
if (flatNode.parentId !== undefined) {
e.preventDefault();
// 1. Close the parent folder (optional, but clean)
toggleExpand(flatNode.parentId, false);
// 2. Move focus "Up" to the parent
if (onFocus) {
// logic to find parent node and call onFocus(parentNode)
}
}
}
When we combine these strategies, we get a fluid navigation experience:
Project.Project opens.src.src opens.utils.ts.src.src closes.In this chapter, we learned how to turn a static list into an interactive tree:
But wait! Simply having a list of "Open IDs" ({'src', 'components'}) doesn't actually show the files on the screen. We need a way to read that list and generate the correct visual lines to print to the terminal.
How do we turn the Data Tree + Expanded State into a Flat List of items? We will explore that math in the next chapter.
Next Chapter: Recursive Tree Flattening
Generated by Code IQ