Skip to content

happyhyep/directory-tree

Repository files navigation

🌳 @happyhyep/tree-component

🇰🇷 한국어 | 🇺🇸 English

🇺🇸 English ver

React Tree Component Library where both folders and files are clickable

npm version License: MIT

✨ Key Differentiators

Unlike other Tree components, this revolutionary Tree component allows both folders and files to be clickable for interaction.

Feature Typical Tree 🌳 Tree Component
Folder Click ❌ Expand/collapse only Click event + expand/collapse
File Click ✅ Clickable ✅ Clickable
Search Feature ❌ Requires separate implementation Built-in search + highlight
Default Expand State ❌ Manual setup Expand all folders at once
TypeScript ⚠️ Limited support Full type safety

🎬 Demo

Tree Component Usage Examples

Tree Component Demo

📦 Installation

# npm
npm install @happyhyep/tree-component

# yarn
yarn add @happyhyep/tree-component

# pnpm
pnpm add @happyhyep/tree-component

🚀 Quick Start

1️⃣ Basic Tree Component

import React, { useState } from 'react';
import { Tree, TreeItem } from '@happyhyep/tree-component';

interface FileData {
  name: string;
  type: 'folder' | 'file';
  size?: number;
}

const data: TreeItem<FileData>[] = [
  {
    id: '1',
    parentId: null,
    canOpen: true,
    data: { name: 'Documents', type: 'folder' },
  },
  {
    id: '2',
    parentId: '1',
    canOpen: false,
    data: { name: 'report.pdf', type: 'file', size: 1024 },
  },
  {
    id: '3',
    parentId: '1',
    canOpen: true,
    data: { name: 'Projects', type: 'folder' },
  },
];

function MyApp() {
  const [selectedId, setSelectedId] = useState<string>();

  return (
    <Tree
      items={data}
      selectedId={selectedId}
      onItemClick={(item) => {
        setSelectedId(item.id);
        console.log('Clicked item:', item.data);

        // 💡 Both folders and files are clickable!
        if (item.data.type === 'folder') {
          console.log('Folder clicked:', item.data.name);
        } else {
          console.log('File clicked:', item.data.name);
        }
      }}
      renderLabel={(data) => (
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <span>{data.type === 'folder' ? '📁' : '📄'}</span>
          <span>{data.name}</span>
          {data.size && <span>({data.size}KB)</span>}
        </div>
      )}
    />
  );
}

2️⃣ Tree with Search

import { TreeWithSearch } from '@happyhyep/tree-component';

function SearchableTree() {
  const [selectedId, setSelectedId] = useState<string>();

  return (
    <TreeWithSearch
      items={data}
      selectedId={selectedId}
      onItemClick={(item) => setSelectedId(item.id)}
      renderLabel={(data, highlight) => (
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <span>{data.type === 'folder' ? '📁' : '📄'}</span>
          {/* 🔍 Automatic search term highlighting */}
          <HighlightText text={data.name} highlight={highlight} />
        </div>
      )}
      searchFn={(data, keyword) => data.name.toLowerCase().includes(keyword.toLowerCase())}
    >
      {/* 🎯 Built-in search input */}
      <TreeWithSearch.Input placeholder="Search files..." />
    </TreeWithSearch>
  );
}

// Highlight helper component
const HighlightText = ({ text, highlight }) => {
  if (!highlight) return <span>{text}</span>;

  const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
  return (
    <span>
      {parts.map((part, i) =>
        part.toLowerCase() === highlight.toLowerCase() ? <mark key={i}>{part}</mark> : part,
      )}
    </span>
  );
};

3️⃣ Advanced Usage - Default Expand All

function ExpandedTree() {
  return (
    <Tree
      items={data}
      defaultExpandAll={true} // 🚀 Expand all folders by default
      renderLabel={(data) => <span>{data.name}</span>}
    />
  );
}

📋 Real-world Use Cases

File Explorer

import { Tree } from '@happyhyep/tree-component';

