/
kaws
/
ui-kit-ce
Обзор
Документация
Войти
/
kaws
/
ui-kit-ce
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
packages/tree/examples/TreeWithVirtualization.tsx
451 строка
16 KB
Урядышев Павел Александрович
refactor(tree,tree-dnd): исправление документации и фикс багов
11 июл 2025, 10:49
11 июл 2025, 10:49
6aa8bed
Код
Авторство
О чём код?
import * as React from 'react' import { Tree, TreeMainProps, TreeItem, TreeNodeItem, TreeImperativeHandlersRef, defaultTreeComponents, isEqualKeyboardKeys, DATA_ITEM_ID, TreeProps, useMergedRefs, } from '@v-uik/base' import { VariableSizeList as List, ListChildComponentProps } from 'react-window' import AutoSizer, { Size as AutoSizerSize } from 'react-virtualized-auto-sizer' /* ------------------------------------------------------------------------------------ * ----------------------------- Конфигурация компонента ------------------------------ * ------------------------------------------------------------------------------------ / /* * Заводим константное значение для базовой высоты узла дерева. */ const BASE_NODE_SIZE = 32 /* * Отдельно заводим тип для задания ссылки на компонент виртуализированного списка. */ type ListImperativeHandlersRef = List<TreeNodeItem<TreeItem>[]> /* * Определяем контекст компонента для удобного доступа к ссылкам на компонент * дерева и виртуализированного списка. */ type SharedContextProps = { treeRef: React.RefObject<TreeImperativeHandlersRef<TreeItem>> listRef?: React.MutableRefObject<ListImperativeHandlersRef | null> } type SharedContextProviderProps = SharedContextProps const SharedContext = React.createContext<SharedContextProps>( {} as SharedContextProps ) /* * Задаем контекстный провайдер, который будет позволять * передавать ссылки на компоненты дерева и виртуализированного списка * из родительского компонента в дочерние. */ const SharedContextProvider: React.FC<SharedContextProviderProps> = ({ children, ...restProps }): React.ReactElement | null => { const listRef = React.useRef<ListImperativeHandlersRef | null>(null) const Context = SharedContext as unknown as React.Context<SharedContextProps> const value = React.useMemo( () => ({ listRef, ...restProps, }), [restProps, listRef] ) return <Context.Provider value={value}>{children}</Context.Provider> } const useSharedContext = (): SharedContextProps => { const sharedContext = React.useContext<SharedContextProps>( SharedContext as unknown as React.Context<SharedContextProps> ) if (!sharedContext) { throw new Error( 'Hook useSharedContext must be used within SharedContextProvider!' ) } return sharedContext } /* * Объявляем функцию для динамического определения высоты * верхнеуровнего элемента дерева. */ const makeGetItemSize = ( tree: TreeNodeItem<TreeItem>[], treeRef: React.RefObject<TreeImperativeHandlersRef<TreeItem>> ) => (index: number) => { /* * С помощью встроенной функции компонента `Tree` `getTreeNodeVisibleChildren` * получаем количество видимых дочерних узлов для целевого узла. */ const visibleChildrenCount = treeRef.current?.getTreeNodeVisibleChildren(tree[index].key).length || 0 /* * Затем рассчитываем актуальную высоту для целевого узла дерева. */ return BASE_NODE_SIZE + visibleChildrenCount * BASE_NODE_SIZE } /* * Забираем из пакета `@v-uik/tree` компонент основного элемента дерева для * дальнейшего использования. */ const { Item } = defaultTreeComponents /* * Объявляем базовый компонент виртуализированного списка, который будет * отрисовывать верхнеуровневые элемента дерева. */ const Row: React.FC<ListChildComponentProps<TreeNodeItem<TreeItem>[]>> = ({ data, index, style, }) => { if (!Item) { return null } const getStyles = (node: TreeNodeItem<TreeItem>) => { if (node.$meta.depth === 0) { return style } return undefined } return <Item nodeIndex={index} nodeKey={data[index].key} style={getStyles} /> } /* * Объявляем функцию высшего порядка, в которой для основного элемента дерева * `ul` будут назначаться недостающие атрибуты из свойств компонента дерева * напрямую через ссылку в `ref`-объекте. */ const makeSetAttributesForUl = (props: JSX.IntrinsicElements['ul']) => (element: HTMLUListElement) => { if (!element) { return element } /* * Обнуляем левый внутренний отступ, который назначается HTML-элементу * `ul` по умолчанию. */ element.style.paddingLeft = '0px' /* * Назначаем недостающие классы компонента. */ if (props.className) { element.className = props.className } /* * Проставляем остальные data-атрибуты */ element.setAttribute('data-tree', 'true') if (props['role']) { element.setAttribute('role', props['role']) } if (props['aria-orientation']) { element.setAttribute('aria-orientation', props['aria-orientation']) } if (props['aria-label']) { element.setAttribute('aria-label', props['aria-label']) } /* * В этом месте мы можем проставить остальные пользовательские data-атрибуты. */ // element.setAttribute('data-my-attribute', 'my-value') return element } /* * Объявляем основной пользовательский компонент дерева, который будет * передан через свойство `components`. */ const TreeMain = React.forwardRef<HTMLUListElement, TreeMainProps<TreeItem>>( ({ tree, style, ...rest }, ref) => { const listRef = React.useRef<ListImperativeHandlersRef>(null) const { treeRef, listRef: listRefContext } = useSharedContext() if (listRefContext) { listRefContext.current = listRef.current } /* * Назначаем свойства, предназначенные для элемента `ul`, элементу-обертке `div`, * чтобы реализовать имитацию стандартного поведения компонента `Tree`. */ const divPropsAsTreeProps: JSX.IntrinsicElements['div'] = { ref: ref as JSX.IntrinsicElements['div']['ref'], onKeyDown: rest.onKeyDown as JSX.IntrinsicElements['div']['onKeyDown'], onFocus: rest.onFocus as JSX.IntrinsicElements['div']['onFocus'], onBlur: rest.onBlur as JSX.IntrinsicElements['div']['onBlur'], onMouseDown: rest.onMouseDown as JSX.IntrinsicElements['div']['onMouseDown'], } return ( /* * Оборачиваем компонент `AutoSizer` во `flex`-контейнер для * задания автоматической высоты и ширины дерева. */ <div style={{ display: 'flex' }}> <div {...divPropsAsTreeProps} /* * Обязательно проставляем атрибут `tabIndex` для возможности фокуса * на пседво-дерево и дальнейшего управления им с клавиатуры. */ tabIndex={0} style={{ ...style, outline: 'none !important', overflow: 'visible', flex: '1 1 auto', height: 350, }} > {/* * Используем компонент `AutoSizer` для автоматического расчета ширины * и высоты компонента `Tree`. */} <AutoSizer> {({ height, width }: AutoSizerSize) => ( /* * Отрисовываем виртуализированный список с помощью библиотеки * `react-window`. */ <List ref={listRef} innerRef={makeSetAttributesForUl(rest)} innerElementType="ul" itemCount={tree.length} /* * Динамически высчитываем высоту отдельно для каждого верхнеуровневого * элемента дерева. */ itemSize={makeGetItemSize(tree, treeRef)} height={height} width={width} itemKey={(index: number) => tree[index].key} /* * Для удобства разработки прокидываем дополнительно массив данных * дерева в свойства компонента `Row`. */ itemData={tree} > {Row} </List> )} </AutoSizer> </div> </div> ) } ) const TreeComponent = React.forwardRef< TreeImperativeHandlersRef<TreeItem>, TreeProps<TreeItem> >((props, ref) => { const { treeRef, listRef } = useSharedContext() const mergedRefs = useMergedRefs([ref, treeRef]) /* * Объявляем функцию для перерасчета высоты верхнеуровневых элементов * в виртуализированном списке, исходя из количества видимых дочерних элементов * у целевого узла дерева, и данное действие выносим из основного потока, чтобы * выполнить расчет после изменения состояния дерева. */ const recalculateItemsSize = (node: TreeNodeItem<TreeItem>) => { /* * С помощью встроенной функции компонента `Tree` `getParentRootNode` получаем * корневой родительский узел для целевого узла. */ const rootNode = treeRef.current?.getParentRootNode(node.key) if (!rootNode) { return } setTimeout(() => { listRef?.current?.resetAfterIndex(rootNode.$meta.index) }, 1) } /* * Объявляем функцию, которая будет производить прокрутку контейнера с деревом до * виртуализированного элемента дерева, который мог удалиться из DOM-дерева * средствами `react-window`. */ const scrollToVirtualizedRootNode = (node: TreeNodeItem<TreeItem>) => { if ( !treeRef.current ?.getElementRef() .current?.querySelector(`[${DATA_ITEM_ID}="${node.key}"]`) ) { const parentRoot = treeRef.current?.getParentRootNode(node.key) if (!parentRoot) { return } setTimeout(() => { listRef?.current?.scrollToItem(parentRoot.$meta.index, 'center') }, 1) } } const handleNodeClick = ( _: React.MouseEvent<HTMLLIElement, MouseEvent>, node: TreeNodeItem<TreeItem> ) => { recalculateItemsSize(node) } const handleTreeKeyDown = ( event: React.KeyboardEvent<HTMLUListElement>, node: TreeNodeItem<TreeItem> ) => { if ( isEqualKeyboardKeys('ArrowUp', event.key) || isEqualKeyboardKeys('ArrowDown', event.key) || isEqualKeyboardKeys('ArrowLeft', event.key) || isEqualKeyboardKeys('ArrowRight', event.key) || isEqualKeyboardKeys('Enter', event.key) ) { recalculateItemsSize(node) /* * Во время навигации по дереву с клавиатуры вызываем прокрутку * до сфокусированного узла дерева с помощью функции компонента * `Tree` `scrollToNode`. */ treeRef.current?.scrollToNode(node.key, { /* * В качестве шаблона прокрутки для виртуализированного дерева * рекомендуется использовать значение `center`. */ block: 'center', }) /* * Дополнительно вызываем прокрутку вне основного потока на случай, если * виртуальное дерево еще не успело отрисовать видимые узлы дерева. */ setTimeout(() => { treeRef.current?.scrollToNode(node.key, { /* * В качестве шаблона прокрутки для виртуализированного дерева * рекомендуется использовать значение `center`. */ block: 'center', }) }, 1) scrollToVirtualizedRootNode(node) } } const handleFocus = () => { /* * Вызываем событие фокуса вне основного потока, чтобы * состояние дерева успело обновиться. */ setTimeout(() => { const node = treeRef.current?.getFocusItem() if (!node) { return } recalculateItemsSize(node) scrollToVirtualizedRootNode(node) treeRef.current?.scrollToNode(node.key, { /* * В качестве шаблона прокрутки для виртуализированного дерева * рекомендуется использовать значение `center`. */ block: 'center', }) }, 1) } return ( <Tree ref={mergedRefs} checkable dataSource={dataSource} components={{ TreeMain }} defaultExpandedKeys={['node-6', 'node-3', 'node-3-1', 'node-10']} onNodeClick={handleNodeClick} onTreeKeyDown={handleTreeKeyDown} onFocus={handleFocus} /> ) }) const VirtualizedTree: React.FC<TreeProps<TreeItem>> = (props) => { const treeRef = React.useRef<TreeImperativeHandlersRef<TreeItem>>(null) return ( <SharedContextProvider treeRef={treeRef}> <TreeComponent {...props} /> </SharedContextProvider> ) } /* ------------------------------------------------------------------------------------ * ---------------------------------- Использование ----------------------------------- * ------------------------------------------------------------------------------------ / /* * Генерируем массив данных. */ const dataSource: TreeItem[] = Array.from({ length: 1500 }).map((_, i) => ({ key: `node-${i}`, label: `node-${i}`, children: Array.from({ length: 4 }).map((_, j) => ({ key: `node-${i}-${j}`, label: `node-${i}-${j}`, children: Array.from({ length: 3 }).map((_, k) => ({ key: `node-${i}-${j}-${k}`, label: `node-${i}-${j}-${k}`, children: Array.from({ length: 2 }).map((_, z) => ({ key: `node-${i}-${j}-${k}-${z}`, label: `node-${i}-${j}-${k}-${z}`, })), })), })), })) /* * Отрисовываем компонент виртуализированного дерева. */ export const TreeWithVirtualization: React.FC = () => { return <VirtualizedTree checkable dataSource={dataSource} /> }