Closed UmbrellaCrow612 closed 1 month ago
Here is something hacked together using some inspiration with the Accordion component. I mainly needed it just for a folder tree directory so its very basic. Unfortunately still needs some work, but its a start. Hope it helps!
Data Structure
const data = [
{ id: "1", name: "Unread" },
{ id: "2", name: "Threads" },
{
id: "3",
name: "Chat Rooms",
children: [
{ id: "c1", name: "General" },
{ id: "c2", name: "Random" },
{ id: "c3", name: "Open Source Projects" },
],
},
{
id: "4",
name: "Direct Messages",
children: [
{
id: "d1",
name: "Alice",
children: [
{ id: "d1", name: "Alice2" },
{ id: "d2", name: "Bob2" },
{ id: "d3", name: "Charlie2" },
],
},
{ id: "d2", name: "Bob" },
{ id: "d3", name: "Charlie" },
],
},
];
import React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { cn } from "@/lib/utils";
import { ChevronRight, Folder, FolderPlus } from "lucide-react";
function Tree({ data }: { data: any[] | any }) {
return (
<ul role="list" className="space-y-1">
{data instanceof Array ? (
data.map((item) => (
<li key={item.id}>
{item.children ? (
<a href="#">
<AccordionPrimitive.Root type="single" collapsible>
<AccordionPrimitive.Item value="item-1">
<AccordionTrigger>
<FolderPlus
className="h-6 w-6 shrink-0 mr-5"
aria-hidden="true"
/>
{item.name}
</AccordionTrigger>
<AccordionContent className="pl-4">
<Tree data={item.children ? item.children : item.name} />
</AccordionContent>
</AccordionPrimitive.Item>
</AccordionPrimitive.Root>
</a>
) : (
<Leaf name={item.name} />
)}
</li>
))
) : (
<li>
<Leaf name={data.name} />
</li>
)}
</ul>
);
}
export default Tree;
function Leaf({ name }: { name: string }) {
return (
<a href="#" className={"flex flex-1 items-center py-4 font-medium"}>
<ChevronRight className="h-4 w-4 shrink-0 opacity-0" />
<Folder className="h-6 w-6 shrink-0 mr-5" aria-hidden="true" />
{name}
</a>
);
}
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center py-4 font-medium transition-all first:[&[data-state=open]>svg]:rotate-90",
className
)}
{...props}
>
<ChevronRight className="h-4 w-4 shrink-0 transition-transform duration-200" />
{children}
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className={cn(
"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
className
)}
{...props}
>
<div className="pb-4 pt-0">{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
Please add this feature soon <3
inspired by @bytechase , my simple tree component:
add features:
1. initial select item
2. expand initial-selected folder
3. highlight item
4. hover item
5. scroll window
6. custom icon
"use client"
import * as React from "react"
import { Shell } from "@acme/components/shells/shell"
import { Tree } from "@acme/components/ui/tree"
import { Workflow, Folder, Layout } from "lucide-react";
const data = [
{ id: "1", name: "Unread" },
{ id: "2", name: "Threads" },
{
id: "3",
name: "Chat Rooms",
children: [
{ id: "c1", name: "General" },
{ id: "c2", name: "Random" },
{ id: "c3", name: "Open Source Projects" },
],
},
{
id: "4",
name: "Direct Messages",
children: [
{
id: "d1",
name: "Alice",
children: [
{ id: "d11", name: "Alice2", icon: Layout },
{ id: "d12", name: "Bob2" },
{ id: "d13", name: "Charlie2" },
],
},
{ id: "d2", name: "Bob", icon: Layout },
{ id: "d3", name: "Charlie" },
],
},
{
id: "5",
name: "Direct Messages",
children: [
{
id: "e1",
name: "Alice",
children: [
{ id: "e11", name: "Alice2" },
{ id: "e12", name: "Bob2" },
{ id: "e13", name: "Charlie2" },
],
},
{ id: "e2", name: "Bob" },
{ id: "e3", name: "Charlie" },
],
},
{
id: "6",
name: "Direct Messages",
children: [
{
id: "f1",
name: "Alice",
children: [
{ id: "f11", name: "Alice2" },
{ id: "f12", name: "Bob2" },
{ id: "f13", name: "Charlie2" },
],
},
{ id: "f2", name: "Bob" },
{ id: "f3", name: "Charlie" },
],
},
];
export default function IndexPage() {
const [content, setContent] = React.useState("Admin Page")
return (
<Shell className="gap-12 min-h-screen">
<div className="flex min-h-full space-x-2">
<Tree
data={data}
className="flex-shrink-0 w-[200px] h-[460px] border-[1px]"
initialSlelectedItemId="f12"
onSelectChange={(item) => setContent(item?.name ?? "")}
folderIcon={Folder}
itemIcon={Workflow}
/>
<div className="flex-1">{content}</div>
</div>
</Shell>
)
}
"use client";
import React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ScrollArea } from "@acme/components/ui/scroll-area"
import { cn } from "@acme/components/lib/utils";
import { ChevronRight, type LucideIcon } from "lucide-react";
import useResizeObserver from "use-resize-observer";
interface TreeDataItem {
id: string;
name: string;
icon?: LucideIcon,
children?: TreeDataItem[];
}
type TreeProps =
React.HTMLAttributes<HTMLDivElement> &
{
data: TreeDataItem[] | TreeDataItem,
initialSlelectedItemId?: string,
onSelectChange?: (item: TreeDataItem | undefined) => void,
expandAll?: boolean,
folderIcon?: LucideIcon,
itemIcon?: LucideIcon
}
const Tree = React.forwardRef<
HTMLDivElement,
TreeProps
>(({
data, initialSlelectedItemId, onSelectChange, expandAll,
folderIcon,
itemIcon,
className, ...props
}, ref) => {
const [selectedItemId, setSelectedItemId] = React.useState<string | undefined>(initialSlelectedItemId)
const handleSelectChange = React.useCallback((item: TreeDataItem | undefined) => {
setSelectedItemId(item?.id);
if (onSelectChange) {
onSelectChange(item)
}
}, [onSelectChange]);
const expandedItemIds = React.useMemo(() => {
if (!initialSlelectedItemId) {
return [] as string[]
}
const ids: string[] = []
function walkTreeItems(items: TreeDataItem[] | TreeDataItem, targetId: string) {
if (items instanceof Array) {
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < items.length; i++) {
ids.push(items[i]!.id);
if (walkTreeItems(items[i]!, targetId) && !expandAll) {
return true;
}
if (!expandAll) ids.pop();
}
} else if (!expandAll && items.id === targetId) {
return true;
} else if (items.children) {
return walkTreeItems(items.children, targetId)
}
}
walkTreeItems(data, initialSlelectedItemId)
return ids;
}, [data, initialSlelectedItemId])
const { ref: refRoot, width, height } = useResizeObserver();
return (
<div ref={refRoot} className={cn("overflow-hidden", className)}>
<ScrollArea style={{ width, height }}>
<div className="relative p-2">
<TreeItem
data={data}
ref={ref}
selectedItemId={selectedItemId}
handleSelectChange={handleSelectChange}
expandedItemIds={expandedItemIds}
FolderIcon={folderIcon}
ItemIcon={itemIcon}
{...props}
/>
</div>
</ScrollArea>
</div>
)
})
type TreeItemProps =
TreeProps &
{
selectedItemId?: string,
handleSelectChange: (item: TreeDataItem | undefined) => void,
expandedItemIds: string[],
FolderIcon?: LucideIcon,
ItemIcon?: LucideIcon
}
const TreeItem = React.forwardRef<
HTMLDivElement,
TreeItemProps
>(({ className, data, selectedItemId, handleSelectChange, expandedItemIds, FolderIcon, ItemIcon, ...props }, ref) => {
return (
<div ref={ref} role="tree" className={className} {...props}><ul>
{data instanceof Array ? (
data.map((item) => (
<li key={item.id}>
{item.children ? (
<AccordionPrimitive.Root type="multiple" defaultValue={expandedItemIds}>
<AccordionPrimitive.Item value={item.id}>
<AccordionTrigger
className={cn(
"px-2 hover:before:opacity-100 before:absolute before:left-0 before:w-full before:opacity-0 before:bg-muted/80 before:h-[1.75rem] before:-z-10",
selectedItemId === item.id && "before:opacity-100 before:bg-accent text-accent-foreground before:border-l-2 before:border-l-accent-foreground/50 dark:before:border-0"
)}
onClick={() => handleSelectChange(item)}
>
{item.icon &&
<item.icon
className="h-4 w-4 shrink-0 mr-2 text-accent-foreground/50"
aria-hidden="true"
/>
}
{!item.icon && FolderIcon &&
<FolderIcon
className="h-4 w-4 shrink-0 mr-2 text-accent-foreground/50"
aria-hidden="true"
/>
}
<span className="text-sm truncate">{item.name}</span>
</AccordionTrigger>
<AccordionContent className="pl-6">
<TreeItem
data={item.children ? item.children : item}
selectedItemId={selectedItemId}
handleSelectChange={handleSelectChange}
expandedItemIds={expandedItemIds}
FolderIcon={FolderIcon}
ItemIcon={ItemIcon}
/>
</AccordionContent>
</AccordionPrimitive.Item>
</AccordionPrimitive.Root>
) : (
<Leaf
item={item}
isSelected={selectedItemId === item.id}
onClick={() => handleSelectChange(item)}
Icon={ItemIcon}
/>
)}
</li>
))
) : (
<li>
<Leaf
item={data}
isSelected={selectedItemId === data.id}
onClick={() => handleSelectChange(data)}
Icon={ItemIcon}
/>
</li>
)}
</ul></div>
);
})
const Leaf = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & {
item: TreeDataItem, isSelected?: boolean,
Icon?: LucideIcon
}
>(({ className, item, isSelected, Icon, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
"flex items-center py-2 px-2 cursor-pointer \
hover:before:opacity-100 before:absolute before:left-0 before:right-1 before:w-full before:opacity-0 before:bg-muted/80 before:h-[1.75rem] before:-z-10",
className,
isSelected && "before:opacity-100 before:bg-accent text-accent-foreground before:border-l-2 before:border-l-accent-foreground/50 dark:before:border-0"
)}
{...props}
>
{item.icon && <item.icon className="h-4 w-4 shrink-0 mr-2 text-accent-foreground/50" aria-hidden="true" />}
{!item.icon && Icon && <Icon className="h-4 w-4 shrink-0 mr-2 text-accent-foreground/50" aria-hidden="true" />}
<span className="flex-grow text-sm truncate">{item.name}</span>
</div>
);
})
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header>
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 w-full items-center py-2 transition-all last:[&[data-state=open]>svg]:rotate-90",
className
)}
{...props}
>
{children}
<ChevronRight className="h-4 w-4 shrink-0 transition-transform duration-200 text-accent-foreground/50 ml-auto" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className={cn(
"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
className
)}
{...props}
>
<div className="pb-1 pt-0">{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Tree, type TreeDataItem }
scroll-area.tsx, support 'truncate'( text ellipsis):
- <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
+ <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit] [&>div]:!block">
refer: [https://github.com/radix-ui/primitives/issues/926#issuecomment-1266790070](https://github.com/radix-ui/primitives/issues/926#issuecomment-1266790070)
Thank you @WangLarry for not changing the api and sharing your Tree, I am now using yours ♥
I'm clueless on how to use the code from @WangLarry because of the imports. I can't figure out where these imports are coming from. Does anyone have the code on github?
@CohleM anything in the imports starting with @acme/components
refers to the path where you have stored your shadcn ui components + the lib/utils file.
You just replace it with your own path to those components.
You can probably find the path in your components.json file that got created if you installed the components using the CLI tool. See https://ui.shadcn.com/docs/components-json
Awesome work @WangLarry I'm using your component and looks very nice! love it
There is still no support at the moment
There is still no support at the moment
@WangLarry Easy to use
This issue has been automatically closed because it received no activity for a while. If you think it was closed by accident, please leave a comment. Thank you.
Let’s keep this open. I think this is quite a useful component for many use cases. At the very least, this can serve as an aggregator of ideas and references for other implementations.
Description
I noticed that the shadcn UI library doesn't currently have a File Tree component, and I think it would be a great addition to the library. A File Tree component would allow users to display and navigate file structures in their web applications, similar to the functionality provided by many integrated development environments (IDEs).
Proposed API
The File Tree component should allow users to pass in a hierarchical data structure representing the files and folders to be displayed. It should support expanding and collapsing folders, selecting files, and navigating through the tree using the keyboard. It should also allow users to customize the appearance of the component using props.
Example Usage / Could be different
Additional Context
A File Tree component would be a valuable addition to the shadcn UI library, providing users with an easy-to-use interface for displaying and navigating file structures in their web applications.