function FileExplorer() {
  const [selectedFile, setSelectedFile] = useState(null);

  const handleItemClick = (item) => {
    if (item.data.type === 'file') {
      // File click - open file
      openFile(item.data);
    } else {
      // Folder click - select folder (expand/collapse is automatic)
      setSelectedFolder(item.data);
    }
  };

  return (
    <div style={{ display: 'flex' }}>
      <Tree
        items={fileSystemData}
        onItemClick={handleItemClick}
        renderLabel={(data) => <FileIcon type={data.type} name={data.name} />}
      />
      {selectedFile && <FilePreview file={selectedFile} />}
    </div>
  );
}

Organization Chart / Hierarchy

function OrganizationChart() {
  return (
    <Tree
      items={orgData}
      defaultExpandAll={true}
      onItemClick={(item) => {
        // Both departments and employees are clickable
        showPersonDetails(item.data);
      }}
      renderLabel={(data) => (
        <div>
          <strong>{data.name}</strong>
          <span>({data.position})</span>
        </div>
      )}
    />
  );
}

🔧 API Documentation

TreeItem Interface

interface TreeItem<T = unknown> {
  id: string; // Unique identifier
  parentId: string | null; // Parent ID (null for root)
  data: T; // User data
  canOpen?: boolean; // Whether it can be expanded
  hasLeaf?: boolean; // Whether it's a leaf node
  children?: TreeItem<T>[]; // Child nodes (auto-generated)
}

Tree Props

Props Type Required Default Description
items TreeItem<T>[] - Tree data
renderLabel (data: T) => ReactNode - Label rendering function
onItemClick (item: TreeItem<T>) => void - Click handler (both folders/files)
selectedId string - Selected item ID
defaultExpandAll boolean false Expand all folders by default
className string "" CSS class

TreeWithSearch Props

All Tree props + additional:

Props Type Required Description
searchFn (data: T, keyword: string) => boolean Search function
children ReactNode Search input, etc.

💡 Tips and Tricks

1. Conditional Click Handling

const handleClick = (item) => {
  if (item.data.type === 'folder') {
    // Folder click - special logic
    if (item.data.permissions?.canAccess) {
      navigateToFolder(item);
    } else {
      showPermissionError();
    }
  } else {
    // File click - open file
    openFile(item);
  }
};

2. Custom Search

// Multi-condition search
const advancedSearch = (data, keyword) => {
  return (
    data.name.toLowerCase().includes(keyword.toLowerCase()) ||
    data.tags?.some((tag) => tag.includes(keyword)) ||
    data.content?.includes(keyword)
  );
};

// Extension search
const extensionSearch = (data, keyword) => {
  const extension = data.name.split('.').pop();
  return extension?.toLowerCase().includes(keyword.toLowerCase());
};

3. Performance Optimization

// Memoization for large datasets
const MemoizedTree = React.memo(() => (
  <Tree
    items={largeDataSet}
    renderLabel={React.useCallback(
      (data) => (
        <span>{data.name}</span>
      ),
      [],
    )}
  />
));

🎨 Styling

Using CSS Classes

<Tree
  className="my-custom-tree"
  items={data}
  renderLabel={(data) => <span className={`item-${data.type}`}>{data.name}</span>}
/>
.my-custom-tree {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
}

.item-folder {
  font-weight: bold;
  color: #4a90e2;
}

.item-file {
  color: #666;
}

🛠️ Development

Local Development Setup

# Clone repository
git clone https://github.com/happyhyep/tree-component.git
cd tree-component

# Install dependencies
pnpm install

# Run Storybook
pnpm run storybook

# Build
pnpm run build

# Lint
pnpm run lint

Storybook

Component documentation and examples are available in Storybook:

pnpm run storybook

🤝 Contributing

  1. Fork this repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

📄 License

This project is licensed under the MIT License.

🙋‍♂️ Support


🇰🇷 한국어 ver

폴더와 파일 모두 클릭 가능한 React Tree 컴포넌트 라이브러리 입니다.

npm version License: MIT

✨ 주요 차별점

다른 Tree 컴포넌트와 달리 폴더와 파일 모두 클릭하여 상호작용할 수 있는 Tree 컴포넌트입니다.

