Tree
Navigation
드래그 정렬과 가상화를 지원하는 트리.
Usage
계층 항목을 펼치고 접으며 드래그로 순서를 바꾸거나 큰 목록을 탐색할 때 사용한다.
import
import
import { Tree } from '@mildang/design-system/TreeListItem';예제를 복사해 쓸 때 필요한 준비
• @mildang/icons 를 따로 설치한다. DS 패키지에 아이콘 컴포넌트가 포함되지 않는다.
• @mildang/styled-system 은 이 저장소에서 Panda 가 생성하는 산출물이다. 저장소 안에서는 turbo run ship 이후 쓸 수 있고, 패키지 소비자는 자기 Panda 산출물이나 다른 레이아웃 수단으로 바꿔야 한다.
API Reference
Tree Props
Prop
Type
Default
TreeItems<T>
지정 안 함
(props: RenderItemProps<T>) => ReactNode
지정 안 함
(item: FlattenedItem<T>) => boolean
지정 안 함
(args: CanDropArgs<T>) => boolean
지정 안 함
"default" | "compact"
지정 안 함
boolean
false
TreeDragAccessibility<T>
지정 안 함
boolean
지정 안 함
boolean
지정 안 함
(items: TreeItems<T>) => void
지정 안 함
(event: { operation: DragOperationSnapshot<Draggable<Data>, Droppable<Data>>; nativeEvent?: Event; canceled: boolean; suspend(): { resume(): void; abort(): void; }; }, manager: DragDropManager<Data, Draggable<Data>, Droppable<Data>>) => void
지정 안 함
(offset: number) => void
지정 안 함
UniqueIdentifier
지정 안 함
boolean
지정 안 함
boolean
false
string | number
지정 안 함
MutableRefObject<VListHandle>
지정 안 함
StandardTreeListItem
Prop
Type
Default
ReactNode
지정 안 함
boolean
지정 안 함
ReactNode
지정 안 함
"success" | "info" | "error"
success
number
지정 안 함
boolean
지정 안 함
ReactNode
지정 안 함
ReactNode
지정 안 함
ReactNode
지정 안 함
boolean
지정 안 함
(selected: boolean) => void
지정 안 함
boolean
false
boolean
지정 안 함
boolean
false
"sm" | "md"
md
ReactNode
지정 안 함
ReactNode
지정 안 함
TreeListItem
Prop
Type
Default
React.ReactNode
지정 안 함
boolean
지정 안 함
"success" | "info" | "error"
success
boolean
지정 안 함
boolean
지정 안 함
(selected: boolean) => void
지정 안 함
boolean
지정 안 함
"sm" | "md"
md
renderItem 에 무엇을 반환할까
Tree 는 renderItem 이 반환하는 내용에 관여하지 않는다 — Tree.tsx 는 StandardTreeListItem 을
전혀 import 하지 않고, renderItem 이 돌려준 TreeListItem.Root 앞에 DragHandle·Depth·Expansion
만 주입한다. 행 내용을 고르는 방법은 두 가지다.
- TreeListItem 조각을 직접 조합 —
Root아래Icon·TitleArea·ActionArea등을 원하는 대로 배치한다. 조합 자유도가 가장 높다. - StandardTreeListItem 사용 — 위 조합을 미리 만들어 둔 편의 컴포넌트다.
icon·title·count·hoverActions·actions같은 prop만 채우면 되고, Tree 의 renderItem 안에서 가장 자주 쓰는 조합이다.
둘 다 Tree 없이도 완결되게 렌더되므로(각자 자기 스타일 컨텍스트를 만든다) 상세 페이지도 따로 두지만, 실제로는 Tree 의 renderItem 자리를 채우는 같은 계열의 부품이다.
Tree 는 renderItem 이 돌려준 콘텐츠 앞에 다음을 이 순서로 자동 주입한다.
- DragHandle —
sortable일 때만 - DepthItem(또는
showDepthPipe면 DepthPipe) —item.depth개수만큼 - ExpansionItem(자식이 있거나
hasChildren: true) 또는 빈 DepthItem
expandByClick 이 true 면 행 클릭으로 펼치기/접기가 토글되지만, 다음 선택자에 걸리는 대상을 클릭한
경우는 제외한다 — button, input, a, [role="button"], [role="checkbox"], [data-tree-expand-ignore="true"].
DragHandle·Checkbox 등은 이미 이 선택자에 해당해 자동으로 빠진다.
lazy-loading 트리에서는 children 배열이 비어 있어도 나중에 채워질 예정이면 hasChildren: true,
하위 데이터를 불러오는 중이면 isChildrenLoading: true 를 준다 — 이때 caret 대신 spinner 가 표시되고
토글이 막힌다.
기본 사용
Tree 의 renderItem 안에서 TreeListItem 으로 각 행을 그립니다 — 단원·차시처럼 계층이 있는 목록에 씁니다.
import { TreeItems, TreeItem } from '@mildang/design-system/TreeListItem';
import { useState } from 'react';
import { useTreeSelection } from '@mildang/design-system/TreeListItem';
import { Tree } from '@mildang/design-system/TreeListItem';
import { TreeListItem } from '@mildang/design-system/TreeListItem';
interface ItemData {
title: string;
type?: 'unit' | 'activity' | 'step';
}
const initialItems: TreeItems<ItemData> = [
{
id: 'unit-1',
data: { title: 'Unit 1', type: 'unit' },
collapsed: false,
children: [
{
id: 'activity-1',
data: { title: 'Activity 1', type: 'activity' },
collapsed: false,
children: [
{ id: 'step-1', data: { title: 'Step 1', type: 'step' }, children: [] },
{ id: 'step-2', data: { title: 'Step 2', type: 'step' }, children: [] },
],
},
{ id: 'activity-2', data: { title: 'Activity 2', type: 'activity' }, children: [] },
],
},
{
id: 'unit-2',
data: { title: 'Unit 2', type: 'unit' },
collapsed: true,
children: [
{
id: 'activity-3',
data: { title: 'Activity 3', type: 'activity' },
children: [{ id: 'step-3', data: { title: 'Step 3', type: 'step' }, children: [] }],
},
{ id: 'activity-4', data: { title: 'Activity 4', type: 'activity' }, children: [] },
],
},
];
/**
* Selection Mode 비교용 데모. `mode` 는 useTreeSelection 옵션.
* leaf-only(폴더 선택 제외) 같은 제약은 훅 옵션이 아니라 **사용처 가드**로 처리한다 —
* 여기선 `canSelect` predicate 로 "어떤 노드를 선택 가능하게 둘지"를 소비자가 정하는 예시.
*/
function SelectionModeDemo({
title,
description,
mode,
canSelect,
}: {
title: string;
description: string;
mode: 'single' | 'multiple';
/** 사용처 가드 예시(훅 API 아님) — 선택 가능한 노드를 소비자가 결정. 미지정 시 전부 선택 가능. */
canSelect?: (item: TreeItem<ItemData>) => boolean;
}) {
const [items, setItems] = useState<TreeItems<ItemData>>(initialItems);
const { selectedIds, getSelectionState, handleSelectedChange } = useTreeSelection({ items, mode });
return (
<div style={{ width: 280 }}>
<strong style={{ fontSize: '13px' }}>{title}</strong>
<p style={{ fontSize: '11px', color: '#666', margin: '4px 0 8px' }}>{description}</p>
<Tree
items={items}
onChange={setItems}
renderItem={({ item }) => {
const selectable = canSelect ? canSelect(item) : true;
const { checked, indeterminate } = getSelectionState(item);
return (
<TreeListItem.Root
selected={checked}
indeterminate={indeterminate}
onSelectedChange={(isChecked) => {
if (!selectable) return; // 선택 제외 노드는 사용처가 결정
handleSelectedChange(item, isChecked);
}}
>
{selectable && <TreeListItem.Checkbox />}
<TreeListItem.TitleArea title={item.data?.title ?? String(item.id)} />
</TreeListItem.Root>
);
}}
/>
<pre
style={{
marginTop: '8px',
padding: '8px',
background: '#f5f5f5',
borderRadius: '4px',
fontSize: '11px',
}}
>
selected: {JSON.stringify(Array.from(selectedIds))}
</pre>
</div>
);
}
const TreeSelectionModesExample = () => (
<div style={{ display: 'flex', gap: '24px', alignItems: 'flex-start' }}>
<SelectionModeDemo
title="multiple (cascade)"
description="부모 선택 시 하위 전부 선택, 부분 선택은 indeterminate"
mode="multiple"
/>
<SelectionModeDemo title="single" description="폴더·리프 아무 노드나 1개만 선택" mode="single" />
<SelectionModeDemo
title="single + leaf-only"
description="리프(자식 없는 노드)만 선택 · 폴더엔 체크박스 없음 (사용처 가드)"
mode="single"
canSelect={(item) => (item.children?.length ?? 0) === 0}
/>
</div>
);
export default TreeSelectionModesExample;
드래그 앤 드롭 동작
onChange 를 넘겨야 실제로 순서가 바뀐다 — 드래그가 취소되지 않았고 canDrop 이 허용했고 원위치가
아니면 onChange(updatedTree) 가 호출된다. onChange 없이 onDragEnd 만 넘기면 데이터 갱신은
직접 구현해야 한다.
canDrag(item)이 false 를 반환하면 그 아이템의 DragHandle 이 숨김(hide) 처리된다 — 자리는 남지만 드래그는 시작되지 않는다.canDrop(args)이 false 를 반환하면 드롭이 무시되고 원위치로 되돌아간다.disableDepthChange가 true 면 좌우 이동·키보드 방향키로도 depth 가 바뀌지 않는다 — 순서만 바뀌는 Flat List 에 쓴다.
| canDrop 인자 | 의미 |
|---|---|
activeItem | 드래그 중인 아이템 |
overItem | 현재 hover 중인 아이템 |
projected.depth / projected.parentId | 놓였을 때 적용될 depth·부모 id |
items | 평탄화된 전체 아이템 목록 |
depth 변경은 마우스 드래그의 가로 이동량, 또는 키보드 좌우 방향키로 계산되며 원래 depth 근처로 돌아오면 스냅되는 구간이 있다.
드래그 중 시각 피드백은 포인터 드래그와 키보드 드래그가 다르다.
- 포인터 드래그 — DragOverlay 가 드래그 시작 시점 아이템의 모습을 그대로 띄우고, 목록 안 원래 자리에는 옅은 placeholder 가 남아 예상 도착 위치(변경된 depth 포함)를 보여준다.
- 키보드 드래그 — DragOverlay 자체가 뜨지 않는다. 대신 목록 안 placeholder 이동과 접근성 안내 문구로만 위치를 확인한다.
기본 사용
Tree 의 renderItem 안에서 TreeListItem 으로 각 행을 그립니다 — 단원·차시처럼 계층이 있는 목록에 씁니다.
import { TreeItems, TreeItem } from '@mildang/design-system/TreeListItem';
import { useState } from 'react';
import { useTreeSelection } from '@mildang/design-system/TreeListItem';
import { Tree } from '@mildang/design-system/TreeListItem';
import { TreeListItem } from '@mildang/design-system/TreeListItem';
interface ItemData {
title: string;
type?: 'unit' | 'activity' | 'step';
}
const initialItems: TreeItems<ItemData> = [
{
id: 'unit-1',
data: { title: 'Unit 1', type: 'unit' },
collapsed: false,
children: [
{
id: 'activity-1',
data: { title: 'Activity 1', type: 'activity' },
collapsed: false,
children: [
{ id: 'step-1', data: { title: 'Step 1', type: 'step' }, children: [] },
{ id: 'step-2', data: { title: 'Step 2', type: 'step' }, children: [] },
],
},
{ id: 'activity-2', data: { title: 'Activity 2', type: 'activity' }, children: [] },
],
},
{
id: 'unit-2',
data: { title: 'Unit 2', type: 'unit' },
collapsed: true,
children: [
{
id: 'activity-3',
data: { title: 'Activity 3', type: 'activity' },
children: [{ id: 'step-3', data: { title: 'Step 3', type: 'step' }, children: [] }],
},
{ id: 'activity-4', data: { title: 'Activity 4', type: 'activity' }, children: [] },
],
},
];
/**
* Selection Mode 비교용 데모. `mode` 는 useTreeSelection 옵션.
* leaf-only(폴더 선택 제외) 같은 제약은 훅 옵션이 아니라 **사용처 가드**로 처리한다 —
* 여기선 `canSelect` predicate 로 "어떤 노드를 선택 가능하게 둘지"를 소비자가 정하는 예시.
*/
function SelectionModeDemo({
title,
description,
mode,
canSelect,
}: {
title: string;
description: string;
mode: 'single' | 'multiple';
/** 사용처 가드 예시(훅 API 아님) — 선택 가능한 노드를 소비자가 결정. 미지정 시 전부 선택 가능. */
canSelect?: (item: TreeItem<ItemData>) => boolean;
}) {
const [items, setItems] = useState<TreeItems<ItemData>>(initialItems);
const { selectedIds, getSelectionState, handleSelectedChange } = useTreeSelection({ items, mode });
return (
<div style={{ width: 280 }}>
<strong style={{ fontSize: '13px' }}>{title}</strong>
<p style={{ fontSize: '11px', color: '#666', margin: '4px 0 8px' }}>{description}</p>
<Tree
items={items}
onChange={setItems}
renderItem={({ item }) => {
const selectable = canSelect ? canSelect(item) : true;
const { checked, indeterminate } = getSelectionState(item);
return (
<TreeListItem.Root
selected={checked}
indeterminate={indeterminate}
onSelectedChange={(isChecked) => {
if (!selectable) return; // 선택 제외 노드는 사용처가 결정
handleSelectedChange(item, isChecked);
}}
>
{selectable && <TreeListItem.Checkbox />}
<TreeListItem.TitleArea title={item.data?.title ?? String(item.id)} />
</TreeListItem.Root>
);
}}
/>
<pre
style={{
marginTop: '8px',
padding: '8px',
background: '#f5f5f5',
borderRadius: '4px',
fontSize: '11px',
}}
>
selected: {JSON.stringify(Array.from(selectedIds))}
</pre>
</div>
);
}
const TreeSelectionModesExample = () => (
<div style={{ display: 'flex', gap: '24px', alignItems: 'flex-start' }}>
<SelectionModeDemo
title="multiple (cascade)"
description="부모 선택 시 하위 전부 선택, 부분 선택은 indeterminate"
mode="multiple"
/>
<SelectionModeDemo title="single" description="폴더·리프 아무 노드나 1개만 선택" mode="single" />
<SelectionModeDemo
title="single + leaf-only"
description="리프(자식 없는 노드)만 선택 · 폴더엔 체크박스 없음 (사용처 가드)"
mode="single"
canSelect={(item) => (item.children?.length ?? 0) === 0}
/>
</div>
);
export default TreeSelectionModesExample;
드래그 접근성 안내
Tree 는 dnd-kit 의 Accessibility 플러그인을 별도 설정 없이 내부에서 구성한다 — 드래그 핸들에
포커스한 뒤 키보드로 드래그를 시작하면 스크린리더 안내가 자동으로 나온다.
기본 안내 문구는 각 아이템의 data.title 을 우선 읽고, title 이 없으면 id 로 대체한다.
| 시점 | 기본 문구 |
|---|---|
| 드래그 시작 | "현재 항목은 {title}입니다." |
| 드래그 중 이동 | "현재 {title} 위로 이동 중입니다." |
| 종료 · 취소됨 | "{title} 이동이 취소되었습니다." |
| 종료 · 제자리 | "{title}을(를) 현재 위치에 놓았습니다." |
| 종료 · 이동됨 | "{title}을(를) {대상 title} 위에 놓았습니다." |
dragAccessibility 로 조정 가능한 범위는 disabled, screenReaderInstructions.draggable,
announcements.{dragstart,dragover,dragend} 뿐이다. manager 를 직접 주입하면 이 설정은
적용되지 않는다 — Tree 가 내부 plugin 구성을 보장할 수 없기 때문이다.
기본 사용
Tree 의 renderItem 안에서 TreeListItem 으로 각 행을 그립니다 — 단원·차시처럼 계층이 있는 목록에 씁니다.
import { TreeItems, TreeItem } from '@mildang/design-system/TreeListItem';
import { useState } from 'react';
import { useTreeSelection } from '@mildang/design-system/TreeListItem';
import { Tree } from '@mildang/design-system/TreeListItem';
import { TreeListItem } from '@mildang/design-system/TreeListItem';
interface ItemData {
title: string;
type?: 'unit' | 'activity' | 'step';
}
const initialItems: TreeItems<ItemData> = [
{
id: 'unit-1',
data: { title: 'Unit 1', type: 'unit' },
collapsed: false,
children: [
{
id: 'activity-1',
data: { title: 'Activity 1', type: 'activity' },
collapsed: false,
children: [
{ id: 'step-1', data: { title: 'Step 1', type: 'step' }, children: [] },
{ id: 'step-2', data: { title: 'Step 2', type: 'step' }, children: [] },
],
},
{ id: 'activity-2', data: { title: 'Activity 2', type: 'activity' }, children: [] },
],
},
{
id: 'unit-2',
data: { title: 'Unit 2', type: 'unit' },
collapsed: true,
children: [
{
id: 'activity-3',
data: { title: 'Activity 3', type: 'activity' },
children: [{ id: 'step-3', data: { title: 'Step 3', type: 'step' }, children: [] }],
},
{ id: 'activity-4', data: { title: 'Activity 4', type: 'activity' }, children: [] },
],
},
];
/**
* Selection Mode 비교용 데모. `mode` 는 useTreeSelection 옵션.
* leaf-only(폴더 선택 제외) 같은 제약은 훅 옵션이 아니라 **사용처 가드**로 처리한다 —
* 여기선 `canSelect` predicate 로 "어떤 노드를 선택 가능하게 둘지"를 소비자가 정하는 예시.
*/
function SelectionModeDemo({
title,
description,
mode,
canSelect,
}: {
title: string;
description: string;
mode: 'single' | 'multiple';
/** 사용처 가드 예시(훅 API 아님) — 선택 가능한 노드를 소비자가 결정. 미지정 시 전부 선택 가능. */
canSelect?: (item: TreeItem<ItemData>) => boolean;
}) {
const [items, setItems] = useState<TreeItems<ItemData>>(initialItems);
const { selectedIds, getSelectionState, handleSelectedChange } = useTreeSelection({ items, mode });
return (
<div style={{ width: 280 }}>
<strong style={{ fontSize: '13px' }}>{title}</strong>
<p style={{ fontSize: '11px', color: '#666', margin: '4px 0 8px' }}>{description}</p>
<Tree
items={items}
onChange={setItems}
renderItem={({ item }) => {
const selectable = canSelect ? canSelect(item) : true;
const { checked, indeterminate } = getSelectionState(item);
return (
<TreeListItem.Root
selected={checked}
indeterminate={indeterminate}
onSelectedChange={(isChecked) => {
if (!selectable) return; // 선택 제외 노드는 사용처가 결정
handleSelectedChange(item, isChecked);
}}
>
{selectable && <TreeListItem.Checkbox />}
<TreeListItem.TitleArea title={item.data?.title ?? String(item.id)} />
</TreeListItem.Root>
);
}}
/>
<pre
style={{
marginTop: '8px',
padding: '8px',
background: '#f5f5f5',
borderRadius: '4px',
fontSize: '11px',
}}
>
selected: {JSON.stringify(Array.from(selectedIds))}
</pre>
</div>
);
}
const TreeSelectionModesExample = () => (
<div style={{ display: 'flex', gap: '24px', alignItems: 'flex-start' }}>
<SelectionModeDemo
title="multiple (cascade)"
description="부모 선택 시 하위 전부 선택, 부분 선택은 indeterminate"
mode="multiple"
/>
<SelectionModeDemo title="single" description="폴더·리프 아무 노드나 1개만 선택" mode="single" />
<SelectionModeDemo
title="single + leaf-only"
description="리프(자식 없는 노드)만 선택 · 폴더엔 체크박스 없음 (사용처 가드)"
mode="single"
canSelect={(item) => (item.children?.length ?? 0) === 0}
/>
</div>
);
export default TreeSelectionModesExample;
useTreeSelection 으로 선택 상태 연결
useTreeSelection 은 Tree 의 selected/indeterminate 를 계산해 주는 훅이다 — 같은 바렐에서 함께
import 한다.
| 옵션 | 타입 | 설명 |
|---|---|---|
items | TreeItems<T> | 트리 데이터(필수) |
defaultSelectedIds | UniqueIdentifier[] | 초기 선택 ID |
mode | 'single' | 'multiple'(기본 'multiple') | multiple 은 부모/자식 cascade + indeterminate, single 은 cascade 없는 단일 선택 |
| 반환값 | 설명 |
|---|---|
selectedIds | 선택된 id Set |
selectedItems | 선택된 아이템 객체 배열 |
getSelectionState(item) | { checked, indeterminate } 조회 |
handleSelectedChange(item, checked) | 선택 토글(자식/부모 자동 반영) |
setSelectedIds | selectedIds 직접 설정 |
multiple 모드의 선택 로직(bubble up/down):
- 부모를 선택하면 모든 자식이 함께 선택된다.
- 부모를 해제하면 모든 자식이 함께 해제된다.
- 자식 일부만 선택되면 부모는 indeterminate, leaf 기준으로 자식이 전부 선택되면 부모도 checked 로 올라간다.
single 모드는 이 cascade 가 없다 — 새 항목을 선택하면 기존 선택이 자동으로 해제될 뿐이다.
leaf-only(폴더 선택 제외)나 라디오식(해제 불가) 같은 제약은 훅이 강제하지 않으므로
onSelectedChange 쪽에서 직접 가드해야 한다.
기본 사용
Tree 의 renderItem 안에서 TreeListItem 으로 각 행을 그립니다 — 단원·차시처럼 계층이 있는 목록에 씁니다.
import { TreeItems, TreeItem } from '@mildang/design-system/TreeListItem';
import { useState } from 'react';
import { useTreeSelection } from '@mildang/design-system/TreeListItem';
import { Tree } from '@mildang/design-system/TreeListItem';
import { TreeListItem } from '@mildang/design-system/TreeListItem';
interface ItemData {
title: string;
type?: 'unit' | 'activity' | 'step';
}
const initialItems: TreeItems<ItemData> = [
{
id: 'unit-1',
data: { title: 'Unit 1', type: 'unit' },
collapsed: false,
children: [
{
id: 'activity-1',
data: { title: 'Activity 1', type: 'activity' },
collapsed: false,
children: [
{ id: 'step-1', data: { title: 'Step 1', type: 'step' }, children: [] },
{ id: 'step-2', data: { title: 'Step 2', type: 'step' }, children: [] },
],
},
{ id: 'activity-2', data: { title: 'Activity 2', type: 'activity' }, children: [] },
],
},
{
id: 'unit-2',
data: { title: 'Unit 2', type: 'unit' },
collapsed: true,
children: [
{
id: 'activity-3',
data: { title: 'Activity 3', type: 'activity' },
children: [{ id: 'step-3', data: { title: 'Step 3', type: 'step' }, children: [] }],
},
{ id: 'activity-4', data: { title: 'Activity 4', type: 'activity' }, children: [] },
],
},
];
/**
* Selection Mode 비교용 데모. `mode` 는 useTreeSelection 옵션.
* leaf-only(폴더 선택 제외) 같은 제약은 훅 옵션이 아니라 **사용처 가드**로 처리한다 —
* 여기선 `canSelect` predicate 로 "어떤 노드를 선택 가능하게 둘지"를 소비자가 정하는 예시.
*/
function SelectionModeDemo({
title,
description,
mode,
canSelect,
}: {
title: string;
description: string;
mode: 'single' | 'multiple';
/** 사용처 가드 예시(훅 API 아님) — 선택 가능한 노드를 소비자가 결정. 미지정 시 전부 선택 가능. */
canSelect?: (item: TreeItem<ItemData>) => boolean;
}) {
const [items, setItems] = useState<TreeItems<ItemData>>(initialItems);
const { selectedIds, getSelectionState, handleSelectedChange } = useTreeSelection({ items, mode });
return (
<div style={{ width: 280 }}>
<strong style={{ fontSize: '13px' }}>{title}</strong>
<p style={{ fontSize: '11px', color: '#666', margin: '4px 0 8px' }}>{description}</p>
<Tree
items={items}
onChange={setItems}
renderItem={({ item }) => {
const selectable = canSelect ? canSelect(item) : true;
const { checked, indeterminate } = getSelectionState(item);
return (
<TreeListItem.Root
selected={checked}
indeterminate={indeterminate}
onSelectedChange={(isChecked) => {
if (!selectable) return; // 선택 제외 노드는 사용처가 결정
handleSelectedChange(item, isChecked);
}}
>
{selectable && <TreeListItem.Checkbox />}
<TreeListItem.TitleArea title={item.data?.title ?? String(item.id)} />
</TreeListItem.Root>
);
}}
/>
<pre
style={{
marginTop: '8px',
padding: '8px',
background: '#f5f5f5',
borderRadius: '4px',
fontSize: '11px',
}}
>
selected: {JSON.stringify(Array.from(selectedIds))}
</pre>
</div>
);
}
const TreeSelectionModesExample = () => (
<div style={{ display: 'flex', gap: '24px', alignItems: 'flex-start' }}>
<SelectionModeDemo
title="multiple (cascade)"
description="부모 선택 시 하위 전부 선택, 부분 선택은 indeterminate"
mode="multiple"
/>
<SelectionModeDemo title="single" description="폴더·리프 아무 노드나 1개만 선택" mode="single" />
<SelectionModeDemo
title="single + leaf-only"
description="리프(자식 없는 노드)만 선택 · 폴더엔 체크박스 없음 (사용처 가드)"
mode="single"
canSelect={(item) => (item.children?.length ?? 0) === 0}
/>
</div>
);
export default TreeSelectionModesExample;
예제
Default
import type { ComponentProps } from 'react';
import { useState } from 'react';
import { type TreeItems } from '@mildang/design-system/TreeListItem';
import { Tree } from '@mildang/design-system/TreeListItem';
import { TreeListItem } from '@mildang/design-system/TreeListItem';
import MoreHorizIcon from '@mildang/icons/react/more-horiz';
interface ItemData {
title: string;
type?: 'unit' | 'activity' | 'step';
}
const initialItems: TreeItems<ItemData> = [
{
id: 'unit-1',
data: { title: 'Unit 1', type: 'unit' },
collapsed: false,
children: [
{
id: 'activity-1',
data: { title: 'Activity 1', type: 'activity' },
collapsed: false,
children: [
{ id: 'step-1', data: { title: 'Step 1', type: 'step' }, children: [] },
{ id: 'step-2', data: { title: 'Step 2', type: 'step' }, children: [] },
],
},
{ id: 'activity-2', data: { title: 'Activity 2', type: 'activity' }, children: [] },
],
},
{
id: 'unit-2',
data: { title: 'Unit 2', type: 'unit' },
collapsed: true,
children: [
{
id: 'activity-3',
data: { title: 'Activity 3', type: 'activity' },
children: [{ id: 'step-3', data: { title: 'Step 3', type: 'step' }, children: [] }],
},
{ id: 'activity-4', data: { title: 'Activity 4', type: 'activity' }, children: [] },
],
},
];
const STORY_DEFAULT_ARGS = { ...({}), ...({}) } as ComponentProps<typeof Tree>;
const TreeDefaultExampleRender = function Render(args: ComponentProps<typeof Tree>) {
const [items, setItems] = useState<TreeItems<ItemData>>(initialItems);
return (
<div style={{ width: '400px' }}>
<Tree
items={items}
onChange={setItems}
sortable={args.sortable}
hideExpand={args.hideExpand}
showDepthPipe={args.showDepthPipe}
expandByClick={args.expandByClick}
disableDepthChange={args.disableDepthChange}
renderItem={({ item }) => (
<TreeListItem.Root data-testid={item.id}>
<TreeListItem.TitleArea title={item.data?.title ?? String(item.id)} />
<TreeListItem.ActionArea>
<TreeListItem.ShowOnHover>
<TreeListItem.IconButton>
<MoreHorizIcon />
</TreeListItem.IconButton>
</TreeListItem.ShowOnHover>
</TreeListItem.ActionArea>
</TreeListItem.Root>
)}
/>
</div>
);
};
export default function TreeDefaultExample(props: Partial<ComponentProps<typeof Tree>>) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Tree>;
return TreeDefaultExampleRender(mergedProps);
}
FlatList
import type { ComponentProps } from 'react';
import { useState } from 'react';
import { type TreeItems } from '@mildang/design-system/TreeListItem';
import { Tree } from '@mildang/design-system/TreeListItem';
import { TreeListItem } from '@mildang/design-system/TreeListItem';
interface ItemData {
title: string;
type?: 'unit' | 'activity' | 'step';
}
const flatListItems: TreeItems<ItemData> = [
{ id: 'item-1', data: { title: 'Item 1' } },
{ id: 'item-2', data: { title: 'Item 2' } },
{ id: 'item-3', data: { title: 'Item 3' } },
{ id: 'item-4', data: { title: 'Item 4' } },
{ id: 'item-5', data: { title: 'Item 5' } },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
disableDepthChange: true,
}) } as ComponentProps<typeof Tree>;
const TreeFlatListExampleRender = function Render(args: ComponentProps<typeof Tree>) {
const [items, setItems] = useState<TreeItems<ItemData>>(flatListItems);
return (
<div style={{ width: '400px' }}>
<Tree
items={items}
onChange={setItems}
sortable={args.sortable}
hideExpand={args.hideExpand}
showDepthPipe={args.showDepthPipe}
expandByClick={args.expandByClick}
disableDepthChange={args.disableDepthChange}
renderItem={({ item }) => (
<TreeListItem.Root data-testid={item.id}>
<TreeListItem.TitleArea title={item.data?.title ?? String(item.id)} />
</TreeListItem.Root>
)}
/>
</div>
);
};
export default function TreeFlatListExample(props: Partial<ComponentProps<typeof Tree>>) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Tree>;
return TreeFlatListExampleRender(mergedProps);
}
NonSortable
import type { ComponentProps } from 'react';
import { useState } from 'react';
import { type TreeItems } from '@mildang/design-system/TreeListItem';
import { Tree } from '@mildang/design-system/TreeListItem';
import { TreeListItem } from '@mildang/design-system/TreeListItem';
interface ItemData {
title: string;
type?: 'unit' | 'activity' | 'step';
}
const initialItems: TreeItems<ItemData> = [
{
id: 'unit-1',
data: { title: 'Unit 1', type: 'unit' },
collapsed: false,
children: [
{
id: 'activity-1',
data: { title: 'Activity 1', type: 'activity' },
collapsed: false,
children: [
{ id: 'step-1', data: { title: 'Step 1', type: 'step' }, children: [] },
{ id: 'step-2', data: { title: 'Step 2', type: 'step' }, children: [] },
],
},
{ id: 'activity-2', data: { title: 'Activity 2', type: 'activity' }, children: [] },
],
},
{
id: 'unit-2',
data: { title: 'Unit 2', type: 'unit' },
collapsed: true,
children: [
{
id: 'activity-3',
data: { title: 'Activity 3', type: 'activity' },
children: [{ id: 'step-3', data: { title: 'Step 3', type: 'step' }, children: [] }],
},
{ id: 'activity-4', data: { title: 'Activity 4', type: 'activity' }, children: [] },
],
},
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
sortable: false,
}) } as ComponentProps<typeof Tree>;
const TreeNonSortableExampleRender = function Render(args: ComponentProps<typeof Tree>) {
const [items, setItems] = useState<TreeItems<ItemData>>(initialItems);
return (
<div style={{ width: '400px' }}>
<Tree
items={items}
onChange={setItems}
sortable={args.sortable}
hideExpand={args.hideExpand}
showDepthPipe={args.showDepthPipe}
expandByClick={args.expandByClick}
disableDepthChange={args.disableDepthChange}
renderItem={({ item }) => (
<TreeListItem.Root data-testid={item.id}>
<TreeListItem.TitleArea title={item.data?.title ?? String(item.id)} />
</TreeListItem.Root>
)}
/>
</div>
);
};
export default function TreeNonSortableExample(props: Partial<ComponentProps<typeof Tree>>) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Tree>;
return TreeNonSortableExampleRender(mergedProps);
}
WithCheckbox
import type { ComponentProps } from 'react';
import { useState } from 'react';
import { type TreeItems } from '@mildang/design-system/TreeListItem';
import { useTreeSelection } from '@mildang/design-system/TreeListItem';
import { Tree } from '@mildang/design-system/TreeListItem';
import { TreeListItem } from '@mildang/design-system/TreeListItem';
interface ItemData {
title: string;
type?: 'unit' | 'activity' | 'step';
}
const initialItems: TreeItems<ItemData> = [
{
id: 'unit-1',
data: { title: 'Unit 1', type: 'unit' },
collapsed: false,
children: [
{
id: 'activity-1',
data: { title: 'Activity 1', type: 'activity' },
collapsed: false,
children: [
{ id: 'step-1', data: { title: 'Step 1', type: 'step' }, children: [] },
{ id: 'step-2', data: { title: 'Step 2', type: 'step' }, children: [] },
],
},
{ id: 'activity-2', data: { title: 'Activity 2', type: 'activity' }, children: [] },
],
},
{
id: 'unit-2',
data: { title: 'Unit 2', type: 'unit' },
collapsed: true,
children: [
{
id: 'activity-3',
data: { title: 'Activity 3', type: 'activity' },
children: [{ id: 'step-3', data: { title: 'Step 3', type: 'step' }, children: [] }],
},
{ id: 'activity-4', data: { title: 'Activity 4', type: 'activity' }, children: [] },
],
},
];
const STORY_DEFAULT_ARGS = { ...({}), ...({}) } as ComponentProps<typeof Tree>;
const TreeWithCheckboxExampleRender = function Render(args: ComponentProps<typeof Tree>) {
const [items, setItems] = useState<TreeItems<ItemData>>(initialItems);
const { selectedIds, selectedItems, getSelectionState, handleSelectedChange } = useTreeSelection({
items,
});
return (
<div style={{ width: '400px' }}>
<Tree
items={items}
onChange={setItems}
sortable={args.sortable}
hideExpand={args.hideExpand}
showDepthPipe={args.showDepthPipe}
expandByClick={args.expandByClick}
disableDepthChange={args.disableDepthChange}
renderItem={({ item }) => {
const { checked, indeterminate } = getSelectionState(item);
return (
<TreeListItem.Root
data-testid={item.id}
selected={checked}
indeterminate={indeterminate}
onSelectedChange={(isChecked) => handleSelectedChange(item, isChecked)}
>
<TreeListItem.Checkbox />
<TreeListItem.TitleArea title={item.data?.title ?? String(item.id)} />
</TreeListItem.Root>
);
}}
/>
<div style={{ marginTop: '16px', padding: '12px', background: '#f5f5f5', borderRadius: '4px' }}>
<strong>Selected IDs</strong>
<p style={{ fontSize: '12px' }}>{JSON.stringify(Array.from(selectedIds), null, 2)}</p>
</div>
<div style={{ marginTop: '8px', padding: '12px', background: '#e8f5e9', borderRadius: '4px' }}>
<strong>Selected Items</strong>
<p style={{ fontSize: '12px' }}>
{JSON.stringify(
selectedItems.map((item) => ({ id: item.id, title: item.data?.title })),
null,
2,
)}
</p>
</div>
</div>
);
};
export default function TreeWithCheckboxExample(props: Partial<ComponentProps<typeof Tree>>) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Tree>;
return TreeWithCheckboxExampleRender(mergedProps);
}
DndCustomExample
import type { ComponentProps } from 'react';
import { useState } from 'react';
import { type TreeItems, type CanDropArgs } from '@mildang/design-system/TreeListItem';
import { Tree } from '@mildang/design-system/TreeListItem';
import { TreeListItem } from '@mildang/design-system/TreeListItem';
import FolderFillIcon from '@mildang/icons/react/folder-fill';
import { css } from '@mildang/styled-system/css';
import AssignmentIcon from '@mildang/icons/react/assignment';
interface FolderFileData {
title: string;
type: 'folder' | 'file';
}
const folderFileItems: TreeItems<FolderFileData> = [
{
id: 'folder-0',
data: { title: 'Folder 0', type: 'folder' },
collapsed: false,
children: [
{
id: 'folder-1',
data: { title: 'Folder 1', type: 'folder' },
collapsed: false,
children: [
{ id: 'file-1', data: { title: 'File 1', type: 'file' }, children: [] },
{ id: 'file-2', data: { title: 'File 2', type: 'file' }, children: [] },
],
},
{
id: 'folder-2',
data: { title: 'Folder 2', type: 'folder' },
children: [],
},
],
},
{
id: 'folder-3',
data: { title: 'Folder 3', type: 'folder' },
collapsed: false,
children: [{ id: 'file-3', data: { title: 'File 3', type: 'file' }, children: [] }],
},
];
const STORY_DEFAULT_ARGS = { ...({}), ...({}) } as ComponentProps<typeof Tree>;
const TreeDndCustomExampleExampleRender = function Render(args: ComponentProps<typeof Tree>) {
const [items, setItems] = useState<TreeItems<FolderFileData>>(folderFileItems);
// file은 드래그 불가
const canDrag = (item: { data?: FolderFileData }) => {
return item.data?.type === 'folder';
};
// file이 자식으로 있는 folder 안으로는 드롭 불가
const canDrop = ({ activeItem, projected, items }: CanDropArgs<FolderFileData>) => {
if (projected.parentId === null) return true;
const parent = items.find((item) => item.id === projected.parentId);
if (!parent) return true;
if (activeItem.parentId === projected.parentId) return true;
const parentIndex = items.findIndex((item) => item.id === projected.parentId);
const hasFileChildren = items
.slice(parentIndex + 1)
.some((child) => child.parentId === projected.parentId && child.data?.type === 'file');
if (hasFileChildren) return false;
return true;
};
return (
<div style={{ width: '400px' }}>
<div style={{ marginBottom: '10px', fontSize: '14px', color: '#666' }}>
<p>
<strong>canDrag:</strong> file은 드래그 불가
</p>
<p>
<strong>canDrop:</strong> file이 자식으로 있는 folder 안으로는 드롭 불가
</p>
</div>
<Tree
items={items}
onChange={setItems}
canDrag={canDrag}
canDrop={canDrop}
sortable={args.sortable}
hideExpand={args.hideExpand}
showDepthPipe={args.showDepthPipe}
expandByClick={args.expandByClick}
disableDepthChange={args.disableDepthChange}
renderItem={({ item }) => (
<TreeListItem.Root data-testid={item.id}>
<TreeListItem.Icon>
{item.data?.type === 'folder' ? (
<FolderFillIcon className={css({ color: 'warning.icon.base' })} />
) : (
<AssignmentIcon />
)}
</TreeListItem.Icon>
<TreeListItem.TitleArea title={String(item.id)} />
</TreeListItem.Root>
)}
/>
</div>
);
};
export default function TreeDndCustomExampleExample(props: Partial<ComponentProps<typeof Tree>>) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Tree>;
return TreeDndCustomExampleExampleRender(mergedProps);
}
DragAccessibility
import type { ComponentProps } from 'react';
import { useState } from 'react';
import { type TreeItems } from '@mildang/design-system/TreeListItem';
import { Tree } from '@mildang/design-system/TreeListItem';
import { TreeListItem } from '@mildang/design-system/TreeListItem';
import MoreHorizIcon from '@mildang/icons/react/more-horiz';
interface ItemData {
title: string;
type?: 'unit' | 'activity' | 'step';
}
const initialItems: TreeItems<ItemData> = [
{
id: 'unit-1',
data: { title: 'Unit 1', type: 'unit' },
collapsed: false,
children: [
{
id: 'activity-1',
data: { title: 'Activity 1', type: 'activity' },
collapsed: false,
children: [
{ id: 'step-1', data: { title: 'Step 1', type: 'step' }, children: [] },
{ id: 'step-2', data: { title: 'Step 2', type: 'step' }, children: [] },
],
},
{ id: 'activity-2', data: { title: 'Activity 2', type: 'activity' }, children: [] },
],
},
{
id: 'unit-2',
data: { title: 'Unit 2', type: 'unit' },
collapsed: true,
children: [
{
id: 'activity-3',
data: { title: 'Activity 3', type: 'activity' },
children: [{ id: 'step-3', data: { title: 'Step 3', type: 'step' }, children: [] }],
},
{ id: 'activity-4', data: { title: 'Activity 4', type: 'activity' }, children: [] },
],
},
];
const STORY_DEFAULT_ARGS = { ...({}), ...({}) } as ComponentProps<typeof Tree>;
const TreeDragAccessibilityExampleRender = function Render(args: ComponentProps<typeof Tree>) {
const [items, setItems] = useState<TreeItems<ItemData>>(initialItems);
return (
<div style={{ width: '400px' }}>
<Tree
items={items}
onChange={setItems}
sortable={args.sortable}
hideExpand={args.hideExpand}
showDepthPipe={args.showDepthPipe}
expandByClick={args.expandByClick}
disableDepthChange={args.disableDepthChange}
dragAccessibility={{
announcements: {
dragstart: ({ source }) =>
source ? `현재 항목은 ${source.item?.data?.title ?? String(source.id)}입니다.` : undefined,
dragover: ({ target }) =>
target
? `현재 ${target.item?.data?.title ?? String(target.id)} 위로 이동 중입니다.`
: undefined,
dragend: ({ source, target, canceled }) => {
if (!source) return undefined;
const sourceLabel = source.item?.data?.title ?? String(source.id);
if (canceled) return `${sourceLabel} 이동이 취소되었습니다.`;
if (!target || source.id === target.id) return `${sourceLabel}을(를) 현재 위치에 놓았습니다.`;
const targetLabel = target.item?.data?.title ?? String(target.id);
return `${sourceLabel}이(가) ${targetLabel} 위에 놓였습니다.`;
},
},
}}
renderItem={({ item }) => (
<TreeListItem.Root data-testid={item.id}>
<TreeListItem.TitleArea title={item.data?.title ?? String(item.id)} />
<TreeListItem.ActionArea>
<TreeListItem.ShowOnHover>
<TreeListItem.IconButton>
<MoreHorizIcon />
</TreeListItem.IconButton>
</TreeListItem.ShowOnHover>
</TreeListItem.ActionArea>
</TreeListItem.Root>
)}
/>
</div>
);
};
export default function TreeDragAccessibilityExample(props: Partial<ComponentProps<typeof Tree>>) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Tree>;
return TreeDragAccessibilityExampleRender(mergedProps);
}
Virtualized
import type { ComponentProps } from 'react';
import { useState } from 'react';
import { type TreeItems } from '@mildang/design-system/TreeListItem';
import { Tree } from '@mildang/design-system/TreeListItem';
import { TreeListItem } from '@mildang/design-system/TreeListItem';
import MoreHorizIcon from '@mildang/icons/react/more-horiz';
interface ItemData {
title: string;
type?: 'unit' | 'activity' | 'step';
}
const generateLargeTreeData = (
unitCount: number,
activitiesPerUnit: number,
stepsPerActivity: number,
): TreeItems<ItemData> => {
const items: TreeItems<ItemData> = [];
for (let u = 1; u <= unitCount; u++) {
const activities: TreeItems<ItemData> = [];
for (let a = 1; a <= activitiesPerUnit; a++) {
const steps: TreeItems<ItemData> = [];
for (let s = 1; s <= stepsPerActivity; s++) {
steps.push({
id: `unit-${u}-activity-${a}-step-${s}`,
data: { title: `Unit ${u} > Activity ${a} > Step ${s}`, type: 'step' },
children: [],
});
}
activities.push({
id: `unit-${u}-activity-${a}`,
data: { title: `Unit ${u} > Activity ${a}`, type: 'activity' },
children: steps,
});
}
items.push({
id: `unit-${u}`,
data: { title: `Unit ${u}`, type: 'unit' },
children: activities,
});
}
return items;
};
const STORY_DEFAULT_ARGS = { ...({}), ...({}) } as ComponentProps<typeof Tree>;
const TreeVirtualizedExampleRender = function Render(args: ComponentProps<typeof Tree>) {
const largeTreeItems = generateLargeTreeData(20, 5, 5);
const [items, setItems] = useState<TreeItems<ItemData>>(largeTreeItems);
return (
<div style={{ width: '500px' }}>
<Tree
items={items}
onChange={setItems}
sortable={args.sortable}
hideExpand={args.hideExpand}
showDepthPipe={args.showDepthPipe}
expandByClick={args.expandByClick}
disableDepthChange={args.disableDepthChange}
virtualized={400}
renderItem={({ item }) => (
<TreeListItem.Root>
<TreeListItem.TitleArea title={item.data?.title ?? String(item.id)} />
<TreeListItem.ActionArea>
<TreeListItem.ShowOnHover>
<TreeListItem.IconButton>
<MoreHorizIcon />
</TreeListItem.IconButton>
</TreeListItem.ShowOnHover>
</TreeListItem.ActionArea>
</TreeListItem.Root>
)}
/>
</div>
);
};
export default function TreeVirtualizedExample(props: Partial<ComponentProps<typeof Tree>>) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Tree>;
return TreeVirtualizedExampleRender(mergedProps);
}
HideExpand
import type { ComponentProps } from 'react';
import { useState } from 'react';
import { type TreeItems } from '@mildang/design-system/TreeListItem';
import { Tree } from '@mildang/design-system/TreeListItem';
import { StandardTreeListItem } from '@mildang/design-system/TreeListItem';
import FolderFillIcon from '@mildang/icons/react/folder-fill';
import AssignmentIcon from '@mildang/icons/react/assignment';
interface ItemData {
title: string;
type?: 'unit' | 'activity' | 'step';
}
const initialItems: TreeItems<ItemData> = [
{
id: 'unit-1',
data: { title: 'Unit 1', type: 'unit' },
collapsed: false,
children: [
{
id: 'activity-1',
data: { title: 'Activity 1', type: 'activity' },
collapsed: false,
children: [
{ id: 'step-1', data: { title: 'Step 1', type: 'step' }, children: [] },
{ id: 'step-2', data: { title: 'Step 2', type: 'step' }, children: [] },
],
},
{ id: 'activity-2', data: { title: 'Activity 2', type: 'activity' }, children: [] },
],
},
{
id: 'unit-2',
data: { title: 'Unit 2', type: 'unit' },
collapsed: true,
children: [
{
id: 'activity-3',
data: { title: 'Activity 3', type: 'activity' },
children: [{ id: 'step-3', data: { title: 'Step 3', type: 'step' }, children: [] }],
},
{ id: 'activity-4', data: { title: 'Activity 4', type: 'activity' }, children: [] },
],
},
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
hideExpand: true,
}) } as ComponentProps<typeof Tree>;
const TreeHideExpandExampleRender = function Render(args: ComponentProps<typeof Tree>) {
const [items, setItems] = useState<TreeItems<ItemData>>(initialItems);
return (
<div style={{ width: '400px' }}>
<Tree
items={items}
onChange={setItems}
sortable={args.sortable}
hideExpand={args.hideExpand}
showDepthPipe={args.showDepthPipe}
expandByClick={args.expandByClick}
disableDepthChange={args.disableDepthChange}
renderItem={({ item }) => (
<StandardTreeListItem
data-testid={item.id}
icon={item.children && item.children.length > 0 ? <FolderFillIcon /> : <AssignmentIcon />}
title={item.data?.title ?? String(item.id)}
/>
)}
/>
</div>
);
};
export default function TreeHideExpandExample(props: Partial<ComponentProps<typeof Tree>>) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Tree>;
return TreeHideExpandExampleRender(mergedProps);
}
ExpandByClick
import type { ComponentProps } from 'react';
import { useState } from 'react';
import { type TreeItems } from '@mildang/design-system/TreeListItem';
import { Tree } from '@mildang/design-system/TreeListItem';
import { StandardTreeListItem } from '@mildang/design-system/TreeListItem';
import FolderFillIcon from '@mildang/icons/react/folder-fill';
import AssignmentIcon from '@mildang/icons/react/assignment';
interface ItemData {
title: string;
type?: 'unit' | 'activity' | 'step';
}
const initialItems: TreeItems<ItemData> = [
{
id: 'unit-1',
data: { title: 'Unit 1', type: 'unit' },
collapsed: false,
children: [
{
id: 'activity-1',
data: { title: 'Activity 1', type: 'activity' },
collapsed: false,
children: [
{ id: 'step-1', data: { title: 'Step 1', type: 'step' }, children: [] },
{ id: 'step-2', data: { title: 'Step 2', type: 'step' }, children: [] },
],
},
{ id: 'activity-2', data: { title: 'Activity 2', type: 'activity' }, children: [] },
],
},
{
id: 'unit-2',
data: { title: 'Unit 2', type: 'unit' },
collapsed: true,
children: [
{
id: 'activity-3',
data: { title: 'Activity 3', type: 'activity' },
children: [{ id: 'step-3', data: { title: 'Step 3', type: 'step' }, children: [] }],
},
{ id: 'activity-4', data: { title: 'Activity 4', type: 'activity' }, children: [] },
],
},
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
expandByClick: true,
}) } as ComponentProps<typeof Tree>;
const TreeExpandByClickExampleRender = function Render(args: ComponentProps<typeof Tree>) {
const [items, setItems] = useState<TreeItems<ItemData>>(initialItems);
return (
<div style={{ width: '400px' }}>
<Tree
items={items}
onChange={setItems}
sortable={args.sortable}
hideExpand={args.hideExpand}
showDepthPipe={args.showDepthPipe}
expandByClick={args.expandByClick}
disableDepthChange={args.disableDepthChange}
renderItem={({ item }) => (
<StandardTreeListItem
data-testid={item.id}
icon={item.children && item.children.length > 0 ? <FolderFillIcon /> : <AssignmentIcon />}
title={item.data?.title ?? String(item.id)}
/>
)}
/>
</div>
);
};
export default function TreeExpandByClickExample(props: Partial<ComponentProps<typeof Tree>>) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Tree>;
return TreeExpandByClickExampleRender(mergedProps);
}
ShowDepthPipe
import type { ComponentProps } from 'react';
import { useState } from 'react';
import { type TreeItems } from '@mildang/design-system/TreeListItem';
import { Tree } from '@mildang/design-system/TreeListItem';
import { StandardTreeListItem } from '@mildang/design-system/TreeListItem';
import FolderFillIcon from '@mildang/icons/react/folder-fill';
import AssignmentIcon from '@mildang/icons/react/assignment';
interface ItemData {
title: string;
type?: 'unit' | 'activity' | 'step';
}
const initialItems: TreeItems<ItemData> = [
{
id: 'unit-1',
data: { title: 'Unit 1', type: 'unit' },
collapsed: false,
children: [
{
id: 'activity-1',
data: { title: 'Activity 1', type: 'activity' },
collapsed: false,
children: [
{ id: 'step-1', data: { title: 'Step 1', type: 'step' }, children: [] },
{ id: 'step-2', data: { title: 'Step 2', type: 'step' }, children: [] },
],
},
{ id: 'activity-2', data: { title: 'Activity 2', type: 'activity' }, children: [] },
],
},
{
id: 'unit-2',
data: { title: 'Unit 2', type: 'unit' },
collapsed: true,
children: [
{
id: 'activity-3',
data: { title: 'Activity 3', type: 'activity' },
children: [{ id: 'step-3', data: { title: 'Step 3', type: 'step' }, children: [] }],
},
{ id: 'activity-4', data: { title: 'Activity 4', type: 'activity' }, children: [] },
],
},
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
showDepthPipe: true,
}) } as ComponentProps<typeof Tree>;
const TreeShowDepthPipeExampleRender = function Render(args: ComponentProps<typeof Tree>) {
const [items, setItems] = useState<TreeItems<ItemData>>(initialItems);
return (
<div style={{ width: '400px' }}>
<Tree
items={items}
onChange={setItems}
sortable={args.sortable}
hideExpand={args.hideExpand}
showDepthPipe={args.showDepthPipe}
expandByClick={args.expandByClick}
disableDepthChange={args.disableDepthChange}
renderItem={({ item }) => (
<StandardTreeListItem
data-testid={item.id}
icon={item.children && item.children.length > 0 ? <FolderFillIcon /> : <AssignmentIcon />}
title={item.data?.title ?? String(item.id)}
/>
)}
/>
</div>
);
};
export default function TreeShowDepthPipeExample(props: Partial<ComponentProps<typeof Tree>>) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Tree>;
return TreeShowDepthPipeExampleRender(mergedProps);
}
LazyLoading
import type { ComponentProps } from 'react';
import { useState } from 'react';
import { type TreeItems } from '@mildang/design-system/TreeListItem';
import { Tree } from '@mildang/design-system/TreeListItem';
import { StandardTreeListItem } from '@mildang/design-system/TreeListItem';
import FolderFillIcon from '@mildang/icons/react/folder-fill';
import AssignmentIcon from '@mildang/icons/react/assignment';
interface ItemData {
title: string;
type?: 'unit' | 'activity' | 'step';
}
const STORY_DEFAULT_ARGS = { ...({}), ...({
sortable: false,
}) } as ComponentProps<typeof Tree>;
const TreeLazyLoadingExampleRender = function Render(args: ComponentProps<typeof Tree>) {
const [items, setItems] = useState<TreeItems<ItemData>>([
{
id: 'unit-lazy',
data: { title: 'Lazy Unit', type: 'unit' },
collapsed: true,
hasChildren: true,
isChildrenLoading: false,
children: [],
},
{
id: 'unit-loading',
data: { title: 'Loading Unit', type: 'unit' },
collapsed: false,
hasChildren: true,
isChildrenLoading: true,
children: [],
},
]);
return (
<div style={{ width: '400px' }}>
<Tree
items={items}
onChange={(nextItems) => {
setItems(nextItems);
setTimeout(() => {
setItems((prev) =>
prev.map((item) =>
item.id === 'unit-lazy'
? {
...item,
isChildrenLoading: true,
}
: item,
),
);
}, 0);
}}
sortable={args.sortable}
hideExpand={args.hideExpand}
showDepthPipe={args.showDepthPipe}
expandByClick={args.expandByClick}
disableDepthChange={args.disableDepthChange}
renderItem={({ item }) => (
<StandardTreeListItem
data-testid={item.id}
icon={
item.hasChildren ?? (item.children?.length ?? 0) > 0 ? <FolderFillIcon /> : <AssignmentIcon />
}
title={item.data?.title ?? String(item.id)}
/>
)}
/>
</div>
);
};
export default function TreeLazyLoadingExample(props: Partial<ComponentProps<typeof Tree>>) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Tree>;
return TreeLazyLoadingExampleRender(mergedProps);
}