عرض شجري
عقد في مستويات تُفتح وتُغلق. الفروع المفتوحة تُسطَّح في قائمة واحدة تعرض ما يظهر فقط، فتبقى 10,000 عقدة سريعة.
client - الحجم لم يُقَس بعد
عرض شجري
client - الحجم لم يُقَس بعدعقد في مستويات تُفتح وتُغلق. الفروع المفتوحة تُسطَّح في قائمة واحدة تعرض ما يظهر فقط، فتبقى 10,000 عقدة سريعة.
جرّبه
visibleRows
loadMoreThreshold
status
الشيفرة
tree-view.tsx
import { fileText } from 'topclassui/icons/file-text'
import { folder } from 'topclassui/icons/folder'
import { Svg } from 'topclassui/svg'
import { TreeView, type TreeViewDesignProps } from 'topclassui/tree-view'
<TreeView aria-label="Files" items={ITEMS} defaultExpanded={['src', 'app']} />items بيانات عادية وأيقونة العقدة عنصر، فيمرّر مكوّن الخادم الشجرة كما هي، ولا يوجد getKey.
كل الحالات، بدون تمرير الفأرة
4 حالاتجارٍ تحميل المزيد
جارٍ التحميل
لا يوجد شيء بعد
أو استورده من الحزمة
topclassui/tree-view
import { TreeView } from 'topclassui/tree-view'ثبّت مرة واحدة:npm i topclassui@beta zod
أمثلة
- العقدة هي
{ id, label, icon?, disabled?, children? }. الـidهو المفتاح، والعقدة التي تحملchildren، ولو[]، فرع. - لتحميل أبناء فرع، استمع إلى
onExpandedChangeومرّرهم عبرitems. loadMoreيطلب الصفحة التالية من العقد الجذرية، مرة لكل صفحة، ما دامhasMore. الشجرة لا تجلب البيانات أبدًا.visibleRowsيحدّد عدد الصفوف الظاهرة قبل أن تُمرَّر الشجرة، وإن لم يُمرَّر يُطبَّق المتغير--tc-list-rows. وvirtualizeيعرض ما يظهر فقط فوق 200 صف ظاهر.statusيجعل الشجرة مشغولة فيloadingوloading-more، ويقول إنه لا يوجد شيء فيempty.classNameوstyleوrefوكل خاصية أخرى تصل إلى العنصر الذي يحملrole="tree"، لا إلى الصندوق المحيط به.
basic
basic.tsx
import { fileText } from 'topclassui/icons/file-text'
import { folder } from 'topclassui/icons/folder'
import { Svg } from 'topclassui/svg'
import { TreeView } from 'topclassui/tree-view'
const FOLDER = <Svg icon={folder} size="0.875rem" />
const FILE = <Svg icon={fileText} size="0.875rem" />
/** The drawn tree: two folders open. Each icon is an element, which crosses from a server page as it is. */
const ITEMS = [
{
id: 'src', label: 'src', icon: FOLDER, children: [
{
id: 'app', label: 'app', icon: FOLDER, children: [
{ id: 'layout', label: 'layout.tsx', icon: FILE },
{ id: 'page', label: 'page.tsx', icon: FILE },
],
},
{ id: 'components', label: 'components', icon: FOLDER, children: [{ id: 'button', label: 'button.tsx', icon: FILE }] },
{ id: 'lib', label: 'lib', icon: FOLDER, children: [] },
],
},
{ id: 'package', label: 'package.json', icon: FILE },
{ id: 'readme', label: 'README.md', icon: FILE },
]
export default function Demo() {
return <TreeView aria-label="Files" items={ITEMS} defaultExpanded={['src', 'app']} />
}
selected
Project
selected.tsx
import { TreeView } from 'topclassui/tree-view'
const ITEMS = [
{
id: 'app', label: 'app', children: [
{ id: 'layout', label: 'layout.tsx' },
{ id: 'page', label: 'page.tsx' },
],
},
{ id: 'docs', label: 'docs', children: [{ id: 'guide', label: 'guide.md' }, { id: 'faq', label: 'faq.md', disabled: true }] },
{ id: 'config', label: 'next.config.ts' },
]
/** One file selected inside an open folder, as the card draws it, beside a closed folder that holds a disabled file. */
export default function Demo() {
return (
<div className="grid gap-1">
<span id="tree-view-selected" className="text-sm font-medium text-body">Project</span>
<TreeView aria-labelledby="tree-view-selected" items={ITEMS} defaultValue="page" defaultExpanded={['app']} />
</div>
)
}
load-more
Repository
Pages asked for: 0, loaded: 1
load-more.tsx
'use client'
import { useCallback, useState } from 'react'
import { TreeView } from 'topclassui/tree-view'
const PAGE = 20
const LAST = 4
/** One page of the server's answer, made here so the demo needs no network: folders of two files each. */
function pageOf(page: number) {
return Array.from({ length: PAGE }, (_, index) => {
const n = page * PAGE + index + 1
return {
id: `d${String(n)}`,
label: `Folder ${String(n)}`,
children: [{ id: `d${String(n)}-a`, label: `a-${String(n)}.ts` }, { id: `d${String(n)}-b`, label: `b-${String(n)}.ts` }],
}
})
}
/** The tree asks and never fetches: loadMore is called once per page of root folders as the tree nears its end, and
* the page holds what has arrived. Four pages of twenty, each after a short wait. */
export default function Demo() {
const [items, setItems] = useState(() => pageOf(0))
const [asked, setAsked] = useState(0)
const [loading, setLoading] = useState(false)
const pages = items.length / PAGE
const loadMore = useCallback(() => {
setAsked((n) => n + 1)
setLoading(true)
return new Promise<void>((resolve) => {
setTimeout(() => {
setItems((current) => [...current, ...pageOf(current.length / PAGE)])
setLoading(false)
resolve()
}, 300)
})
}, [])
return (
<div className="grid gap-1">
<span id="tree-view-load-more" className="text-sm font-medium text-body">Repository</span>
<TreeView
aria-labelledby="tree-view-load-more"
items={items}
hasMore={pages < LAST}
loadMore={loadMore}
status={loading ? 'loading-more' : 'idle'}
/>
<p data-testid="asked" className="text-xs text-muted">{`Pages asked for: ${String(asked)}, loaded: ${String(pages)}`}</p>
</div>
)
}
status
جارٍ التحميل
لا يوجد شيء بعد
status.tsx
import { TreeView } from 'topclassui/tree-view'
/** loading: the tree is busy and says Loading until its first nodes arrive. empty: it says there is nothing. Both
* texts sit under the tree, in a status line, since a tree holds tree items only. */
export default function Demo() {
return (
<div className="grid gap-3">
<TreeView aria-label="Files, loading" status="loading" items={[]} />
<TreeView aria-label="Files, empty" status="empty" items={[]} />
</div>
)
}
ten-thousand
ten-thousand.tsx
import { TreeView } from 'topclassui/tree-view'
/** Ten folders of a thousand files, all open: 10,010 visible rows, built once at module scope so the array is stable. */
const items = Array.from({ length: 10 }, (_, folder) => ({
id: `f${String(folder + 1)}`,
label: `Folder ${String(folder + 1)}`,
children: Array.from({ length: 1000 }, (__, file) => ({
id: `f${String(folder + 1)}-${String(file + 1)}`,
label: `File ${String(folder * 1000 + file + 1)}.ts`,
})),
}))
const open = items.map((node) => node.id)
/** Windowed: a few dozen rows are mounted whatever the scroll, and the keys still reach every one of them. */
export default function Demo() {
return <TreeView aria-label="Ten thousand files" items={items} defaultExpanded={open} visibleRows={12} />
}
long-content
long-content.tsx
import { TreeView } from 'topclassui/tree-view'
const WORD = 'Supercalifragilistic'.repeat(15)
/** Ten levels deep, each folder the only child of the one above. */
function nest(depth: number): { id: string, label: string, children?: { id: string, label: string }[] } {
const id = `level-${String(depth)}`
if (depth === 10) return { id, label: `Level ${String(depth)}, ${WORD}` }
return { id, label: `Level ${String(depth)}`, children: [nest(depth + 1)] }
}
const sixty = Array.from({ length: 60 }, (_, index) => ({ id: `item-${String(index + 1)}`, label: `Item ${String(index + 1)}` }))
const items = [
{ id: 'word', label: WORD },
{ id: 'mixed', label: 'تقرير report 2026 النهائي final version with a long name that goes on' },
nest(1),
{ id: 'sixty', label: 'Sixty items', children: sixty },
]
const open = ['sixty', ...Array.from({ length: 9 }, (_, index) => `level-${String(index + 1)}`)]
/** Every hostile case: a 300-character word, mixed directions, ten levels deep and sixty siblings. Nothing widens the page. */
export default function Demo() {
return <TreeView aria-label="Every hostile case" items={items} defaultExpanded={open} defaultValue="word" />
}
بالعربية، من اليمين إلى اليسار
rtl.tsx
import { fileText } from 'topclassui/icons/file-text'
import { folder } from 'topclassui/icons/folder'
import { TopClassProvider } from 'topclassui/provider'
import { Svg } from 'topclassui/svg'
import { TreeView } from 'topclassui/tree-view'
const FOLDER = <Svg icon={folder} size="0.875rem" />
const FILE = <Svg icon={fileText} size="0.875rem" />
const ITEMS = [
{
id: 'docs', label: 'المستندات', icon: FOLDER, children: [
{ id: 'report', label: 'التقرير السنوي.pdf', icon: FILE },
{ id: 'budget', label: 'الميزانية.xlsx', icon: FILE },
],
},
{ id: 'photos', label: 'الصور', icon: FOLDER, children: [{ id: 'trip', label: 'رحلة العقبة.jpg', icon: FILE }] },
{ id: 'notes', label: 'ملاحظات.txt', icon: FILE },
]
/** Arabic, inside TopClassProvider as the root layout would mount it: the indent is on the right, the closed caret
* points left, and ArrowLeft opens a branch. */
export default function Demo() {
return (
<TopClassProvider direction="rtl" locale="ar">
<div dir="rtl" lang="ar">
<TreeView aria-label="الملفات" items={ITEMS} defaultValue="report" defaultExpanded={['docs']} />
</div>
</TopClassProvider>
)
}
الخصائص
لوحة المفاتيح وإمكانية الوصول
- الشجرة محطة Tab واحدة وتحتفظ بالتركيز، و
aria-activedescendantيسمّي الصف الذي تعمل عليه المفاتيح، فلا يضيع التركيز مع العرض الجزئي. - الشجرة لا ترسم اسمًا لنفسها: الأنواع تشترط
aria-labelأوaria-labelledby. - كل صف يحمل مستواه وموضعه بين إخوته وهل هو مفتوح، فيقول قارئ الشاشة أين هو.
من اليمين إلى اليسار
المسافة البادئة على اليمين، والسهم المغلق يشير إلى اليسار. ArrowLeft يفتح الفرع وArrowRight يغلقه.
الخادم والمتصفح
مكوّن متصفح: يُعرض على الخادم ثم يُرطَّب في المتصفح.
الملفات: index.tsx, client.tsx.
على الشارة: client - الحجم لم يُقَس بعد.