@olundot/ui · Navigation/Data
TreeView
폴더·분류처럼 계층 구조를 펼치고 접으며 탐색하는 단일 선택 트리(WAI-ARIA tree) 컴포넌트입니다.
- nodes + renderItem
- 트리 구조는 nodes로 전달하고, 각 항목의 생김새는 renderItem으로 직접 그립니다. 펼침·선택·키보드 같은 트리 동작은 컴포넌트가 알아서 처리합니다.
- 단일 선택
- selectedId로 지정한 노드 하나를 현재 위치로 강조합니다. 강조는 왼쪽 보더 대신 은은한 배경 채움으로 표시해 다른 컴포넌트와 결을 맞춥니다.
- 펼침 제어
- 처음 펼쳐 둘 노드는 defaultExpandedIds로 지정합니다. 펼침 상태를 직접 다뤄야 하면 expandedIds와 onExpandedChange로 제어할 수 있습니다.
- 키보드 탐색
- 방향키로 항목을 옮기고 펼치거나 접으며, Home·End로 처음과 끝으로 이동합니다. Enter로 선택하고, 하위가 있는 노드는 Space로 펼치거나 접습니다. Tab 한 번으로 트리에 들어오는 roving tabindex 등 WAI-ARIA tree 패턴을 따릅니다.
폴더 트리
defaultExpandedIds, selectedId, 펼침/접힘- 해부학
- 두경부
- 흉부
- 임상의학
- 생리학
코드 보기tsx
import { Folder, FolderOpen } from "lucide-react";
import { TreeView, type TreeNode } from "@olundot/ui";
interface FolderNode extends TreeNode {
name: string;
children?: FolderNode[];
}
const folders: FolderNode[] = [
{
id: "anatomy",
name: "해부학",
children: [
{ id: "anatomy-head", name: "두경부" },
{ id: "anatomy-thorax", name: "흉부", children: [{ id: "anatomy-heart", name: "심장" }] },
],
},
{ id: "physiology", name: "생리학" },
];
export function FolderTree() {
return (
<TreeView
aria-label="콘텐츠 폴더"
nodes={folders}
selectedId="anatomy-head"
defaultExpandedIds={["anatomy"]}
renderItem={(node, state) => (
<>
{state.hasChildren ? (state.expanded ? <FolderOpen size={16} strokeWidth={1.75} /> : <Folder size={16} strokeWidth={1.75} />) : <Folder size={16} strokeWidth={1.75} />}
<span className="truncate">{node.name}</span>
</>
)}
/>
);
}제어형 선택
selectedId·expandedIds·onSelect·onExpandedChange- 해부학
- 두경부
- 흉부
- 임상의학
- 생리학
코드 보기tsx
import { useState } from "react";
import { Folder, FolderOpen } from "lucide-react";
import { TreeView, type TreeNode } from "@olundot/ui";
interface FolderNode extends TreeNode {
name: string;
children?: FolderNode[];
}
export function ControlledTree({ folders }: { folders: FolderNode[] }) {
const [selectedId, setSelectedId] = useState("anatomy-head");
const [expandedIds, setExpandedIds] = useState<string[]>(["anatomy"]);
return (
<TreeView
aria-label="제어형 폴더 트리"
nodes={folders}
selectedId={selectedId}
expandedIds={expandedIds}
onSelect={(id) => setSelectedId(id)}
onExpandedChange={setExpandedIds}
renderItem={(node, state) => (
<>
{state.expanded ? <FolderOpen size={16} strokeWidth={1.75} /> : <Folder size={16} strokeWidth={1.75} />}
<span className="truncate">{node.name}</span>
</>
)}
/>
);
}링크 라벨
renderItem에 링크(현재 폴더 강조)코드 보기tsx
import Link from "next/link";
import { Folder } from "lucide-react";
import { TreeView, type TreeNode } from "@olundot/ui";
interface FolderNode extends TreeNode {
name: string;
href: string;
children?: FolderNode[];
}
// 항목 라벨이 링크일 때: treeitem wrapper가 포커스를 소유하므로 자식 링크는
// tabIndex={-1}로 두어 단일 Tab stop을 유지한다. 포인터 클릭 시 이동은 그대로 동작하고,
// 키보드 사용자는 Enter(onSelect)에서 라우팅한다.
export function LinkedTree({ folders, currentId }: { folders: FolderNode[]; currentId: string }) {
return (
<TreeView
aria-label="폴더 내비게이션"
nodes={folders}
selectedId={currentId}
selectionAriaCurrent="page"
defaultExpandedIds={["anatomy"]}
renderItem={(node) => (
<>
<Folder size={16} strokeWidth={1.75} />
<Link href={node.href} tabIndex={-1} className="truncate">
{node.name}
</Link>
</>
)}
/>
);
}