기능 일반적인 Tree 🌳 Tree Component
폴더 클릭 ❌ 펼치기/접기만 클릭 이벤트 + 펼치기/접기
파일 클릭 ✅ 클릭 가능 ✅ 클릭 가능
검색 기능 ❌ 별도 구현 필요 내장 검색 + 하이라이트
기본 확장 상태 ❌ 수동 설정 한 번에 모든 폴더 확장
TypeScript ⚠️ 제한적 지원 완전한 타입 안전성

🎬 데모

Tree 컴포넌트 사용 예시

Basic Tree Demo

📦 설치

# npm
npm install @happyhyep/tree-component

# yarn
yarn add @happyhyep/tree-component

# pnpm
pnpm add @happyhyep/tree-component

🚀 빠른 시작

1️⃣ 기본 Tree 컴포넌트

import React, { useState } from 'react';
import { Tree, TreeItem } from '@happyhyep/tree-component';

interface FileData {
  name: string;
  type: 'folder' | 'file';
  size?: number;
}

const data: TreeItem<FileData>[] = [
  {
    id: '1',
    parentId: null,
    canOpen: true,
    data: { name: 'Documents', type: 'folder' },
  },
  {
    id: '2',
    parentId: '1',
    canOpen: false,
    data: { name: 'report.pdf', type: 'file', size: 1024 },
  },
  {
    id: '3',
    parentId: '1',
    canOpen: true,
    data: { name: 'Projects', type: 'folder' },
  },
];

function MyApp() {
  const [selectedId, setSelectedId] = useState<string>();

  return (
    <Tree
      items={data}
      selectedId={selectedId}
      onItemClick={(item) => {
        setSelectedId(item.id);
        console.log('클릭된 항목:', item.data);

        // 💡 폴더든 파일이든 모두 클릭 가능!
        if (item.data.type === 'folder') {
          console.log('폴더 클릭:', item.data.name);
        } else {
          console.log('파일 클릭:', item.data.name);
        }
      }}
      renderLabel={(data) => (
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <span>{data.type === 'folder' ? '📁' : '📄'}</span>
          <span>{data.name}</span>
          {data.size && <span>({data.size}KB)</span>}
        </div>
      )}
    />
  );
}

2️⃣ 검색 기능이 있는 Tree

import { TreeWithSearch } from '@happyhyep/tree-component';

function SearchableTree() {
  const [selectedId, setSelectedId] = useState<string>();

  return (
    <TreeWithSearch
      items={data}
      selectedId={selectedId}
      onItemClick={(item) => setSelectedId(item.id)}
      renderLabel={(data, highlight) => (
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <span>{data.type === 'folder' ? '📁' : '📄'}</span>
          {/* 🔍 검색어 자동 하이라이트 */}
          <HighlightText text={data.name} highlight={highlight} />
        </div>
      )}
      searchFn={(data, keyword) => data.name.toLowerCase().includes(keyword.toLowerCase())}
    >
      {/* 🎯 내장 검색 입력창 */}
      <TreeWithSearch.Input placeholder="파일명 검색..." />
    </TreeWithSearch>
  );
}

// 하이라이트 헬퍼 컴포넌트
const HighlightText = ({ text, highlight }) => {
  if (!highlight) return <span>{text}</span>;

  const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
  return (
    <span>
      {parts.map((part, i) =>
        part.toLowerCase() === highlight.toLowerCase() ? <mark key={i}>{part}</mark> : part,
      )}
    </span>
  );
};

3️⃣ 고급 사용법 - 모든 폴더 기본 확장

function ExpandedTree() {
  return (
    <Tree
      items={data}
      defaultExpandAll={true} // 🚀 모든 폴더 기본 확장
      renderLabel={(data) => <span>{data.name}</span>}
    />
  );
}

📋 실제 사용 사례

파일 탐색기

import { Tree } from '@happyhyep/tree-component';

function FileExplorer() {
  const [selectedFile, setSelectedFile] = useState(null);

  const handleItemClick = (item) => {
    if (item.data.type === 'file') {
      // 파일 클릭 시 - 파일 열기
      openFile(item.data);
    } else {
      // 폴더 클릭 시 - 폴더 선택 (펼치기/접기는 자동)
      setSelectedFolder(item.data);
    }
  };

  return (
    <div style={{ display: 'flex' }}>
      <Tree
        items={fileSystemData}
        onItemClick={handleItemClick}
        renderLabel={(data) => <FileIcon type={data.type} name={data.name} />}
      />
      {selectedFile && <FilePreview file={selectedFile} />}
    </div>
  );
}

