From 8d654429e36150d63913b9ee90c70b6ad44189ec Mon Sep 17 00:00:00 2001 From: yuyang Date: Wed, 12 Jul 2023 14:15:56 +0800 Subject: [PATCH] feat(tree): support virtual scroll (#2359) * feat(tree): support virtual scroll * feat: add treeScroll hooks * feat(tree): support virtual scroll * chore: update snapshot * chore: update demo * chore: update docs --- src/hooks/useVirtualScroll.ts | 4 +- src/tree/Tree.tsx | 62 +- src/tree/TreeItem.tsx | 627 +++++++++--------- src/tree/_example/vscroll.jsx | 55 ++ src/tree/{ => hooks}/TreeDraggableContext.tsx | 8 +- src/tree/hooks/style/css.js | 1 + src/tree/hooks/style/index.js | 1 + src/tree/{ => hooks}/useControllable.ts | 4 +- src/tree/{ => hooks}/useDraggable.tsx | 6 +- src/tree/{ => hooks}/useStore.ts | 45 +- src/tree/{ => hooks}/useTreeConfig.ts | 4 +- src/tree/hooks/useTreeVirtualScroll.ts | 107 +++ src/tree/tree.en-US.md | 17 +- src/tree/tree.md | 19 +- src/tree/type.ts | 21 +- test/snap/__snapshots__/csr.test.jsx.snap | 249 +++++++ test/snap/__snapshots__/ssr.test.jsx.snap | 2 + 17 files changed, 845 insertions(+), 387 deletions(-) create mode 100644 src/tree/_example/vscroll.jsx rename src/tree/{ => hooks}/TreeDraggableContext.tsx (90%) create mode 100644 src/tree/hooks/style/css.js create mode 100644 src/tree/hooks/style/index.js rename src/tree/{ => hooks}/useControllable.ts (84%) rename src/tree/{ => hooks}/useDraggable.tsx (94%) rename src/tree/{ => hooks}/useStore.ts (76%) rename src/tree/{ => hooks}/useTreeConfig.ts (95%) create mode 100644 src/tree/hooks/useTreeVirtualScroll.ts diff --git a/src/hooks/useVirtualScroll.ts b/src/hooks/useVirtualScroll.ts index aa28628b6..406c60ad1 100644 --- a/src/hooks/useVirtualScroll.ts +++ b/src/hooks/useVirtualScroll.ts @@ -43,7 +43,6 @@ const useVirtualScroll = (container: MutableRefObject, params: UseV // 当前场景是否满足开启虚拟滚动的条件 const isVirtualScroll = useMemo(() => tScroll.type === 'virtual' && tScroll.threshold < data.length, [tScroll, data]); - const getTrScrollTopHeightList = (trHeightList: number[], containerHeight: number) => { const list: number[] = []; // 大数据场景不建议使用 forEach 一类函数迭代 @@ -78,7 +77,7 @@ const useVirtualScroll = (container: MutableRefObject, params: UseV } }; - // 固定高度场景,不需要通过行渲染获取高度(仅非固定高度场景需要) + // 仅非固定高度场景需要 const handleRowMounted = (rowData: any) => { if (!isVirtualScroll || !rowData || tScroll.isFixedRowHeight || !container?.current) return; const trHeight = rowData.ref.offsetHeight; @@ -158,6 +157,7 @@ const useVirtualScroll = (container: MutableRefObject, params: UseV setScrollHeight(data.length * tScroll.rowHeight); const startIndex = startAndEndIndex[0]; const tmpData = data.slice(startIndex, startIndex + tripleBufferSize); + setVisibleData(tmpData); const timer = setTimeout(() => { diff --git a/src/tree/Tree.tsx b/src/tree/Tree.tsx index 075c8c449..0dad47b1b 100644 --- a/src/tree/Tree.tsx +++ b/src/tree/Tree.tsx @@ -1,20 +1,24 @@ -import React, { forwardRef, useState, useImperativeHandle, useMemo, RefObject, MouseEvent } from 'react'; +import React, { forwardRef, useState, useImperativeHandle, useMemo, RefObject, MouseEvent, useRef } from 'react'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; import classNames from 'classnames'; -import { TreeNodeState, TreeNodeValue, TypeTreeNodeData, TypeTreeNodeModel } from '../_common/js/tree/types'; + import TreeNode from '../_common/js/tree/tree-node'; -import { TreeOptionData } from '../common'; -import { usePersistFn } from '../_util/usePersistFn'; -import { TreeInstanceFunctions, TdTreeProps } from './type'; -import { useTreeConfig } from './useTreeConfig'; -import useControllable from './useControllable'; +import { TreeOptionData, StyledProps } from '../common'; import { TreeItemProps } from './interface'; import TreeItem from './TreeItem'; -import { useStore } from './useStore'; -import { TreeDraggableContext } from './TreeDraggableContext'; + +import useControllable from './hooks/useControllable'; +import { useStore } from './hooks/useStore'; +import { useTreeConfig } from './hooks/useTreeConfig'; +import { TreeDraggableContext } from './hooks/TreeDraggableContext'; import parseTNode from '../_util/parseTNode'; +import { usePersistFn } from '../_util/usePersistFn'; +import useTreeVirtualScroll from './hooks/useTreeVirtualScroll'; -export type TreeProps = TdTreeProps; +import type { TreeNodeState, TreeNodeValue, TypeTreeNodeData, TypeTreeNodeModel } from '../_common/js/tree/types'; +import type { TreeInstanceFunctions, TdTreeProps } from './type'; + +export type TreeProps = TdTreeProps & StyledProps; /** * 树组件 @@ -40,6 +44,8 @@ const Tree = forwardRef((props: TreeProps, ref: React.Ref transition, // 动画默认开启 expandOnClickNode, onClick, + scroll, + style, } = props; const { value, onChange, expanded, onExpand, onActive, actived } = useControllable(props); @@ -65,7 +71,6 @@ const Tree = forwardRef((props: TreeProps, ref: React.Ref const newVisibleNodes = nodes?.filter((node) => node.visible); setVisibleNodes(newVisibleNodes); } - // 因为是被 useImperativeHandle 依赖的方法,使用 usePersistFn 变成持久化的。或者也可以使用 useCallback const setExpanded = usePersistFn( ( @@ -80,6 +85,13 @@ const Tree = forwardRef((props: TreeProps, ref: React.Ref return expanded; }, ); + const treeRef = useRef(null); + + const { visibleData, isVirtual, treeNodeStyle, cursorStyle, handleRowMounted } = useTreeVirtualScroll({ + treeRef, + scroll, + data: visibleNodes, + }); const setActived = usePersistFn( ( @@ -212,14 +224,18 @@ const Tree = forwardRef((props: TreeProps, ref: React.Ref const renderEmpty = () => parseTNode(empty, null, emptyText); - const renderItems = () => { - if (visibleNodes.length <= 0) { + const renderItems = (renderNode: TreeNode[]) => { + if (renderNode.length <= 0) { return renderEmpty(); } return ( - - {visibleNodes.map((node, index) => ( + + {renderNode.map((node, index) => ( // https://github.com/reactjs/react-transition-group/issues/668 > disableCheck={disableCheck} onClick={handleItemClick} onChange={handleChange} + onTreeItemMounted={handleRowMounted} + isVirtual={true} /> ))} @@ -266,9 +284,19 @@ const Tree = forwardRef((props: TreeProps, ref: React.Ref [treeClassNames.treeCheckable]: checkable, [treeClassNames.treeFx]: transition, [treeClassNames.treeBlockNode]: expandOnClickNode, + [`${treeClassNames.tree}__vscroll`]: isVirtual, })} + style={style} + ref={treeRef} > - {renderItems()} + {isVirtual ? ( + <> +
+ {renderItems(visibleData)} + + ) : ( + renderItems(visibleNodes) + )}
); diff --git a/src/tree/TreeItem.tsx b/src/tree/TreeItem.tsx index 68c8981bd..2f7b70a89 100644 --- a/src/tree/TreeItem.tsx +++ b/src/tree/TreeItem.tsx @@ -7,6 +7,7 @@ import React, { useRef, DragEvent, isValidElement, + useEffect, } from 'react'; import classNames from 'classnames'; import isFunction from 'lodash/isFunction'; @@ -17,345 +18,359 @@ import useDomRefCallback from '../hooks/useDomRefCallback'; import useGlobalIcon from '../hooks/useGlobalIcon'; import TreeNode from '../_common/js/tree/tree-node'; import Checkbox from '../checkbox'; -import { useTreeConfig } from './useTreeConfig'; -import { TreeItemProps } from './interface'; -import useDraggable from './useDraggable'; +import { useTreeConfig } from './hooks/useTreeConfig'; +import useDraggable from './hooks/useDraggable'; import composeRefs from '../_util/composeRefs'; import useConfig from '../hooks/useConfig'; +import type { TreeItemProps } from './interface'; + /** * 树节点组件 */ -const TreeItem = forwardRef((props: TreeItemProps, ref: React.Ref) => { - const { - node, - icon, - label, - line, - expandOnClickNode, - activable, - checkProps, - disableCheck, - operations, - onClick, - onChange, - } = props; - - const { CaretRightSmallIcon } = useGlobalIcon({ - CaretRightSmallIcon: TdCaretRightSmallIcon, - }); - const { level } = node; - - const { treeClassNames, locale } = useTreeConfig(); - const { classPrefix } = useConfig(); - - const handleClick = (evt: MouseEvent) => { - const srcTarget = evt.target as HTMLElement; - const isBranchTrigger = - node.children && - expandOnClickNode && - (srcTarget.className === `${classPrefix}-checkbox__input` || srcTarget.tagName.toLowerCase() === 'input'); - - if (isBranchTrigger) return; - - // 处理expandOnClickNode时与checkbox的选中的逻辑冲突 - if (expandOnClickNode && node.children && srcTarget.className?.indexOf?.(`${classPrefix}-tree__label`) !== -1) - evt.preventDefault(); - - onClick?.(node, { - e: evt, - expand: expandOnClickNode, - active: activable, - trigger: 'node-click', +const TreeItem = forwardRef( + ( + props: TreeItemProps & { + onTreeItemMounted?: (rowData: { ref: HTMLElement; data: TreeNode }) => void; + isVirtual?: boolean; + }, + ref: React.Ref, + ) => { + const { + node, + icon, + label, + line, + expandOnClickNode, + activable, + checkProps, + disableCheck, + operations, + onClick, + onChange, + isVirtual, + onTreeItemMounted, + } = props; + + const { CaretRightSmallIcon } = useGlobalIcon({ + CaretRightSmallIcon: TdCaretRightSmallIcon, }); - }; - - const handleItemClick = (evt: MouseEvent) => { - if (node.loading) { - return; - } - onClick?.(node, { - e: evt, - expand: true, - active: false, - trigger: 'icon-click', - }); - }; - - const handleIconClick = (evt: MouseEvent) => { - evt.stopPropagation(); - handleItemClick(evt); - }; - - const stopPropagation = (e: MouseEvent) => { - e.stopPropagation(); - }; - - /* ======== render ======= */ - const renderIcon = () => { - // 这里按 vue 的逻辑定义 - let isDefaultIcon = false; - const renderIconNode = () => { - if (icon === false) { - return null; - } - if (icon instanceof Function) { - return icon(node.getModel()); - } - if (React.isValidElement(icon)) { - return icon; - } - if (icon && icon !== true) { - // 非 ReactNode、Function、Boolean 类型,抛出错误提示 - throw new Error('invalid type of icon'); - } - - if (!node.isLeaf()) { - isDefaultIcon = true; - if (node.loading && node.expanded) { - return ; - } + const { level } = node; + const nodeRef = useRef(null); + + const { treeClassNames, locale } = useTreeConfig(); + const { classPrefix } = useConfig(); + + useEffect(() => { + onTreeItemMounted?.({ ref: nodeRef.current, data: node }); + }, [isVirtual, nodeRef, node, onTreeItemMounted]); + + const handleClick = (evt: MouseEvent) => { + const srcTarget = evt.target as HTMLElement; + const isBranchTrigger = + node.children && + expandOnClickNode && + (srcTarget.className === `${classPrefix}-checkbox__input` || srcTarget.tagName.toLowerCase() === 'input'); + + if (isBranchTrigger) return; + + // 处理expandOnClickNode时与checkbox的选中的逻辑冲突 + if (expandOnClickNode && node.children && srcTarget.className?.indexOf?.(`${classPrefix}-tree__label`) !== -1) + evt.preventDefault(); + + onClick?.(node, { + e: evt, + expand: expandOnClickNode, + active: activable, + trigger: 'node-click', + }); + }; - return ; + const handleItemClick = (evt: MouseEvent) => { + if (node.loading) { + return; } - return null; + onClick?.(node, { + e: evt, + expand: true, + active: false, + trigger: 'icon-click', + }); }; - const iconNode = renderIconNode(); - return ( - - {iconNode} - - ); - }; + const handleIconClick = (evt: MouseEvent) => { + evt.stopPropagation(); + handleItemClick(evt); + }; - const renderLine = () => { - const iconVisible = icon !== false; + const stopPropagation = (e: MouseEvent) => { + e.stopPropagation(); + }; - if (line === false) { - return null; - } - - if (isFunction(line)) { - return line(node.getModel()); - } - - if (React.isValidElement(line)) { - return line; - } - - if (node.parent && node.tree) { - // 如果节点的父节点,不是最后的节点 - // 则需要绘制节点延长线 - const shadowStyles: string[] = []; - const parents = node.getParents(); - parents.pop(); - parents.forEach((pnode: TreeNode, index: number) => { - if (!pnode.vmIsLast) { - shadowStyles.push(`calc(-${index + 1} * var(--space)) 0 var(--color)`); + /* ======== render ======= */ + const renderIcon = () => { + // 这里按 vue 的逻辑定义 + let isDefaultIcon = false; + const renderIconNode = () => { + if (icon === false) { + return null; } - }); + if (icon instanceof Function) { + return icon(node.getModel()); + } + if (React.isValidElement(icon)) { + return icon; + } + if (icon && icon !== true) { + // 非 ReactNode、Function、Boolean 类型,抛出错误提示 + throw new Error('invalid type of icon'); + } + + if (!node.isLeaf()) { + isDefaultIcon = true; + if (node.loading && node.expanded) { + return ; + } - const styles = { - '--level': level, - boxShadow: shadowStyles.join(','), + return ; + } + return null; }; + const iconNode = renderIconNode(); return ( + className={classNames(treeClassNames.treeIcon, treeClassNames.folderIcon, { + [treeClassNames.treeIconDefault]: isDefaultIcon, + })} + onClick={handleIconClick} + > + {iconNode} + ); - } - return null; - }; - - // 使用 斜八角动画 - const [labelDom, setRefCurrent] = useDomRefCallback(); - useRipple(labelDom); - - const renderLabel = () => { - const emptyView = locale('empty'); - let labelText: string | ReactNode = ''; - if (label instanceof Function) { - labelText = label(node.getModel()) || emptyView; - } else { - labelText = node.label || emptyView; - } - - const labelClasses = classNames(treeClassNames.treeLabel, treeClassNames.treeLabelStrictly, { - [treeClassNames.actived]: node.isActivable() ? node.actived : false, - }); + }; - if (node.isCheckable()) { - let checkboxDisabled: boolean; - if (typeof disableCheck === 'function') { - checkboxDisabled = disableCheck(node.getModel()); - } else { - checkboxDisabled = !!disableCheck; + const renderLine = () => { + const iconVisible = icon !== false; + + if (line === false) { + return null; } - if (node.isDisabled()) { - checkboxDisabled = true; + if (isFunction(line)) { + return line(node.getModel()); + } + + if (React.isValidElement(line)) { + return line; + } + + if (node.parent && node.tree) { + // 如果节点的父节点,不是最后的节点 + // 则需要绘制节点延长线 + const shadowStyles: string[] = []; + const parents = node.getParents(); + parents.pop(); + parents.forEach((pnode: TreeNode, index: number) => { + if (!pnode.vmIsLast) { + shadowStyles.push(`calc(-${index + 1} * var(--space)) 0 var(--color)`); + } + }); + + const styles = { + '--level': level, + boxShadow: shadowStyles.join(','), + }; + + return ( + + ); + } + return null; + }; + + // 使用 斜八角动画 + const [labelDom, setRefCurrent] = useDomRefCallback(); + useRipple(labelDom); + + const renderLabel = () => { + const emptyView = locale('empty'); + let labelText: string | ReactNode = ''; + if (label instanceof Function) { + labelText = label(node.getModel()) || emptyView; + } else { + labelText = node.label || emptyView; } + const labelClasses = classNames(treeClassNames.treeLabel, treeClassNames.treeLabelStrictly, { + [treeClassNames.actived]: node.isActivable() ? node.actived : false, + }); + + if (node.isCheckable()) { + let checkboxDisabled: boolean; + if (typeof disableCheck === 'function') { + checkboxDisabled = disableCheck(node.getModel()); + } else { + checkboxDisabled = !!disableCheck; + } + + if (node.isDisabled()) { + checkboxDisabled = true; + } + + return ( + onChange(node, ctx)} + className={labelClasses} + stopLabelTrigger={!!node.children} + {...checkProps} + > + {labelText} + + ); + } return ( - onChange(node, ctx)} + date-target="label" className={labelClasses} - stopLabelTrigger={!!node.children} - {...checkProps} + // label 可以传入 ReactNode, 如果直接取里面的 children 值,当多层级的时候会有问题 + // 所以这里判断如果 label是 ReactNode, 并且 text没有值 就不展示 title + title={isValidElement(node.label) && !node.data?.text ? '' : String(node.data?.text || node.label)} > - {labelText} - + {labelText} + ); - } - return ( - - {labelText} - - ); - }; - - const renderOperations = () => { - let operationsView = null; - if (operations) { - // ReactNode 类型处理 - if (React.isValidElement(operations)) { - operationsView = operations; - } else if (operations instanceof Function) { - // Function 类型处理 - const treeNodeModel = node?.getModel(); - operationsView = operations(treeNodeModel); - } else { - // 非 ReactNode、Function 类型,抛出错误提示 - throw new Error('invalid type of operations'); + }; + + const renderOperations = () => { + let operationsView = null; + if (operations) { + // ReactNode 类型处理 + if (React.isValidElement(operations)) { + operationsView = operations; + } else if (operations instanceof Function) { + // Function 类型处理 + const treeNodeModel = node?.getModel(); + operationsView = operations(treeNodeModel); + } else { + // 非 ReactNode、Function 类型,抛出错误提示 + throw new Error('invalid type of operations'); + } } - } - if (operationsView) { - return ( - - {operationsView} - - ); - } - return null; - }; - - const nodeRef = useRef(null); - - const { setDragStatus, isDragging, dropPosition, isDragOver } = useDraggable({ - node, - nodeRef, - }); - - const handleDragStart: DragEventHandler = (evt: DragEvent) => { - const { node } = props; - if (!node.isDraggable()) return; - evt.stopPropagation(); - setDragStatus('dragStart', evt); - - try { - // ie throw error firefox-need-it - evt.dataTransfer?.setData('text/plain', ''); - } catch (e) { - // empty - } - }; - const handleDragEnd: DragEventHandler = (evt: DragEvent) => { - const { node } = props; - if (!node.isDraggable()) return; - evt.stopPropagation(); - setDragStatus('dragEnd', evt); - }; - const handleDragOver: DragEventHandler = (evt: DragEvent) => { - const { node } = props; - if (!node.isDraggable()) return; - evt.stopPropagation(); - evt.preventDefault(); - setDragStatus('dragOver', evt); - }; - const handleDragLeave: DragEventHandler = (evt: DragEvent) => { - const { node } = props; - if (!node.isDraggable()) return; - evt.stopPropagation(); - setDragStatus('dragLeave', evt); - }; - const handleDrop: DragEventHandler = (evt: DragEvent) => { - const { node } = props; - if (!node.isDraggable()) return; - evt.stopPropagation(); - evt.preventDefault(); - setDragStatus('drop', evt); - }; - - return ( -
0, - [treeClassNames.treeNodeDragTipHighlight]: !isDragging && isDragOver && dropPosition === 0, - })} - style={ - { - '--level': level, - boxShadow: '', - } as CSSProperties + if (operationsView) { + return ( + + {operationsView} + + ); } - onClick={handleClick} - draggable={node.isDraggable()} - onDragStart={handleDragStart} - onDragEnd={handleDragEnd} - onDragOver={handleDragOver} - onDragLeave={handleDragLeave} - onDrop={handleDrop} - > - {renderLine()} - {renderIcon()} - {renderLabel()} - {renderOperations()} -
- ); -}); + return null; + }; + + const { setDragStatus, isDragging, dropPosition, isDragOver } = useDraggable({ + node, + nodeRef, + }); + + const handleDragStart: DragEventHandler = (evt: DragEvent) => { + const { node } = props; + if (!node.isDraggable()) return; + evt.stopPropagation(); + setDragStatus('dragStart', evt); + + try { + // ie throw error firefox-need-it + evt.dataTransfer?.setData('text/plain', ''); + } catch (e) { + // empty + } + }; + const handleDragEnd: DragEventHandler = (evt: DragEvent) => { + const { node } = props; + if (!node.isDraggable()) return; + evt.stopPropagation(); + setDragStatus('dragEnd', evt); + }; + const handleDragOver: DragEventHandler = (evt: DragEvent) => { + const { node } = props; + if (!node.isDraggable()) return; + evt.stopPropagation(); + evt.preventDefault(); + setDragStatus('dragOver', evt); + }; + const handleDragLeave: DragEventHandler = (evt: DragEvent) => { + const { node } = props; + if (!node.isDraggable()) return; + evt.stopPropagation(); + setDragStatus('dragLeave', evt); + }; + const handleDrop: DragEventHandler = (evt: DragEvent) => { + const { node } = props; + if (!node.isDraggable()) return; + evt.stopPropagation(); + evt.preventDefault(); + setDragStatus('drop', evt); + }; + + return ( +
0, + [treeClassNames.treeNodeDragTipHighlight]: !isDragging && isDragOver && dropPosition === 0, + })} + style={ + { + '--level': level, + boxShadow: '', + } as CSSProperties + } + onClick={handleClick} + draggable={node.isDraggable()} + onDragStart={handleDragStart} + onDragEnd={handleDragEnd} + onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + > + {renderLine()} + {renderIcon()} + {renderLabel()} + {renderOperations()} +
+ ); + }, +); TreeItem.displayName = 'TreeItem'; diff --git a/src/tree/_example/vscroll.jsx b/src/tree/_example/vscroll.jsx new file mode 100644 index 000000000..40cf0dc1d --- /dev/null +++ b/src/tree/_example/vscroll.jsx @@ -0,0 +1,55 @@ +import React, { useEffect, useState } from 'react'; +import { Tree, Switch, Space, Form } from 'tdesign-react'; + +export default () => { + const [checkable, setCheckable] = useState(true); + const [showLine, toggleShowLine] = useState(true); + const [options, setOptions] = useState([]); + + useEffect(() => { + const newOptions = []; + for (let i = 0; i < 3000; i++) { + newOptions.push({ + label: `第${i + 1}段`, + value: i, + children: [ + { + label: `第${i + 1}段第1个子节点`, + value: `${i}.1`, + }, + { + label: `第${i + 1}段第2个子节点`, + value: `${i}.2`, + }, + ], + }); + } + setOptions(newOptions); + }, []); + + const defaultChecked = ['1.2', '2.2']; + return ( + +
+ + + + + + +
+ +
+ ); +}; diff --git a/src/tree/TreeDraggableContext.tsx b/src/tree/hooks/TreeDraggableContext.tsx similarity index 90% rename from src/tree/TreeDraggableContext.tsx rename to src/tree/hooks/TreeDraggableContext.tsx index fc2020c00..4639fb24d 100644 --- a/src/tree/TreeDraggableContext.tsx +++ b/src/tree/hooks/TreeDraggableContext.tsx @@ -1,8 +1,8 @@ import { useRef, DragEvent } from 'react'; -import TreeStore from '../_common/js/tree/tree-store'; -import TreeNode from '../_common/js/tree/tree-node'; -import { TreeProps } from './Tree'; -import { createHookContext } from '../_util/createHookContext'; +import TreeStore from '../../_common/js/tree/tree-store'; +import TreeNode from '../../_common/js/tree/tree-node'; +import { TreeProps } from '../Tree'; +import { createHookContext } from '../../_util/createHookContext'; interface Value { props: TreeProps; diff --git a/src/tree/hooks/style/css.js b/src/tree/hooks/style/css.js new file mode 100644 index 000000000..f388879ac --- /dev/null +++ b/src/tree/hooks/style/css.js @@ -0,0 +1 @@ +import '../../style/index.css'; diff --git a/src/tree/hooks/style/index.js b/src/tree/hooks/style/index.js new file mode 100644 index 000000000..4a2e7d583 --- /dev/null +++ b/src/tree/hooks/style/index.js @@ -0,0 +1 @@ +import '../../style/index.js'; diff --git a/src/tree/useControllable.ts b/src/tree/hooks/useControllable.ts similarity index 84% rename from src/tree/useControllable.ts rename to src/tree/hooks/useControllable.ts index 86d124bb9..2286e775f 100644 --- a/src/tree/useControllable.ts +++ b/src/tree/hooks/useControllable.ts @@ -1,5 +1,5 @@ -import useControlled from '../hooks/useControlled'; -import { TdTreeProps } from './type'; +import useControlled from '../../hooks/useControlled'; +import { TdTreeProps } from '../type'; export default function useControllable( props: TdTreeProps, diff --git a/src/tree/useDraggable.tsx b/src/tree/hooks/useDraggable.tsx similarity index 94% rename from src/tree/useDraggable.tsx rename to src/tree/hooks/useDraggable.tsx index 68c6bd836..25b18ed31 100644 --- a/src/tree/useDraggable.tsx +++ b/src/tree/hooks/useDraggable.tsx @@ -1,9 +1,9 @@ import throttle from 'lodash/throttle'; import { RefObject, DragEvent, useState, useRef } from 'react'; -import { TreeNode } from '../_common/js/tree/tree-node'; +import { TreeNode } from '../../_common/js/tree/tree-node'; import { useTreeDraggableContext } from './TreeDraggableContext'; -import { DropPosition } from './interface'; -import { usePersistFn } from '../_util/usePersistFn'; +import { DropPosition } from '../interface'; +import { usePersistFn } from '../../_util/usePersistFn'; export default function useDraggable(props: { nodeRef: RefObject; node: TreeNode }) { const { nodeRef, node } = props; diff --git a/src/tree/useStore.ts b/src/tree/hooks/useStore.ts similarity index 76% rename from src/tree/useStore.ts rename to src/tree/hooks/useStore.ts index 8dc0a8a54..41d4777c4 100644 --- a/src/tree/useStore.ts +++ b/src/tree/hooks/useStore.ts @@ -1,13 +1,10 @@ import { useRef } from 'react'; import cloneDeep from 'lodash/cloneDeep'; -// import isEqual from 'lodash/isEqual'; -import useUpdateEffect from '../_util/useUpdateEffect'; -// import { TreeOptionData } from '../common'; -import TreeStore from '../_common/js/tree/tree-store'; -// import TreeNode from '../_common/js/tree/tree-node'; -import { usePersistFn } from '../_util/usePersistFn'; -import { TdTreeProps } from './type'; -import { TypeEventState } from './interface'; +import useUpdateEffect from '../../_util/useUpdateEffect'; +import TreeStore from '../../_common/js/tree/tree-store'; +import { usePersistFn } from '../../_util/usePersistFn'; +import type { TdTreeProps } from '../type'; +import type { TypeEventState } from '../interface'; export function useStore(props: TdTreeProps, refresh: () => void): TreeStore { const storeRef = useRef(); @@ -32,7 +29,6 @@ export function useStore(props: TdTreeProps, refresh: () => void): TreeStore { lazy, valueMode, filter, - // onDataChange, onLoad, allowFoldNodeOnFilter = false, } = props; @@ -42,34 +38,6 @@ export function useStore(props: TdTreeProps, refresh: () => void): TreeStore { refresh(); }); - // const handleReflow = usePersistFn(() => { - // if (!onDataChange) { - // return; - // } - - // const nodes = storeRef.current.getNodes(); - - // const rootNodes = nodes.filter((v) => !v.parent); - - // const getChild = (list: TreeNode[] | boolean) => { - // if (Array.isArray(list) && list.length > 0) { - // return list.map((v) => { - // const nodeData: TreeOptionData = v.data; - // if (Array.isArray(v.children) && v.children.length > 0) { - // nodeData.children = getChild(v.children); - // } - // return nodeData; - // }); - // } - // }; - - // const newData = getChild(rootNodes); - - // if (!isEqual(newData, data)) { - // onDataChange?.(newData); - // } - // }); - const getExpandedArr = (arr: TdTreeProps['expanded'], store: TreeStore) => { const expandedMap = new Map(); arr.forEach((val) => { @@ -109,7 +77,6 @@ export function useStore(props: TdTreeProps, refresh: () => void): TreeStore { }, onUpdate: handleUpdate, allowFoldNodeOnFilter, - // onReflow: handleReflow, }); // 初始化 store 的节点排列 + 状态 @@ -140,8 +107,6 @@ export function useStore(props: TdTreeProps, refresh: () => void): TreeStore { store.setActived(actived); } - // refresh(); - store.refreshNodes(); return store; }; diff --git a/src/tree/useTreeConfig.ts b/src/tree/hooks/useTreeConfig.ts similarity index 95% rename from src/tree/useTreeConfig.ts rename to src/tree/hooks/useTreeConfig.ts index 815cc522e..1b642279e 100644 --- a/src/tree/useTreeConfig.ts +++ b/src/tree/hooks/useTreeConfig.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react'; -import useConfig from '../hooks/useConfig'; -import { useLocaleReceiver } from '../locale/LocalReceiver'; +import useConfig from '../../hooks/useConfig'; +import { useLocaleReceiver } from '../../locale/LocalReceiver'; export function useTreeConfig() { const { classPrefix: prefix } = useConfig(); diff --git a/src/tree/hooks/useTreeVirtualScroll.ts b/src/tree/hooks/useTreeVirtualScroll.ts new file mode 100644 index 000000000..dd7b866b8 --- /dev/null +++ b/src/tree/hooks/useTreeVirtualScroll.ts @@ -0,0 +1,107 @@ +import { useMemo, useCallback, useEffect, CSSProperties } from 'react'; +import useVirtualScroll from '../../hooks/useVirtualScroll'; +import TreeNode from '../../_common/js/tree/tree-node'; + +import type { TScroll } from '../../common'; + +export default function useTreeVirtualScroll({ + treeRef, + scroll, + data = [], +}: { + data: TreeNode[]; + scroll: TScroll; + treeRef: React.MutableRefObject; +}) { + const scrollThreshold = scroll?.threshold || 100; + const scrollType = scroll?.type; + + const isVirtual = useMemo( + () => scrollType === 'virtual' && data?.length > scrollThreshold, + [scrollType, scrollThreshold, data], + ); + + const scrollParams = useMemo( + () => + ({ + type: 'virtual', + isFixedRowHeight: scroll?.isFixedRowHeight || false, // expand and collapse operation make height of tree item different + rowHeight: scroll?.rowHeight || 34, + bufferSize: scroll?.bufferSize || 20, + threshold: scrollThreshold, + } as const), + [scroll, scrollThreshold], + ); + + const { + visibleData = null, + handleScroll: handleVirtualScroll = null, + scrollHeight = null, + translateY = null, + handleRowMounted = null, + } = useVirtualScroll(treeRef, { + data: data || [], + scroll: scrollParams, + }); + let lastScrollY = -1; + const onInnerVirtualScroll = useCallback( + (e: WheelEvent) => { + if (!isVirtual) { + return; + } + const target = e.target as HTMLElement; + const top = target.scrollTop; + // 排除横向滚动出发的纵向虚拟滚动计算 + if (Math.abs(lastScrollY - top) > 5) { + handleVirtualScroll(); + // eslint-disable-next-line react-hooks/exhaustive-deps + lastScrollY = top; + } else { + lastScrollY = -1; + } + }, + [isVirtual, data], + ); + + useEffect(() => { + const treeList = treeRef?.current; + if (isVirtual) { + treeList?.addEventListener?.('scroll', onInnerVirtualScroll); + } + return () => { + // 卸载时取消监听 + if (isVirtual) { + treeList?.removeEventListener?.('scroll', onInnerVirtualScroll); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isVirtual, onInnerVirtualScroll, treeRef.current]); + + const cursorStyle = { + position: 'absolute', + width: '1px', + height: '1px', + transition: 'transform 0.2s', + transform: `translate(0, ${scrollHeight}px)`, + MsTransform: `translate(0, ${scrollHeight}px)`, + MozTransform: `translate(0, ${scrollHeight}px)`, + WebkitTransform: `translate(0, ${scrollHeight}px)`, + } as CSSProperties; + + const treeNodeStyle = { + transform: `translate(0, ${translateY}px)`, + MsTransform: `translate(0, ${translateY}px)`, + MozTransform: `translate(0, ${translateY}px)`, + WebkitTransform: `translate(0, ${translateY}px)`, + } as CSSProperties; + + return { + scrollHeight, + translateY, + visibleData, + handleRowMounted, + isVirtual, + cursorStyle, + treeNodeStyle, + }; +} diff --git a/src/tree/tree.en-US.md b/src/tree/tree.en-US.md index c39fceb67..085b38990 100644 --- a/src/tree/tree.en-US.md +++ b/src/tree/tree.en-US.md @@ -10,7 +10,6 @@ style | Object | - | 样式,Typescript:`React.CSSProperties` | N activable | Boolean | false | \- | N activeMultiple | Boolean | false | \- | N actived | Array | - | Typescript:`Array` | N -defaultActived | Array | - | uncontrolled property。Typescript:`Array` | N allowFoldNodeOnFilter | Boolean | false | \- | N checkProps | Object | - | Typescript:`CheckboxProps`,[Checkbox API Documents](./checkbox?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/tree/type.ts) | N checkStrictly | Boolean | false | \- | N @@ -26,7 +25,6 @@ expandMutex | Boolean | false | \- | N expandOnClickNode | Boolean | false | \- | N expandParent | Boolean | false | \- | N expanded | Array | [] | Typescript:`Array` | N -defaultExpanded | Array | [] | uncontrolled property。Typescript:`Array` | N filter | Function | - | Typescript:`(node: TreeNodeModel) => boolean` | N hover | Boolean | - | \- | N icon | TNode | true | Typescript:`boolean \| TNode>`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N @@ -36,10 +34,11 @@ lazy | Boolean | true | \- | N line | TNode | false | Typescript:`boolean \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N load | Function | - | Typescript:`(node: TreeNodeModel) => Promise>` | N operations | TElement | - | Typescript:`TNode>`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +scroll | Object | - | lazy load and virtual scroll。Typescript:`TScroll`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N transition | Boolean | true | \- | N value | Array | [] | Typescript:`Array` `type TreeNodeValue = string \| number`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/tree/type.ts) | N defaultValue | Array | [] | uncontrolled property。Typescript:`Array` `type TreeNodeValue = string \| number`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/tree/type.ts) | N -valueMode | String | onlyLeaf | options:onlyLeaf/parentFirst/all | N +valueMode | String | onlyLeaf | options: onlyLeaf/parentFirst/all | N onActive | Function | | Typescript:`(value: Array, context: { node: TreeNodeModel; e?: MouseEvent; trigger: 'node-click' \| 'setItem' }) => void`
| N onChange | Function | | Typescript:`(value: Array, context: { node: TreeNodeModel; e?: any; trigger: 'node-click' \| 'setItem' }) => void`
| N onClick | Function | | Typescript:`(context: { node: TreeNodeModel; e: MouseEvent }) => void`
| N @@ -50,6 +49,7 @@ onDragStart | Function | | Typescript:`(context: { e: DragEvent; node: TreeNo onDrop | Function | | Typescript:`(context: { e: DragEvent; dragNode: TreeNodeModel; dropNode: TreeNodeModel; dropPosition: number; }) => void`
| N onExpand | Function | | Typescript:`(value: Array, context: { node: TreeNodeModel; e?: MouseEvent; trigger: 'node-click' \| 'icon-click' \| 'setItem' }) => void`
| N onLoad | Function | | Typescript:`(context: { node: TreeNodeModel }) => void`
| N +onScroll | Function | | Typescript:`(params: { e: WheelEvent }) => void`
trigger on content scroll | N ### TreeInstanceFunctions 组件实例方法 @@ -78,6 +78,7 @@ actived | Boolean | false | \- | N checkable | Boolean | false | \- | N checked | Boolean | false | \- | N disabled | Boolean | false | \- | N +draggable | Boolean | true | \- | N expandMutex | Boolean | false | \- | N expanded | Boolean | false | \- | N indeterminate | Boolean | false | \- | N @@ -117,3 +118,13 @@ isLast | \- | `boolean` | required isLeaf | \- | `boolean` | required remove | `(value?: TreeNodeValue)` | \- | required setData | `(data: T)` | \- | required。set node data, `T` extends `TreeOptionData` + +### TScroll + +name | type | default | description | required +-- | -- | -- | -- | -- +bufferSize | Number | 20 | \- | N +isFixedRowHeight | Boolean | false | \- | N +rowHeight | Number | - | \- | N +threshold | Number | 100 | \- | N +type | String | - | required。options: lazy/virtual | Y diff --git a/src/tree/tree.md b/src/tree/tree.md index b9d229d22..16563f3f7 100644 --- a/src/tree/tree.md +++ b/src/tree/tree.md @@ -10,7 +10,6 @@ style | Object | - | 样式,TS 类型:`React.CSSProperties` | N activable | Boolean | false | 节点是否可高亮 | N activeMultiple | Boolean | false | 是否允许多个节点同时高亮 | N actived | Array | - | 高亮的节点值。TS 类型:`Array` | N -defaultActived | Array | - | 高亮的节点值。非受控属性。TS 类型:`Array` | N allowFoldNodeOnFilter | Boolean | false | 是否允许在过滤时节点折叠节点 | N checkProps | Object | - | 透传属性到 checkbox 组件。参考 checkbox 组件 API。TS 类型:`CheckboxProps`,[Checkbox API Documents](./checkbox?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/tree/type.ts) | N checkStrictly | Boolean | false | 父子节点选中状态不再关联,可各自选中或取消 | N @@ -26,20 +25,20 @@ expandMutex | Boolean | false | 同级别展开互斥,手风琴效果 | N expandOnClickNode | Boolean | false | 是否支持点击节点也能展开收起 | N expandParent | Boolean | false | 展开子节点时是否自动展开父节点 | N expanded | Array | [] | 展开的节点值。TS 类型:`Array` | N -defaultExpanded | Array | [] | 展开的节点值。非受控属性。TS 类型:`Array` | N filter | Function | - | 节点过滤方法,只呈现返回值为 true 的节点,泛型 `T` 表示树节点 TS 类型。TS 类型:`(node: TreeNodeModel) => boolean` | N hover | Boolean | - | 节点是否有悬浮状态 | N icon | TNode | true | 节点图标,可自定义。TS 类型:`boolean \| TNode>`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N -keys | Object | - | 用来定义 value / label / children 在 `options` 中对应的字段别名。TS 类型:`TreeKeysType` `interface TreeKeysType { value?: string; label?: string; children?: string }`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/tree/type.ts) | N +keys | Object | - | 用来定义 `value / label / children` 在 `data` 数据中对应的字段别名,示例:`{ value: 'key', label 'name', children: 'list' }`。TS 类型:`TreeKeysType` `interface TreeKeysType { value?: string; label?: string; children?: string }`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/tree/type.ts) | N label | TNode | true | 自定义节点内容,值为 `false` 不显示,值为 `true` 显示默认 label,值为字符串直接输出该字符串。泛型 `T` 表示树节点 TS 类型。
如果期望只有点击复选框才选中,而点击节点不选中,可以使用 `label` 自定义节点,然后加上点击事件 `e.preventDefault()`,通过调整自定义节点的宽度和高度决定禁止点击选中的范围。TS 类型:`string \| boolean \| TNode>`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N lazy | Boolean | true | 延迟加载 children 为 true 的节点的子节点数据,即使 expandAll 被设置为 true,也同样延迟加载 | N line | TNode | false | 连接线。值为 false 不显示连接线;值为 true 显示默认连接线;值类型为 Function 表示自定义连接线。TS 类型:`boolean \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N load | Function | - | 加载子数据的方法,在展开节点时调用(仅当节点 children 为 true 时生效),泛型 `T` 表示树节点 TS 类型。TS 类型:`(node: TreeNodeModel) => Promise>` | N operations | TElement | - | 自定义节点操作项,泛型 `T` 表示树节点 TS 类型。TS 类型:`TNode>`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +scroll | Object | - | 懒加载和虚拟滚动。为保证组件收益最大化,当数据量小于阈值 `scroll.threshold` 时,无论虚拟滚动的配置是否存在,组件内部都不会开启虚拟滚动,`scroll.threshold` 默认为 `100`。TS 类型:`TScroll`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N transition | Boolean | true | 节点展开折叠时是否使用过渡动画 | N value | Array | [] | 选中值(组件为可选状态时)。TS 类型:`Array` `type TreeNodeValue = string \| number`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/tree/type.ts) | N defaultValue | Array | [] | 选中值(组件为可选状态时)。非受控属性。TS 类型:`Array` `type TreeNodeValue = string \| number`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/tree/type.ts) | N -valueMode | String | onlyLeaf | 选中值模式。all 表示父节点和子节点全部会出现在选中值里面;parentFirst 表示当子节点全部选中时,仅父节点在选中值里面;onlyLeaf 表示无论什么情况,选中值仅呈现叶子节点。可选项:onlyLeaf/parentFirst/all | N +valueMode | String | onlyLeaf | 选中值模式。all 表示父节点和子节点全部会出现在选中值里面;parentFirst 表示当子节点全部选中时,仅父节点在选中值里面;onlyLeaft 表示无论什么情况,选中值仅呈现叶子节点。可选项:onlyLeaf/parentFirst/all | N onActive | Function | | TS 类型:`(value: Array, context: { node: TreeNodeModel; e?: MouseEvent; trigger: 'node-click' \| 'setItem' }) => void`
节点激活时触发,泛型 `T` 表示树节点 TS 类型 | N onChange | Function | | TS 类型:`(value: Array, context: { node: TreeNodeModel; e?: any; trigger: 'node-click' \| 'setItem' }) => void`
节点选中状态变化时触发,context.node 表示当前变化的选项,泛型 `T` 表示树节点 TS 类型 | N onClick | Function | | TS 类型:`(context: { node: TreeNodeModel; e: MouseEvent }) => void`
节点点击时触发,泛型 `T` 表示树节点 TS 类型 | N @@ -50,6 +49,7 @@ onDragStart | Function | | TS 类型:`(context: { e: DragEvent; node: TreeNod onDrop | Function | | TS 类型:`(context: { e: DragEvent; dragNode: TreeNodeModel; dropNode: TreeNodeModel; dropPosition: number; }) => void`
节点在目标元素上释放时触发,泛型 `T` 表示树节点 TS 类型 | N onExpand | Function | | TS 类型:`(value: Array, context: { node: TreeNodeModel; e?: MouseEvent; trigger: 'node-click' \| 'icon-click' \| 'setItem' }) => void`
节点展开或收起时触发,泛型 `T` 表示树节点 TS 类型 | N onLoad | Function | | TS 类型:`(context: { node: TreeNodeModel }) => void`
异步加载后触发,泛型 `T` 表示树节点 TS 类型 | N +onScroll | Function | | TS 类型:`(params: { e: WheelEvent }) => void`
滚动事件 | N ### TreeInstanceFunctions 组件实例方法 @@ -78,6 +78,7 @@ actived | Boolean | false | 节点是否被激活 | N checkable | Boolean | false | 节点是否允许被选中 | N checked | Boolean | false | 节点是否被选中 | N disabled | Boolean | false | 节点是否被禁用 | N +draggable | Boolean | true | 该节点是否允许被拖动,当树本身开启时,默认允许 | N expandMutex | Boolean | false | 子节点是否互斥展开 | N expanded | Boolean | false | 节点是否已展开 | N indeterminate | Boolean | false | 节点是否为半选中状态 | N @@ -117,3 +118,13 @@ isLast | \- | `boolean` | 必需。是否为兄弟节点中的最后一个节点 isLeaf | \- | `boolean` | 必需。是否为叶子节点 remove | `(value?: TreeNodeValue)` | \- | 必需。移除当前节点或当前节点的子节点,值为空则移除当前节点,值存在则移除当前节点的子节点 setData | `(data: T)` | \- | 必需。设置节点数据,数据变化可自动刷新页面,泛型 `T` 表示树节点 TS 类型,继承 `TreeOptionData` + +### TScroll + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +bufferSize | Number | 20 | 表示除可视区域外,额外渲染的行数,避免快速滚动过程中,新出现的内容来不及渲染从而出现空白 | N +isFixedRowHeight | Boolean | false | 表示每行内容是否同一个固定高度,仅在 `scroll.type` 为 `virtual` 时有效,该属性设置为 `true` 时,可用于简化虚拟滚动内部计算逻辑,提升性能,此时则需要明确指定 `scroll.rowHeight` 属性的值 | N +rowHeight | Number | - | 行高,不会给``元素添加样式高度,仅作为滚动时的行高参考。一般情况不需要设置该属性。如果设置,可尽量将该属性设置为每行平均高度,从而使得滚动过程更加平滑 | N +threshold | Number | 100 | 启动虚拟滚动的阈值。为保证组件收益最大化,当数据量小于阈值 `scroll.threshold` 时,无论虚拟滚动的配置是否存在,组件内部都不会开启虚拟滚动 | N +type | String | - | 必需。滚动加载类型,有两种:懒加载和虚拟滚动。
值为 `lazy` ,表示滚动时会进行懒加载,非可视区域内的内容将不会默认渲染,直到该内容可见时,才会进行渲染,并且已渲染的内容滚动到不可见时,不会被销毁;
值为`virtual`时,表示会进行虚拟滚动,无论滚动条滚动到哪个位置,同一时刻,仅渲染该可视区域内的内容,当需要展示的数据量较大时,建议开启该特性。可选项:lazy/virtual | Y diff --git a/src/tree/type.ts b/src/tree/type.ts index 6e6b711e3..3e50bee36 100644 --- a/src/tree/type.ts +++ b/src/tree/type.ts @@ -5,8 +5,8 @@ * */ import { CheckboxProps } from '../checkbox'; -import { TNode, TreeOptionData } from '../common'; -import { MouseEvent, DragEvent } from 'react'; +import { TNode, TreeOptionData, TScroll } from '../common'; +import { MouseEvent, WheelEvent, DragEvent } from 'react'; export interface TdTreeProps { /** @@ -118,7 +118,7 @@ export interface TdTreeProps { */ icon?: boolean | TNode>; /** - * 用来定义 value / label / children 在 `options` 中对应的字段别名 + * 用来定义 `value / label / children` 在 `data` 数据中对应的字段别名,示例:`{ value: 'key', label 'name', children: 'list' }` */ keys?: TreeKeysType; /** @@ -144,6 +144,10 @@ export interface TdTreeProps { * 自定义节点操作项,泛型 `T` 表示树节点 TS 类型 */ operations?: TNode>; + /** + * 懒加载和虚拟滚动。为保证组件收益最大化,当数据量小于阈值 `scroll.threshold` 时,无论虚拟滚动的配置是否存在,组件内部都不会开启虚拟滚动,`scroll.threshold` 默认为 `100` + */ + scroll?: TScroll; /** * 节点展开折叠时是否使用过渡动画 * @default true @@ -160,7 +164,7 @@ export interface TdTreeProps { */ defaultValue?: Array; /** - * 选中值模式。all 表示父节点和子节点全部会出现在选中值里面;parentFirst 表示当子节点全部选中时,仅父节点在选中值里面;onlyLeaf 表示无论什么情况,选中值仅呈现叶子节点 + * 选中值模式。all 表示父节点和子节点全部会出现在选中值里面;parentFirst 表示当子节点全部选中时,仅父节点在选中值里面;onlyLeaft 表示无论什么情况,选中值仅呈现叶子节点 * @default onlyLeaf */ valueMode?: 'onlyLeaf' | 'parentFirst' | 'all'; @@ -222,6 +226,10 @@ export interface TdTreeProps { * 异步加载后触发,泛型 `T` 表示树节点 TS 类型 */ onLoad?: (context: { node: TreeNodeModel }) => void; + /** + * 滚动事件 + */ + onScroll?: (params: { e: WheelEvent }) => void; } /** 组件实例方法 */ @@ -298,6 +306,11 @@ export interface TreeNodeState { * @default false */ disabled?: boolean; + /** + * 该节点是否允许被拖动,当树本身开启时,默认允许 + * @default true + */ + draggable?: boolean; /** * 子节点是否互斥展开 * @default false diff --git a/test/snap/__snapshots__/csr.test.jsx.snap b/test/snap/__snapshots__/csr.test.jsx.snap index ef2035328..5dd82fb29 100644 --- a/test/snap/__snapshots__/csr.test.jsx.snap +++ b/test/snap/__snapshots__/csr.test.jsx.snap @@ -287900,6 +287900,255 @@ exports[`csr snapshot test > csr test src/tree/_example/sync.jsx 1`] = ` } `; +exports[`csr snapshot test > csr test src/tree/_example/vscroll.jsx 1`] = ` +{ + "asFragment": [Function], + "baseElement": +