/
sdds
/
plasma
Обзор
Документация
Войти
/
sdds
/
plasma
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
dev
website/sdds-dfa-docs/docs/components/Tree.mdx
729 строк
28 KB
Dima Shugaev
feat(*): Spinner size correction
10 фев 2026, 15:51
10 фев 2026, 15:51
bc248e5
Код
Авторство
О чём код?
--- id: tree title: Tree --- import { PropsTable } from '@site/src/components'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Tree Многоуровневый раскрывающийся список в виде дерева с возможностью выбора (selection) и выделения (checkbox). <PropsTable name="Tree" /> Обязательным параметром является только массив `items`. Формат следующий: ```tsx type TreeItem = { /** * Уникальный идентификатор элемента. */ key: React.Key; /** * Заголовок элемента. */ title?: React.ReactNode | ((data: TreeItem) => React.ReactNode); /** * Classname для текущего элемента. */ className?: string; /** * Стиль для текущего элемента. */ style?: React.CSSProperties; /** * Флаг выключения элемента. * @default false */ disabled?: boolean; /** * Иконка для текущего элемента. */ icon?: React.ReactNode; /** * Дочерние items. */ children?: TreeItem[]; /** * Контент справа. */ contentRight?: React.ReactNode; }; ``` ## Примеры <Tabs> <TabItem value="uncontrolled" label="Uncontrolled" default> ```tsx live import React from 'react'; import { Tree } from '@salutejs/sdds-dfa'; export function App() { const treeData = [ { title: 'Основной каталог', key: '0-0', children: [ { title: 'Документы', key: '0-0-0', children: [ { title: 'Отчёты', key: '0-0-0-0', disabled: true, }, { title: 'Проекты', key: '0-0-0-1', children: [ { title: 'Проект Альфа', key: '0-0-0-1-0', }, { title: 'Проект Бета', key: '0-0-0-1-1', }, ], }, { title: 'Категории', key: '0-0-0-2', children: [ { title: 'Категория Гамма', key: '0-0-0-2-0', }, { title: 'Категория Дельта', key: '0-0-0-2-1', }, ], }, ], }, { title: 'Медиа', key: '0-0-1', children: [ { title: 'Фотографии', key: '0-0-1-0', }, ], }, ], }, { title: 'Корзина', key: '0-1', children: [ { title: 'Удалённые файлы', key: '0-1-0', }, ], }, ]; return ( <div style={{ display: 'block' }}> <Tree items={treeData} defaultExpandAll /> </div> ); } ``` </TabItem> <TabItem value="controlled" label="Controlled"> ```tsx live import React from 'react'; import { Tree } from '@salutejs/sdds-dfa'; export function App() { const treeData = [ { title: 'Основной каталог', key: '0-0', children: [ { title: 'Документы', key: '0-0-0', children: [ { title: 'Отчёты', key: '0-0-0-0', disabled: true, }, { title: 'Проекты', key: '0-0-0-1', children: [ { title: 'Проект Альфа', key: '0-0-0-1-0', }, { title: 'Проект Бета', key: '0-0-0-1-1', }, ], }, { title: 'Категории', key: '0-0-0-2', children: [ { title: 'Категория Гамма', key: '0-0-0-2-0', }, { title: 'Категория Дельта', key: '0-0-0-2-1', }, ], }, ], }, { title: 'Медиа', key: '0-0-1', children: [ { title: 'Фотографии', key: '0-0-1-0', }, ], }, ], }, { title: 'Корзина', key: '0-1', children: [ { title: 'Удалённые файлы', key: '0-1-0', }, ], }, ]; const [expanded, setExpanded] = React.useState<React.Key[]>([]); const [checked, setChecked] = React.useState<React.Key[]>([]); const [selected, setSelected] = React.useState<React.Key[]>([]); return ( <div style={{ display: 'block' }}> <Tree items={treeData} checkable expandedKeys={expanded} onTreeExpand={setExpanded} checkedKeys={checked} onTreeCheck={setChecked} selectedKeys={selected} onTreeSelect={setSelected} /> </div> ); } ``` </TabItem> <TabItem value="virtual" label="Virtual"> Для включения виртуализации обязательными являются 2 свойства: `height` и `itemHeight`. \ Также есть опциональный флаг для явного отключения/включения виртуализации: `virtual`. ```tsx live import React from 'react'; import { Tree } from '@salutejs/sdds-dfa'; export function App() { return ( <div style={{ display: 'block' }}> <Tree items={Array(1000) .fill(1) .map((_, i) => ({ key: i.toString(), title: i.toString() }))} height={400} itemHeight={48} /> </div> ); } ``` </TabItem> </Tabs> ## Drag'n'Drop Для включения режима Drag and Drop нужно включить свойство `draggable`. Также имеется ряд обработчиков, однако наиболее важным из них является `onDrop`. Для более индивидуальной настройки логики работы механизма Drag'n'Drop данный обработчик настраивается на стороне клиента. Примеры ниже будут более наглядными: <Tabs> <TabItem value="default" label="Default" default> Данный пример базовый, покрывающий наиболее распространенные сценарии использования. Также присутствует возможность создавать из крайних элементов группы, вкладывая в них другие элементы. ```tsx live import React from 'react'; import { Tree, TreeProps, TreeItem } from '@salutejs/sdds-dfa'; export function App() { const treeData = [ { title: 'Основной каталог', key: '0-0', children: [ { title: 'Документы', key: '0-0-0', children: [ { title: 'Отчёты', key: '0-0-0-0', disabled: true, }, { title: 'Проекты', key: '0-0-0-1', children: [ { title: 'Проект Альфа', key: '0-0-0-1-0', }, { title: 'Проект Бета', key: '0-0-0-1-1', }, ], }, { title: 'Категории', key: '0-0-0-2', children: [ { title: 'Категория Гамма', key: '0-0-0-2-0', }, { title: 'Категория Дельта', key: '0-0-0-2-1', }, ], }, ], }, { title: 'Медиа', key: '0-0-1', children: [ { title: 'Фотографии', key: '0-0-1-0', }, ], }, ], }, { title: 'Корзина', key: '0-1', children: [ { title: 'Удалённые файлы', key: '0-1-0', }, ], }, ]; const [items, setItems] = React.useState(treeData); const onDrop: TreeProps['onDrop'] = (info) => { const dropKey = info.node.key; const dragKey = info.dragNode.key; const dropPos = info.node.pos.split('-'); const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); // Функция-хелпер для прохода по дереву элементов const loop = ( data: TreeItem[], key: React.Key, callback: (node: TreeItem, i: number, data: TreeItem[]) => void, ) => { data.forEach((item, index, arr) => { if (item.key === key) { callback(item, index, arr); return; } if (item.children) { loop(item.children, key, callback); } }); }; const data = [...items]; // Ищем draggable-элемент let dragObj: TreeItem; loop(data, dragKey, (item, index, arr) => { arr.splice(index, 1); dragObj = item; }); if (dropPosition === 0) { // Вставляем элемент внутрь target-элемента loop(data, dropKey, (item) => { item.children = item.children || []; item.children.unshift(dragObj); }); } else { // Вставляем элемент вначале или после target-элемента let ar: TreeItem[] = []; let i: number; loop(data, dropKey, (_item, index, arr) => { ar = arr; i = index; }); if (dropPosition === -1) { ar.splice(i!, 0, dragObj!); } else { ar.splice(i! + 1, 0, dragObj!); } } setItems(data); }; const allowDrop: TreeProps['allowDrop'] = (info) => { return !info.dropNode.disabled; }; return ( <div style={{ display: 'block' }}> <Tree items={items} draggable onDrop={onDrop} allowDrop={allowDrop} defaultExpandAll /> </div> ); } ``` </TabItem> <TabItem value="allowDrop" label="Allow drop"> Если нужно запретить создавать группы из крайних элементов, нужно воспользоваться свойством `allowDrop`. ```tsx live import React from 'react'; import { Tree, TreeProps, TreeItem } from '@salutejs/sdds-dfa'; export function App() { const treeData = [ { title: 'Основной каталог', key: '0-0', children: [ { title: 'Документы', key: '0-0-0', children: [ { title: 'Отчёты', key: '0-0-0-0', disabled: true, }, { title: 'Проекты', key: '0-0-0-1', children: [ { title: 'Проект Альфа', key: '0-0-0-1-0', }, { title: 'Проект Бета', key: '0-0-0-1-1', }, ], }, { title: 'Категории', key: '0-0-0-2', children: [ { title: 'Категория Гамма', key: '0-0-0-2-0', }, { title: 'Категория Дельта', key: '0-0-0-2-1', }, ], }, ], }, { title: 'Медиа', key: '0-0-1', children: [ { title: 'Фотографии', key: '0-0-1-0', }, ], }, ], }, { title: 'Корзина', key: '0-1', children: [ { title: 'Удалённые файлы', key: '0-1-0', }, ], }, ]; const [items, setItems] = React.useState(treeData); const onDrop: TreeProps['onDrop'] = (info) => { const dropKey = info.node.key; const dragKey = info.dragNode.key; const dropPos = info.node.pos.split('-'); const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); // Функция-хелпер для прохода по дереву элементов const loop = ( data: TreeItem[], key: React.Key, callback: (node: TreeItem, i: number, data: TreeItem[]) => void, ) => { data.forEach((item, index, arr) => { if (item.key === key) { callback(item, index, arr); return; } if (item.children) { loop(item.children, key, callback); } }); }; const data = [...items]; // Ищем draggable-элемент let dragObj: TreeItem; loop(data, dragKey, (item, index, arr) => { arr.splice(index, 1); dragObj = item; }); if (dropPosition === 0) { // Вставляем элемент внутрь target-элемента loop(data, dropKey, (item) => { item.children = item.children || []; item.children.unshift(dragObj); }); } else { // Вставляем элемент вначале или после target-элемента let ar: TreeItem[] = []; let i: number; loop(data, dropKey, (_item, index, arr) => { ar = arr; i = index; }); if (dropPosition === -1) { ar.splice(i!, 0, dragObj!); } else { ar.splice(i! + 1, 0, dragObj!); } } setItems(data); }; const allowDrop: TreeProps['allowDrop'] = (info) => { if (info.dropNode.disabled || !info.dropNode.children && info.dropPosition === 0) { return false; } return true; }; return ( <div style={{ display: 'block' }}> <Tree items={items} draggable onDrop={onDrop} allowDrop={allowDrop} defaultExpandAll /> </div> ); } ``` </TabItem> </Tabs> ## Пример с autoExpandParent Иногда может возникнуть необходимость раскрыть дерево до нужного ключа пробросив только его в поле `expanded`. В стандартном сценарии ничего не произойдет, т.к. вышестоящие уровни будут закрыты. Поэтому данное поведение можно реализовать с помощью свойства `autoExpandParent`. ```tsx live import React from 'react'; import { Tree, Button } from '@salutejs/sdds-dfa'; export function App() { const [expanded, setExpanded] = React.useState<React.Key[]>([]); const [autoExpandParent, setAutoExpandParent] = React.useState<boolean>(true); const treeData = [ { title: 'Основной каталог', key: '0-0', children: [ { title: 'Документы', key: '0-0-0', children: [ { title: 'Отчёты', key: '0-0-0-0', disabled: true, }, { title: 'Проекты', key: '0-0-0-1', children: [ { title: 'Проект Альфа', key: '0-0-0-1-0', }, { title: 'Проект Бета', key: '0-0-0-1-1', }, ], }, { title: 'Категории', key: '0-0-0-2', children: [ { title: 'Категория Гамма', key: '0-0-0-2-0', }, { title: 'Категория Дельта', key: '0-0-0-2-1', }, ], }, ], }, { title: 'Медиа', key: '0-0-1', children: [ { title: 'Фотографии', key: '0-0-1-0', }, ], }, ], }, { title: 'Корзина', key: '0-1', children: [ { title: 'Удалённые файлы', key: '0-1-0', }, ], }, ]; const handleExpand = (expandedValue) => { setAutoExpandParent(false); setExpanded(expandedValue); }; return ( <div style={{ display: 'flex', gap: '50px' }}> <Button onClick={() => { setAutoExpandParent(true); setExpanded(['0-0-0']); }} > Открыть уровень "Документы" </Button> <Tree items={treeData} expandedKeys={expanded} onTreeExpand={handleExpand} autoExpandParent={autoExpandParent} /> </div> ); } ``` ## Асинхронная загрузка элементов Для настройки асинхронной загрузки узлов дерева предусмотрены свойства: `loadData`, `onLoadData` и `isLeaf`. Один из вариантов реализации этого механизма представлен ниже. ```tsx live export function App() { const [treeData, setTreeData] = React.useState([ { title: 'Открыть для загрузки', key: '0' }, { title: 'Конечный элемент', key: '1', isLeaf: true }, ]); interface DataNode { title: string; key: string; isLeaf?: boolean; children?: DataNode[]; } // Носит иллюстрационный характер и должно настраиваться в индивидуальном порядке на стороне пользователя. const updateTreeData = (list: DataNode[], key: React.Key, children: DataNode[]): DataNode[] => list.map((node) => { if (node.key === key) { return { ...node, children, }; } if (node.children) { return { ...node, children: updateTreeData(node.children, key, children), }; } return node; }); return <Tree loadData={({ key, children }) => new Promise<void>((resolve) => { if (children) { resolve(); return; } setTimeout(() => { setTreeData((origin) => updateTreeData(origin, key, [{ title: 'Загруженный элемент', key: `${ key }-0` }]), ); resolve(); }, 2000); })} items={ treeData }/>; } ```