조직도 / 계층 구조

function OrganizationChart() {
  return (
    <Tree
      items={orgData}
      defaultExpandAll={true}
      onItemClick={(item) => {
        // 부서든 직원이든 클릭 가능
        showPersonDetails(item.data);
      }}
      renderLabel={(data) => (
        <div>
          <strong>{data.name}</strong>
          <span>({data.position})</span>
        </div>
      )}
    />
  );
}

🔧 API 문서

TreeItem 인터페이스

interface TreeItem<T = unknown> {
  id: string; // 고유 식별자
  parentId: string | null; // 부모 ID (루트는 null)
  data: T; // 사용자 데이터
  canOpen?: boolean; // 펼칠 수 있는지 여부
  hasLeaf?: boolean; // 리프 노드 여부
  children?: TreeItem<T>[]; // 자식 노드 (자동 생성)
}

Tree Props

Props 타입 필수 기본값 설명
items TreeItem<T>[] - 트리 데이터
renderLabel (data: T) => ReactNode - 라벨 렌더링
onItemClick (item: TreeItem<T>) => void - 클릭 핸들러 (폴더/파일 모두)
selectedId string - 선택된 항목 ID
defaultExpandAll boolean false 모든 폴더 기본 확장
className string "" CSS 클래스

TreeWithSearch Props

Tree의 모든 props + 추가:

Props 타입 필수 설명
searchFn (data: T, keyword: string) => boolean 검색 함수
children ReactNode 검색 입력창 등

💡 팁과 트릭

1. 조건부 클릭 처리

const handleClick = (item) => {
  if (item.data.type === 'folder') {
    // 폴더 클릭 - 특별한 로직
    if (item.data.permissions?.canAccess) {
      navigateToFolder(item);
    } else {
      showPermissionError();
    }
  } else {
    // 파일 클릭 - 파일 열기
    openFile(item);
  }
};

2. 커스텀 검색

// 다중 조건 검색
const advancedSearch = (data, keyword) => {
  return (
    data.name.toLowerCase().includes(keyword.toLowerCase()) ||
    data.tags?.some((tag) => tag.includes(keyword)) ||
    data.content?.includes(keyword)
  );
};

// 확장자 검색
const extensionSearch = (data, keyword) => {
  const extension = data.name.split('.').pop();
  return extension?.toLowerCase().includes(keyword.toLowerCase());
};

3. 성능 최적화

// 큰 데이터셋을 위한 메모이제이션
const MemoizedTree = React.memo(() => (
  <Tree
    items={largeDataSet}
    renderLabel={React.useCallback(
      (data) => (
        <span>{data.name}</span>
      ),
      [],
    )}
  />
));

🎨 스타일링

CSS 클래스 사용

<Tree
  className="my-custom-tree"
  items={data}
  renderLabel={(data) => <span className={`item-${data.type}`}>{data.name}</span>}
/>
.my-custom-tree {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
}

.item-folder {
  font-weight: bold;
  color: #4a90e2;
}

.item-file {
  color: #666;
}

🛠️ 개발

로컬 개발 환경

# 저장소 클론
git clone https://github.com/happyhyep/tree-component.git
cd tree-component

# 의존성 설치
pnpm install

# Storybook 실행
pnpm run storybook

# 빌드
pnpm run build

# 린트
pnpm run lint

Storybook

컴포넌트 문서와 예제는 Storybook에서 확인할 수 있습니다:

pnpm run storybook

🤝 기여하기

  1. 이 저장소를 포크하세요
  2. 기능 브랜치를 만드세요 (git checkout -b feature/amazing-feature)
  3. 변경사항을 커밋하세요 (git commit -m 'Add amazing feature')
  4. 브랜치에 푸시하세요 (git push origin feature/amazing-feature)
  5. Pull Request를 열어주세요

📄 라이센스

이 프로젝트는 MIT 라이센스 하에 배포됩니다.

🙋‍♂️ 지원

About

React Tree Component Library where both folders and files are clickable

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors