Tree View
A tree view that assembles all the functionalities of the Accordion component to create a tree view.
Installation
Run the following command
pnpm dlx @shadext/cli add tree-view
Why there is two components?
The TreeView component has two different ways to be used, that you can choose , based on your choice , that you have picked in the installation section.
-
The first approach is the
tree-viewcomponent that is based on thetree-view-apicomponents, in other words it is a wrapper for devs that don't want to have an custom functionality, but you still have the access to customise the ui , some of the benifits are :- Full control over the UI.
- All functionality is provided out of the box , including the (Virtualization , Selection , Expend/Collapse , Keyboard Navigation).
- You can customise the functionality if you want.
-
The second approach is the
tree-view-apicomponent that is based on the radix-ui-accordion components, and it is the primitive api that you can use to build your own custom tree view, some of the benifits are:- Full control over the UI.
- You can customise the functionality as you want.
API Reference
Props
The TreeView component accepts the following props:
| prop | type | default value |
|---|---|---|
initialSelectedId | string | -- |
expendAll | boolean | false |
elements* | TreeViewElement[] | -- |
initialExpendedItems | string[] | -- |
openIcon | ReactNode | <FolderOpenIcon /> |
closeIcon | ReactNode | <FolderIcon /> |
fileIcon | ReactNode | <FileIcon /> |
Heads up
The initialExpendedItems with expendAll can not work together, if you
want to expend all the items you should use expendAll prop only ,
otherwise if you want only specific items , provide initialExpendedItems
prop.
TreeViewElement
The TreeViewElement object accepts the following props:
| field | type | default Value |
|---|---|---|
id* | string | -- |
name* | string | -- |
children | TreeViewElement[] | -- |
isSelectable | boolean | -- |
Accessibility
Currently, the TreeView component is fully accessible and supports keyboard navigation.
Keyboard Navigation
| key | Description |
|---|---|
Tab | enter a nested folder |
Tab + Shift | exit the current selected folder |
ArrowDown | Increments by one step horzintanly |
ArrowUp | Decrements by one step horzintanly |
Enter | Opens/Close a folder , select a file |
Usage
The TreeView component has two different ways to be used, that you can choose , based on your choice , that you have picked in the installation section.
import { TreeView } from "@/components/extension/tree-view";const elements = [
{
id: "1",
name: "components",
children: [
{
id: "2",
name: "extension",
children: [
{
id: "3",
name: "tree-view.tsx",
},
{
id: "4",
name: "tree-view-api.tsx",
},
],
},
{
id: "5",
name: "dashboard-tree.tsx",
},
],
},
];export default function TreeViewExample() {
return (
<TreeView
elements={elements}
initialSelectedId="3"
initialExpendedItems={["1", "2"]}
/>
);
}This section will be focusing on how to build a custom tree view using the tree-view-api component.
Project Structure
This is the Structure that we will follow for this example
components
└── table_of_content
├── tree-item.tsx
├── tree.tsx
├── toc.tsxPre requisites
In the following example we're going to use this elements array :
const elements: TreeViewElement[] = [
{
id: "1",
name: "components",
children: [
{
id: "2",
name: "extension",
children: [
{
id: "3",
name: "tree-view.tsx",
},
{
id: "4",
name: "tree-view-api.tsx",
},
],
},
{
id: "5",
name: "dashboard-tree.tsx",
},
],
},
{
id: "6",
name: "pages",
children: [
{
id: "7",
name: "page.tsx",
},
{
id: "8",
name: "page-guide.tsx",
},
],
},
{
id: "18",
name: "env.ts",
},
];TOC
TOC component
First we create the TOC component.
import {
Tree,
TreeViewElement,
CollapseButton,
} from "@/components/extension/tree-view-api";
import { TreeItem } from "./tree-item.tsx";
type TOCProps = {
toc: TreeViewElement[];
};
export const TOC = ({ toc }: TOCProps) => {
return (
<Tree className="w-full h-60 bg-background p-2 rounded-md" indicator={true}>
{toc.map((element, _) => (
<TreeItem key={element.id} elements={[element]} />
))}
<CollapseButton elements={toc} expandAll={true} />
</Tree>
);
};Here we can see that we created a TOC component that will be responsible for rendering all the tree , the CollapseButton is used for the expandAll functionality.
TreeItem component
Then the TreeItem element.
import {
File,
Folder,
TreeViewElement,
} from "@/components/extension/tree-view-api";
type TreeItemProps = {
elements: TreeViewElement[];
};
export const TreeItem = ({ elements }: TreeItemProps) => {
return (
<ul className="w-full space-y-1">
{elements.map((element) => (
<li key={element.id} className="w-full space-y-2">
{element.children && element.children?.length > 0 ? (
<Folder
element={element.name}
value={element.id}
isSelectable={element.isSelectable}
className="px-px pr-1"
>
<TreeItem
key={element.id}
aria-label={`folder ${element.name}`}
elements={element.children}
/>
</Folder>
) : (
<File
key={element.id}
value={element.id}
element={element.name}
isSelectable={element.isSelectable}
className={"px-1"}
>
<span className="ml-1">{element?.name}</span>
</File>
)}
</li>
))}
</ul>
);
};Now let's break the code above :
- We have created a
TreeItemcomponent that will be responsible for rendering the elements of the tree view.
import { File , Folder , TreeViewElement } from "@/components/extension/tree-view-api";
type TreeItemProps = {
elements : TreeViewElement[]
};
export const TreeItem = ({ elements } : TreeItemProps) => {
return (
<ul className="w-full space-y-1" >
{elements.map((element) => (
<li key={element.id} className="w-full space-y-2">
{...}
</li>
))
}
</ul>
)
}
- First we check if the element has children or not , if it has children we render a
Foldercomponent , otherwise we render aFilecomponent.
{
element.children && element.children?.length > 0 ? (
<Folder
element={element.name}
value={element.id}
isSelectable={element.isSelectable}
className="px-px pr-1"
>
<TreeItem
key={element.id}
aria-label={`folder ${element.name}`}
elements={element.children}
/>
</Folder>
) : (
<File
key={element.id}
value={element.id}
element={element.name}
isSelectable={element.isSelectable}
className={"px-1"}
>
<span className="ml-1">{element?.name}</span>
</File>
);
}- In case of the
Foldercomponent we're calling theTreeItemcomponent again to render the children of the folder.
But wait, as you might notice we didn't loop over the elements as usual (using loop only), rather we are recalling the TreeItem , this called recursion rendering. And we use it because we don't know how many levels of children we have in the tree view , and this is the most efficient way to render the tree view.
Final Step
Now we can use the create the main component TOC that will contain all of this logic, and we will pass the elements to it.
import { Tree, TreeViewElement } from "@/components/extension/tree-view-api";
const TOCWrapper = () => {
return <TOC element={elements} />;
};
export default TOCWrapper;Final Result
So the end result is going to be like this :