代码演示
如何引入
import { Tree } from '@fibrous/semi-ui-react';
基本用法
最简单的用法,默认为单选模式,每一级菜单项均可选择。
代码
import { Tree } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
const treeData = [
{
label: '亚洲',
value: 'Asia',
key: '0',
children: [
{
label: '中国',
value: 'China',
key: '0-0',
children: [
{
label: '北京',
value: 'Beijing',
key: '0-0-0',
},
{
label: '上海',
value: 'Shanghai',
key: '0-0-1',
},
],
},
],
},
{
label: '北美洲',
value: 'North America',
key: '1',
},
];
const style = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
return <Tree treeData={treeData} defaultExpandAll style={style} />;
}多选
设置 multiple,可以进行多选。多选情况下所有子项都被选择时,自动勾选显示其父项。
代码
import { Tree } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
const treeData = [
{
label: 'Asia',
value: 'Asia',
key: '0',
children: [
{
label: 'China',
value: 'China',
key: '0-0',
children: [
{
label: 'Beijing',
value: 'Beijing',
key: '0-0-0',
},
{
label: 'Shanghai',
value: 'Shanghai',
key: '0-0-1',
},
{
label: 'Chengdu',
value: 'Chengdu',
key: '0-0-2',
},
],
},
{
label: 'Japan',
value: 'Japan',
key: '0-1',
children: [
{
label: 'Osaka',
value: 'Osaka',
key: '0-1-0',
},
],
},
],
},
{
label: 'North America',
value: 'North America',
key: '1',
children: [
{
label: 'United States',
value: 'United States',
key: '1-0',
},
{
label: 'Canada',
value: 'Canada',
key: '1-1',
},
],
},
];
const style = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
return <Tree treeData={treeData} multiple defaultExpandAll style={style} />;
}可搜索的
通过设置 filterTreeNode 属性可支持搜索功能。默认对 label 值进行搜索,可通过 treeNodeFilterProp 更改。如果只希望展示过滤后的结果,可以设置 showFilteredOnly 。
代码
import { useState } from 'react';
import { Tree, Switch } from '@fibrous/semi-ui-react';
const treeData = [
{
label: 'Asia',
value: 'Asia',
key: '0',
children: [
{
label: 'China',
value: 'China',
key: '0-0',
children: [
{
label: 'Beijing',
value: 'Beijing',
key: '0-0-0',
},
{
label: 'Shanghai',
value: 'Shanghai',
key: '0-0-1',
},
],
},
{
label: 'Japan',
value: 'Japan',
key: '0-1',
children: [
{
label: 'Osaka',
value: 'Osaka',
key: '0-1-0',
},
],
},
],
},
{
label: 'North America',
value: 'North America',
key: '1',
children: [
{
label: 'United States',
value: 'United States',
key: '1-0',
},
{
label: 'Canada',
value: 'Canada',
key: '1-1',
},
],
},
];
const style = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
export default function TreeDemo() {
const [showFilteredOnly, setShowFilteredOnly] = useState(false);
return (
<>
<span id="tree-searchable-filtered-only">showFilteredOnly</span>
<Switch aria-labelledby="tree-searchable-filtered-only" checked={showFilteredOnly} onChange={setShowFilteredOnly} size="small" />
<br />
<Tree treeData={treeData} multiple filterTreeNode showFilteredOnly={showFilteredOnly} style={style} />
</>
);
}设置 filterTreeNode 属性开启搜索后,可以通过设置 searchRender 自定义搜索框的渲染方法,设置为false时可以隐藏搜索框。
代码
import { Tree, Input } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
const treeData = [
{
label: 'Asia',
value: 'Asia',
key: '0',
children: [
{
label: 'China',
value: 'China',
key: '0-0',
children: [
{
label: 'Beijing',
value: 'Beijing',
key: '0-0-0',
},
{
label: 'Shanghai',
value: 'Shanghai',
key: '0-0-1',
},
],
},
{
label: 'Japan',
value: 'Japan',
key: '0-1',
children: [
{
label: 'Osaka',
value: 'Osaka',
key: '0-1-0',
},
],
},
],
},
{
label: 'North America',
value: 'North America',
key: '1',
children: [
{
label: 'United States',
value: 'United States',
key: '1-0',
},
{
label: 'Canada',
value: 'Canada',
key: '1-1',
},
],
},
];
return <Tree filterTreeNode searchRender={(searchProps) => <Input {...searchProps} prefix="Search" />} treeData={treeData} />;
}手动触发搜索
可以通过ref的方式获取tree的实例,调用tree的search方法进行搜索。注意需要同时设置filterTreeNode开启搜索,如果搜索框在tree外部,可以通过设置searchRender=false隐藏tree内部的搜索框。
代码
import { useRef } from 'react';
import { Tree, Input, type TreeHandle } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
const ref = useRef<TreeHandle>(null);
const treeData = [
{
label: 'Asia',
value: 'Asia',
key: '0',
children: [
{
label: 'China',
value: 'China',
key: '0-0',
children: [
{
label: 'Beijing',
value: 'Beijing',
key: '0-0-0',
},
{
label: 'Shanghai',
value: 'Shanghai',
key: '0-0-1',
},
],
},
{
label: 'Japan',
value: 'Japan',
key: '0-1',
children: [
{
label: 'Osaka',
value: 'Osaka',
key: '0-1-0',
},
],
},
],
},
{
label: 'North America',
value: 'North America',
key: '1',
children: [
{
label: 'United States',
value: 'United States',
key: '1-0',
},
{
label: 'Canada',
value: 'Canada',
key: '1-1',
},
],
},
];
return (
<div>
<Input aria-label="filter tree" prefix="Search" showClear onChange={(v) => ref.current?.search(v)} />
<div style={{ marginTop: 20 }}>搜索结果如下:</div>
<Tree ref={ref} filterTreeNode searchRender={false} treeData={treeData} blockNode={false} />
</div>
);
}简单 JSON 格式的数据
可以通过 treeDataSimpleJson 传入 JSON 形式的 treeNodes 数据。此时 key-value 键值对中的 key 值将作为 TreeNodeData 的 key 和 label,value 值将作为 TreeNodeData 的 value。返回值为包含选中节点的 JSON 数据。
代码
import { Tree } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
const json = {
Node1: {
'Child Node1': '0-0-1',
'Child Node2': '0-0-2',
},
Node2: '0-1',
};
const style = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
return <Tree treeDataSimpleJson={json} multiple onChange={(e) => console.log('当前所有选中项: ', e)} onSelect={(e) => console.log('当前选项: ', e)} style={style} />;
}行显示节点
可以通过设置 blockNode 使节点显示为整行,此时悬浮选中高亮状态都会显示整行。默认打开。关闭时只高亮节点 label。
代码
import { Tree } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
const treeData = [
{
label: 'Asia',
value: 'Asia',
key: '0',
children: [
{
label: 'China',
value: 'China',
key: '0-0',
children: [
{
label: 'Beijing',
value: 'Beijing',
key: '0-0-0',
},
{
label: 'Shanghai',
value: 'Shanghai',
key: '0-0-1',
},
],
},
{
label: 'Japan',
value: 'Japan',
key: '0-1',
children: [
{
label: 'Osaka',
value: 'Osaka',
key: '0-1-0',
},
],
},
],
},
{
label: 'North America',
value: 'North America',
key: '1',
children: [
{
label: 'United States',
value: 'United States',
key: '1-0',
},
{
label: 'Canada',
value: 'Canada',
key: '1-1',
},
],
},
];
return (
<div>
<Tree treeData={treeData} defaultValue="Shanghai" blockNode={false} />
<br />
<Tree treeData={treeData} defaultValue="Shanghai" multiple blockNode={false} />
</div>
);
}自定义节点内容
TreeNodeData 的 label 属性支持传入 ReactNode 来自定义显示的节点内容。注意如果设置 filterTreeNode 开启搜索,默认是对 label 的值进行搜索,当 label 为节点时,需要自定义 filterTreeNode 的函数来满足搜索需求。
在v>=1.6.0的版本中,你也可以使用 renderLabel 来传入自定义的渲染方法,此时搜索值仍为treeData中的相应的label属性。
节点内容最好只用于展示。节点里的按钮、链接虽然可以用 Tab 到达(见 Accessibility),但读屏软件会把它们当作节点名称的一部分,方向键浏览时也不会经过它们。需要对节点执行的操作请放在树外,例如 onContextMenu 打开的菜单或树旁的工具栏。
代码
import { Tree, Tag } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
// Node content is display-only: focus stays on the tree, so buttons and links inside are keyboard-unreachable
const tag = (text: string) => <Tag size="small">{text}</Tag>;
const style = {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
};
const treeDataWithNode = [
{
label: (
<div style={style}>
<span>亚洲</span>
{tag('大洲')}
</div>
),
value: 'yazhou',
key: 'yazhou',
children: [
{
label: (
<div style={style}>
<span>中国</span>
{tag('国家')}
</div>
),
value: 'zhongguo',
key: 'zhongguo',
},
{
label: (
<div style={style}>
<span>日本</span>
{tag('国家')}
</div>
),
value: 'riben',
key: 'riben',
},
],
},
];
const treeStyle = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
return <Tree treeData={treeDataWithNode} style={treeStyle} />;
}过长省略。在v>=1.6.0的版本中,可以使用 renderLabel 来实现文本过长省略的效果。
代码
import type { ReactNode } from 'react';
import { Tree, Typography } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
const renderLabel = (label?: ReactNode) => (
<Typography.Text ellipsis={{ showTooltip: true }} style={{ width: '100%' }}>
{label}
</Typography.Text>
);
const treeDataWithNode = [
{
label: '亚洲亚洲亚洲亚洲亚洲亚洲亚洲亚洲亚洲亚洲亚洲亚洲',
value: 'yazhou',
key: 'yazhou',
children: [
{
label: '中国中国中国中国中国中国中国中国中国中国中国中国',
value: 'zhongguo',
key: 'zhongguo',
},
{
label: '日本日本日本日本日本日本日本日本日本日本日本日本',
value: 'riben',
key: 'riben',
},
],
},
];
const treeStyle = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
return <Tree treeData={treeDataWithNode} renderLabel={renderLabel} style={treeStyle} />;
}自定义图标
通过设置 icon 属性可添加自定义图标。
代码
import { IconMapPin } from '@fibrous/semi-icons-react';
import { Tree } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
const treeData = [
{
label: 'Asia',
value: 'Asia',
key: '0',
icon: <IconMapPin style={{ color: 'var(--semi-color-text-2)' }} />,
children: [
{
label: 'China',
value: 'China',
key: '0-0',
icon: <IconMapPin style={{ color: 'var(--semi-color-text-2)' }} />,
},
{
label: 'Japan',
value: 'Japan',
key: '0-1',
icon: <IconMapPin style={{ color: 'var(--semi-color-text-2)' }} />,
},
],
},
];
const style = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
return <Tree treeData={treeData} style={style} />;
}目录树模式
通过设置 directory 属性可显示为目录树模式。目录树模式下自带目录图标,可以通过自定义图标覆盖。
代码
import { Tree } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
const treeData = [
{
label: 'Asia',
value: 'Asia',
key: '0',
children: [
{
label: 'China',
value: 'China',
key: '0-0',
children: [
{
label: 'Beijing',
value: 'Beijing',
key: '0-0-0',
},
{
label: 'Shanghai',
value: 'Shanghai',
key: '0-0-1',
},
],
},
{
label: 'Japan',
value: 'Japan',
key: '0-1',
children: [
{
label: 'Osaka',
value: 'Osaka',
key: '0-1-0',
},
],
},
],
},
{
label: 'North America',
value: 'North America',
key: '1',
children: [
{
label: 'United States',
value: 'United States',
key: '1-0',
},
{
label: 'Canada',
value: 'Canada',
key: '1-1',
},
],
},
];
const style = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
return <Tree treeData={treeData} directory style={style} />;
}禁用
可以使用 disableStrictly 来开启严格禁用。开启严格禁用后,当节点是 disabled 的时候,则不能通过子级或者父级的关系改变选中状态。
代码
import { Tree } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
const treeData = [
{
label: 'Asia',
value: 'Asia',
key: '0',
children: [
{
label: 'China',
value: 'China',
key: '0-0',
children: [
{
label: 'Beijing',
value: 'Beijing',
key: '0-0-0',
disabled: true,
},
{
label: 'Shanghai',
value: 'Shanghai',
key: '0-0-1',
disabled: true,
},
],
},
{
label: 'Japan',
value: 'Japan',
key: '0-1',
children: [
{
label: 'Osaka',
value: 'Osaka',
key: '0-1-0',
},
],
},
],
},
{
label: 'North America',
value: 'North America',
key: '1',
children: [
{
label: 'United States',
value: 'United States',
key: '1-0',
},
{
label: 'Canada',
value: 'Canada',
key: '1-1',
},
],
},
];
const style = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
return <Tree treeData={treeData} defaultValue="Shanghai" multiple disableStrictly style={style} />;
}节点选中关系
版本:>= 2.5.0
多选时,可以使用 checkRelation 来设置节点选中关系的类型,可选:'related'(默认)、'unRelated'。当选中关系为 'unRelated',意味着节点之间的选中互不影响。
代码
import { Tree } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
const treeData = [
{
label: 'Asia',
value: 'Asia',
key: '0',
children: [
{
label: 'China',
value: 'China',
key: '0-0',
children: [
{
label: 'Beijing',
value: 'Beijing',
key: '0-0-0',
},
{
label: 'Shanghai',
value: 'Shanghai',
key: '0-0-1',
},
{
label: 'Chengdu',
value: 'Chengdu',
key: '0-0-2',
},
],
},
{
label: 'Japan',
value: 'Japan',
key: '0-1',
children: [
{
label: 'Osaka',
value: 'Osaka',
key: '0-1-0',
},
],
},
],
},
{
label: 'North America',
value: 'North America',
key: '1',
children: [
{
label: 'United States',
value: 'United States',
key: '1-0',
},
{
label: 'Canada',
value: 'Canada',
key: '1-1',
},
],
},
];
const style = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
return <Tree treeData={treeData} multiple checkRelation="unRelated" defaultExpandAll style={style} />;
}默认展开
defaultExpandAll 和 expandAll 均可以设置 Tree 的默认展开/收起状态。二者的区别是,defaultExpandAll 只在初始化时生效,而 expandAll 不仅会在初始化时生效,当数据(treeData/treeDataSimpleJson)发生动态更新时,expandAll 也仍然生效。
其中,expandAll 是从 1.30.0 开始支持的。
在下面的 demo 中,点击按钮更新 TreeData 后,defaultExpandAll 失效,expandAll 仍然生效。
代码
import { useState } from 'react';
import { Tree, Button, type TreeProps } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
const json = {
Node1: {
'Child Node1': '0-0-1',
'Child Node2': '0-0-2',
},
Node2: '0-1',
};
const json2 = {
Node3: {
'Child Node1': '0-0-1',
'Child Node2': '0-0-2',
'Child Node3': '0-0-3',
'Child Node4': '0-0-4',
},
Node2: '0-1',
};
const style = {
marginRight: 20,
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
const [tree, setTree] = useState<TreeProps['treeDataSimpleJson']>(json);
const handleClick = () => {
setTree(json2);
};
return (
<>
<Button onClick={handleClick} style={{ marginBottom: 10 }}>
点击更新 TreeData
</Button>
<div style={{ display: 'flex' }}>
<div>
<span>defaultExpandAll</span>
<Tree defaultExpandAll treeDataSimpleJson={tree} style={style} />
</div>
<div>
<span>expandAll</span>
<Tree expandAll treeDataSimpleJson={tree} style={style} />
</div>
</div>
</>
);
}开启搜索的展开受控
传入 expandedKeys 时即为展开受控组件,可以配合 onExpand 使用。当展开受控时,如果开启 filterTreeNode 并进行搜索是不会再自动展开节点的,此时,节点的展开完全由 expandedKeys 来控制。你可以利用 onSearch 的入参 filteredExpandedKeys(version: >= 2.38.0) 来实现展开受控时的搜索展开效果。
代码
import { useState } from 'react';
import { Tree } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const treeData = [
{
label: '亚洲',
value: 'Asia',
key: '0',
children: [
{
label: '中国',
value: 'China',
key: '0-0',
children: [
{
label: '北京',
value: 'Beijing',
key: '0-0-0',
},
{
label: '上海',
value: 'Shanghai',
key: '0-0-1',
},
],
},
{
label: '日本',
value: 'Japan',
key: '0-1',
},
],
},
{
label: '北美洲',
value: 'North America',
key: '1',
},
];
return (
<Tree
style={{ width: 300 }}
treeData={treeData}
filterTreeNode
expandedKeys={expandedKeys}
onExpand={(expandedKeys) => {
setExpandedKeys(expandedKeys);
}}
onSearch={(_inputValue, filteredExpandedKeys) => {
setExpandedKeys([...filteredExpandedKeys]);
}}
/>
);
}受控
传入 value 时即为受控组件,可以配合 onChange 使用。
代码
import { useState } from 'react';
import { Tree } from '@fibrous/semi-ui-react';
const treeData = [
{
label: '亚洲',
value: 'Asia',
key: '0',
children: [
{
label: '中国',
value: 'China',
key: '0-0',
children: [
{
label: '北京',
value: 'Beijing',
key: '0-0-0',
},
{
label: '上海',
value: 'Shanghai',
key: '0-0-1',
},
],
},
],
},
{
label: '北美洲',
value: 'North America',
key: '1',
},
];
const style = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
export default function TreeDemo() {
const [value, setValue] = useState('Shanghai');
return (
<Tree
treeData={treeData}
value={value}
onChange={(next) => {
if (typeof next === 'string') {
setValue(next);
}
}}
style={style}
/>
);
}自动展开父节点
在展开受控的情况下,当开启了 autoExpandParent ,如果想要收起父元素,则需要把它的所有子元素均收起后才可以。默认情况下,autoExpandParent 为 false,即:父元素收起不受到子元素的影响。
代码
import { useState } from 'react';
import { Tree } from '@fibrous/semi-ui-react';
const treeData = [
{
label: '亚洲',
value: 'Asia',
key: '0',
children: [
{
label: '中国',
value: 'China',
key: '0-0',
children: [
{
label: '北京',
value: 'Beijing',
key: '0-0-0',
},
{
label: '上海',
value: 'Shanghai',
key: '0-0-1',
},
],
},
],
},
{
label: '北美洲',
value: 'North America',
key: '1',
},
];
const style = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
export default function TreeDemo() {
const [expandedKeys, setExpandedKeys] = useState(['0', '0-0']);
return (
<>
<div>需要先将“中国”节点收起后,才能够收起“亚洲”节点</div>
<br />
<Tree autoExpandParent treeData={treeData} onExpand={setExpandedKeys} expandedKeys={expandedKeys} style={style} />
</>
);
}自定义展开 Icon
可以通过 expandIcon 自定义展开 Icon。 支持传入 ReactNode 或者函数。expandIcon 自 2.75.0 开始支持。
expandIcon: ReactNode | ((props: {
onClick: (e: MouseEvent) => void;
className: string;
expanded: boolean;
}))
示例如下:
expandIcon 是 ReactNode
expandIcon 是函数
代码
import { useCallback, type MouseEvent } from 'react';
import { IconMinus, IconPlus, IconChevronDown } from '@fibrous/semi-icons-react';
import { Tree } from '@fibrous/semi-ui-react';
interface ExpandIconProps {
expanded: boolean;
onClick: (e: MouseEvent) => void;
className: string;
}
export default function TreeDemo() {
const treeData = [
{
label: '亚洲',
key: 'yazhou',
children: [
{
label: '中国',
key: 'zhongguo',
children: [
{
label: '北京',
key: 'beijing',
},
{
label: '上海',
key: 'shanghai',
},
],
},
{
label: '日本',
key: 'riben',
},
],
},
{
label: '北美洲',
key: 'beimeizhou',
},
];
const expandIconFunc = useCallback(({ expanded, onClick, className }: ExpandIconProps) => {
if (expanded) {
return <IconMinus size="small" className={className} onClick={onClick} />;
} else {
return <IconPlus size="small" className={className} onClick={onClick} />;
}
}, []);
const style = {
width: 260,
height: 200,
border: '1px solid var(--semi-color-border)',
};
return (
<>
<p>expandIcon 是 ReactNode</p>
<Tree expandIcon={<IconChevronDown size="small" className="testCls" />} multiple defaultExpandedKeys={['yazhou']} treeData={treeData} style={style} />
<br />
<p>expandIcon 是函数 </p>
<Tree multiple expandIcon={expandIconFunc} defaultExpandedKeys={['yazhou']} treeData={treeData} style={style} />
</>
);
}连接线
通过 showLine 设置节点之间的连接线,默认为 false,从 2.50.0 开始支持
代码
import { useState, useCallback, useMemo } from 'react';
import { Tree, Switch } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
const [show, setShow] = useState(true);
const onChange = useCallback((value: boolean) => {
setShow(value);
}, []);
const treeData = useMemo(() => {
return [
{
label: 'parent-0',
key: 'parent-0',
children: [
{
label: 'leaf-0-0',
key: 'leaf-0-0',
children: [
{
label: 'leaf-0-0-0',
key: 'leaf-0-0-0',
},
{
label: 'leaf-0-0-1',
key: 'leaf-0-0-1',
},
{
label: 'leaf-0-0-2',
key: 'leaf-0-0-2',
},
],
},
{
label: 'leaf-0-1',
key: 'leaf-0-1',
},
],
},
{
label: 'parent-1',
key: 'parent-1',
},
];
}, []);
const style = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
return (
<>
<div style={{ display: 'flex', alignItems: 'center', columnGap: 5, marginBottom: 5 }}>
<strong id="tree-line-show-line">showLine </strong>
<Switch aria-labelledby="tree-line-show-line" checked={show} onChange={onChange} />
</div>
<Tree showLine={show} defaultExpandAll treeData={treeData} style={style} />
</>
);
}虚拟化
列表虚拟化,用于大量树节点的情况。开启后,动画效果将被关闭。
virtualize 是一个包含下列值的对象:
- height: 高度值,如果为 string 必须有计算高度才能被渲染出来,即其父节点有 offsetHeight
- width: 宽度值,默认 100%
- itemSize: 每行的treeNode的高度,必传
如果带搜索框,建议开启 showFilteredOnly 减少多余节点的渲染。
代码
import { useState } from 'react';
import { Tree, Button, type TreeNodeData } from '@fibrous/semi-ui-react';
const style = {
width: 260,
border: '1px solid var(--semi-color-border)',
};
function generateData(x = 5, y = 4, z = 3, data: TreeNodeData[] = []) {
// x: nodes per level. y: nodes with children per level. z: depth (0 is one level)
function fill(level: number, preKey: string, nodes: TreeNodeData[]) {
const parents: string[] = [];
for (let i = 0; i < x; i++) {
const key = `${preKey}-${i}`;
nodes.push({ label: `${key}-标签`, key: `${key}-key`, value: `${key}-value` });
if (i < y) {
parents.push(key);
}
}
if (level < 0) {
return;
}
parents.forEach((key, index) => {
const children: TreeNodeData[] = [];
nodes[index].children = children;
fill(level - 1, key, children);
});
}
fill(z, '0', data);
let total = 0;
for (let n = z + 1; n >= 0; n--) {
total += x * y ** n;
}
return { gData: data, total };
}
export default function TreeDemo() {
const [gData, setGData] = useState<TreeNodeData[]>([]);
const [total, setTotal] = useState(0);
const onGen = () => {
const data = generateData();
setGData(data.gData);
setTotal(data.total);
};
return (
<div style={{ padding: '0 20px' }}>
<Button onClick={onGen}>生成数据: </Button>
<span>共 {total} 个节点</span>
<br />
<br />
{Boolean(gData.length) && (
<Tree
treeData={gData}
filterTreeNode
showFilteredOnly
style={style}
virtualize={{
// A set Tree height fills the remaining space
height: 300,
itemSize: 28,
}}
/>
)}
</div>
);
}动态更新数据
代码
import { useState } from 'react';
import { Tree, Button } from '@fibrous/semi-ui-react';
const style = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
export default function TreeDemo() {
const [treeData, setTreeData] = useState([
{
key: '0',
label: 'item-0',
value: '0',
},
]);
const add = () => {
const itemLength = Math.floor(Math.random() * 5) + 1;
const nextTreeData = Array.from({ length: itemLength }).map((_v, i) => {
const length = Math.floor(Math.random() * 3);
const children = Array.from({ length }).map((_cv, ci) => {
const child = {
key: `${i}-${ci}`,
label: `Leaf-${i}-${ci}`,
value: `${i}-${ci}`,
};
return child;
});
const item = {
key: `${i}`,
label: `Item-${i}`,
value: `${i}`,
children,
};
return item;
});
setTreeData(nextTreeData);
};
return (
<div style={style}>
<Tree treeData={treeData} />
<br />
<Button onClick={add} style={{ margin: 20 }}>
动态改变数据
</Button>
</div>
);
}异步加载数据
通过设置 loadData 可以动态加载数据,此时需要在数据中传入 isLeaf 标明叶子节点。
代码
import { useState } from 'react';
import { Tree, type TreeNodeData } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
const initialData = [
{
label: 'Expand to load',
value: '0',
key: '0',
},
{
label: 'Expand to load',
value: '1',
key: '1',
},
{
label: 'Leaf Node',
value: '2',
key: '2',
isLeaf: true,
},
];
const [treeData, setTreeData] = useState<TreeNodeData[]>(initialData);
function updateTreeData(list: TreeNodeData[], key: string | undefined, children: TreeNodeData[]): TreeNodeData[] {
return list.map((node) => {
if (node.key === key) {
return { ...node, children };
}
if (node.children) {
return { ...node, children: updateTreeData(node.children, key, children) };
}
return node;
});
}
function onLoadData(treeNode?: TreeNodeData) {
const key = treeNode?.key;
return new Promise<void>((resolve) => {
if (treeNode?.children) {
resolve();
return;
}
setTimeout(() => {
setTreeData((origin) =>
updateTreeData(origin, key, [
{
label: 'Child Node',
key: `${key}-0`,
},
{
label: 'Child Node',
key: `${key}-1`,
},
])
);
resolve();
}, 1000);
});
}
return <Tree loadData={onLoadData} treeData={[...treeData]} />;
}可拖拽的Tree
通过设置 draggable 配合 onDrop 可以实现 Tree 节点的拖拽。
拖拽事件的回调入参如下:
- onDragEnd: function({ event, node: DragTreeNode })
- onDragEnter:function({ event, node: DragTreeNode, expandedKeys: string[] })
- onDragLeave:function({ event, node: DragTreeNode })
- onDragOver:function({ event, node: DragTreeNode })
- onDragStart: function({ event, node: DragTreeNode })
- onDrop:function({ event, node: DragTreeNode, dragNode: DragTreeNode, dragNodesKeys: string[], dropPosition: number, dropToGap: Boolean })
数据类型 DragTreeNode,除了包含 TreeNodeData 所有属性外,还包含 expanded 和 pos 属性,
DragTreeNode {
expanded: Boolean,
pos: string
value?: string | number;
label?: React.ReactNode;
disabled?: boolean;
isLeaf?: boolean;
[key: string]: any;
}
pos指的是当前节点在整个 treeData 中的位置关系,如第0层第1个节点的第2个节点的第0个节点:'0-1-2-0'dropPosition指的是被拖拽节点在当前层级中被 drop 的位置,如插入在同级的第0个节点前则为 -1,在第0个节点后则为 1,落在其上则为 0,以此类推。配合 dropToGap 可以得到更完整的判断。dropToGap指的是被拖拽节点是否被 drop 在节点之间的位置,如果为 false 则是 drop 在某个节点上方
代码
import { useState } from 'react';
import { Tree, type TreeNodeData, type TreeOnDragProps } from '@fibrous/semi-ui-react';
type LoopCallback = (item: TreeNodeData, ind: number, arr: TreeNodeData[]) => void;
export default function TreeDemo() {
const initialData = [
{
label: 'Asia',
value: 'Asia',
key: '0',
children: [
{
label: 'China',
value: 'China',
key: '0-0',
children: [
{
label: 'Beijing',
value: 'Beijing',
key: '0-0-0',
},
{
label: 'Shanghai',
value: 'Shanghai',
key: '0-0-1',
},
],
},
{
label: 'Japan',
value: 'Japan',
key: '0-1',
children: [
{
label: 'Osaka',
value: 'Osaka',
key: '0-1-0',
},
],
},
],
},
{
label: 'North America',
value: 'North America',
key: '1',
children: [
{
label: 'United States',
value: 'United States',
key: '1-0',
},
{
label: 'Canada',
value: 'Canada',
key: '1-1',
},
],
},
{
label: 'Europe',
value: 'Europe',
key: '2',
},
];
const [treeData, setTreeData] = useState<TreeNodeData[]>(initialData);
function onDrop(info: TreeOnDragProps) {
const { dropToGap, node, dragNode } = info;
const dropKey = node.key;
const dragKey = dragNode.key;
const dropPos = node.pos.split('-');
const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
const data = [...treeData];
const loop = (data: TreeNodeData[], key: string | undefined, callback: LoopCallback) => {
data.forEach((item, ind, arr) => {
if (item.key === key) return callback(item, ind, arr);
if (item.children) return loop(item.children, key, callback);
});
};
let dragObj: TreeNodeData = {};
loop(data, dragKey, (item, ind, arr) => {
arr.splice(ind, 1);
dragObj = item;
});
if (!dropToGap) {
loop(data, dropKey, (item) => {
item.children = item.children || [];
item.children.push(dragObj);
});
} else if (dropPosition === 1 && node.children && node.expanded) {
// Expanded with children and dropped in the bottom gap: insert at the top
loop(data, dropKey, (item) => {
item.children = item.children || [];
item.children.unshift(dragObj);
});
} else {
let dropNodeInd = 0;
let dropNodePosArr: TreeNodeData[] = [];
loop(data, dropKey, (_item, ind, arr) => {
dropNodePosArr = arr;
dropNodeInd = ind;
});
if (dropPosition === -1) {
dropNodePosArr.splice(dropNodeInd, 0, dragObj);
} else {
dropNodePosArr.splice(dropNodeInd + 1, 0, dragObj);
}
}
setTreeData(data);
}
return <Tree treeData={treeData} draggable onDrop={onDrop} />;
}高级定制
版本 v>=1.7.0
Tree 组件的 api 支持了大部分的渲染需求,但是如果有非常特殊的定制要求的话,可以使用 renderFullLabel 来接管整行 option 的渲染效果。
renderFullLabel 参数类型如下:
type RenderFullLabelProps = {
/* 节点数据 */
data: BasicTreeNodeData;
/* 层级 */
level: number;
/* 虚拟化情况下,该 style 必须给到 DOM 节点上*/
style: any;
/* 样式类名,包括内置样式,如缩进、展开按钮、过滤器、禁用、选择等。 */
className: string;
/* 展开按钮 */
expandIcon: any;
/* 选中状态 */
checkStatus: {
/* 是否选中 */
checked: boolean;
/* 是否半选 */
halfChecked: boolean;
};
/* 展开状态 */
expandStatus: {
/* 是否展开 */
expanded: boolean;
/* 是否加载中 */
loading: boolean;
};
/* 该节点是否符合筛选条件 */
filtered: boolean | undefined;
/* 当前搜索框输入值 */
searchWord: string | undefined;
/* 点击回调 */
onClick: (e: MouseEvent) => void;
/* 多选点击回调 */
onCheck: (e: MouseEvent) => void;
/* 右键点击回调 */
onContextMenu: (e: MouseEvent) => void;
/* 二次点击回调 */
onDoubleClick: (e: MouseEvent) => void;
/* 展开回调 */
onExpand: (e: MouseEvent) => void;
};
这里给几个常见的高级用法的 demo。
第一个是针对 “希望只有叶子节点可以选中,父节点只起到分组作用” 的场景。
- 你只需要渲染叶子节点前的 Checkbox,并且点击父节点时不触发选中,点击叶子节点触发。
- 同时开启 leafOnly 可以使 onChange 的回调入参都是叶子节点。
注意:renderFullLabel 只接管了渲染效果,并不影响内部的数据逻辑。但是你可以选取需要的逻辑进行渲染,或者配合受控来实现更复杂的需求。
代码
import { Tree, Checkbox, type TreeProps } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
const renderLabel: TreeProps['renderFullLabel'] = ({ className, onExpand, data, onCheck, checkStatus, expandIcon }) => {
const { label } = data;
const isLeaf = !(data.children && data.children.length);
const handleClick = isLeaf ? onCheck : onExpand;
// Focus stays on the tree (arrows expand, Enter/Space check), so the inline checkbox is display-only
return (
<li className={className} role="treeitem" onClick={handleClick}>
{!isLeaf && expandIcon}
{isLeaf && <Checkbox aria-hidden tabIndex={-1} indeterminate={checkStatus.halfChecked} checked={checkStatus.checked} style={{ marginRight: 8 }} />}
<span>{label}</span>
</li>
);
};
const treeData = [
{
label: 'Asia',
value: 'Asia',
key: '0',
children: [
{
label: 'China',
value: 'China',
key: '0-0',
children: [
{
label: 'Beijing',
value: 'Beijing',
key: '0-0-0',
},
{
label: 'Shanghai',
value: 'Shanghai',
key: '0-0-1',
},
],
},
{
label: 'Japan',
value: 'Japan',
key: '0-1',
children: [
{
label: 'Osaka',
value: 'Osaka',
key: '0-1-0',
},
],
},
],
},
{
label: 'North America',
value: 'North America',
key: '1',
children: [
{
label: 'United States',
value: 'United States',
key: '1-0',
},
{
label: 'Canada',
value: 'Canada',
key: '1-1',
},
],
},
];
const treeStyle = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
return <Tree treeData={treeData} renderFullLabel={renderLabel} multiple leafOnly style={treeStyle} />;
}第二个是针对 “希望只有叶子节点可以单选,父节点只起到分组作用” 的场景。
- 你只需要点击父节点时不触发选中,点击叶子节点触发。
代码
import { Tree, type TreeProps } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
const renderLabel: TreeProps['renderFullLabel'] = ({ className, onExpand, onClick, data, expandIcon }) => {
const { label } = data;
const isLeaf = !(data.children && data.children.length);
const handleClick = isLeaf ? onClick : onExpand;
return (
<li className={className} role="treeitem" onClick={handleClick}>
{expandIcon}
<span>{label}</span>
</li>
);
};
const treeData = [
{
label: 'Asia',
value: 'Asia',
key: '0',
children: [
{
label: 'China',
value: 'China',
key: '0-0',
children: [
{
label: 'Beijing',
value: 'Beijing',
key: '0-0-0',
},
{
label: 'Shanghai',
value: 'Shanghai',
key: '0-0-1',
},
],
},
{
label: 'Japan',
value: 'Japan',
key: '0-1',
children: [
{
label: 'Osaka',
value: 'Osaka',
key: '0-1-0',
},
],
},
],
},
{
label: 'North America',
value: 'North America',
key: '1',
children: [
{
label: 'United States',
value: 'United States',
key: '1-0',
},
{
label: 'Canada',
value: 'Canada',
key: '1-1',
},
],
},
];
const treeStyle = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
return <Tree treeData={treeData} renderFullLabel={renderLabel} style={treeStyle} onChange={(...args) => console.log('change', ...args)} />;
}第三个是针对 “单选选中父节点同时也高亮子节点” 的场景。
代码
import { useState } from 'react';
import { IconFixedStroked, IconSectionStroked, IconAbsoluteStroked, IconInnerSectionStroked, IconComponentStroked } from '@fibrous/semi-icons-react';
import { Tree, type TreeNodeData, type TreeProps } from '@fibrous/semi-ui-react';
export default function TreeDemo() {
const [selected, setSelected] = useState(new Set<string | undefined>());
const [selectedThroughParent, setSelectedThroughParent] = useState(new Set<string | undefined>());
const treeData = [
{
label: '黑色固定按钮',
icon: <IconFixedStroked style={{ marginRight: 8, color: 'var(--semi-color-text-2)' }} />,
key: 'fix-btn-0',
},
{
label: '模块',
key: 'module-0',
icon: <IconSectionStroked style={{ marginRight: 8, color: 'var(--semi-color-text-2)' }} />,
children: [
{
label: '可自由摆放的组件',
icon: <IconAbsoluteStroked style={{ marginRight: 8, color: 'var(--semi-color-text-2)' }} />,
key: 'free-compo-0',
},
{
label: '分栏容器',
icon: <IconInnerSectionStroked style={{ marginRight: 8, color: 'var(--semi-color-text-2)' }} />,
key: 'split-col-0',
children: [
{
label: '按钮组件',
icon: <IconComponentStroked style={{ marginRight: 8, color: 'var(--semi-color-text-2)' }} />,
key: 'btn-0',
},
{
label: '按钮组件',
icon: <IconComponentStroked style={{ marginRight: 8, color: 'var(--semi-color-text-2)' }} />,
key: 'btn-1',
},
],
},
],
},
{
label: '模块',
icon: <IconSectionStroked style={{ marginRight: 8, color: 'var(--semi-color-text-2)' }} />,
key: 'module-1',
children: [
{
label: '自定义组件',
icon: <IconComponentStroked style={{ marginRight: 8, color: 'var(--semi-color-text-2)' }} />,
key: 'cus-0',
},
],
},
];
const findDescendantKeys = (node: TreeNodeData) => {
let res = [node.key];
const findChild = (item?: TreeNodeData) => {
if (!item) return;
const { children } = item;
if (children && children.length) {
children.forEach((child) => {
res.push(child.key);
findChild(child);
});
}
};
findChild(node);
return res;
};
const handleSelect: TreeProps['onSelect'] = (key, _bool, node) => {
setSelected(new Set([key]));
const descendantKeys = findDescendantKeys(node);
setSelectedThroughParent(new Set(descendantKeys));
};
const renderLabel: TreeProps['renderFullLabel'] = ({ className, data, onClick, expandIcon }) => {
const { label, icon, key } = data;
const isLeaf = !(data.children && data.children.length);
const style = {
backgroundColor: selected.has(key) ? 'var(--semi-blue-0)' : selectedThroughParent.has(key) ? 'rgb(from var(--semi-blue-0) r g b / .5)' : 'transparent',
};
return (
<li className={className} role="treeitem" onClick={onClick} style={style}>
{isLeaf ? <span style={{ width: 24 }} /> : expandIcon}
{icon}
<span>{label}</span>
</li>
);
};
const treeStyle = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
return <Tree treeData={treeData} renderFullLabel={renderLabel} onSelect={handleSelect} style={treeStyle} defaultExpandAll />;
}可拖拽的高级定制
我们从 1.27.0 版本开始支持可拖拽(draggable)和高级定制(renderFullLabel)同时使用,在该版本之前,并不支持二者同时使用。
代码
import { useState } from 'react';
import { IconFixedStroked, IconSectionStroked, IconAbsoluteStroked, IconComponentStroked } from '@fibrous/semi-icons-react';
import { Tree, type TreeNodeData, type TreeOnDragProps, type TreeProps } from '@fibrous/semi-ui-react';
type LoopCallback = (item: TreeNodeData, ind: number, arr: TreeNodeData[]) => void;
export default function TreeDemo() {
const [selected, setSelected] = useState(new Set<string | undefined>());
const [selectedThroughParent, setSelectedThroughParent] = useState(new Set<string | undefined>());
const defaultTreeData = [
{
label: '黑色固定按钮',
icon: <IconFixedStroked style={{ marginRight: 8, color: 'var(--semi-color-text-2)' }} />,
key: 'fix-btn-0',
},
{
label: '模块',
key: 'module-0',
icon: <IconSectionStroked style={{ marginRight: 8, color: 'var(--semi-color-text-2)' }} />,
children: [
{
label: '可自由摆放的组件',
icon: <IconAbsoluteStroked style={{ marginRight: 8, color: 'var(--semi-color-text-2)' }} />,
key: 'free-compo-0',
},
{
label: '分栏容器',
icon: <IconSectionStroked style={{ marginRight: 8, color: 'var(--semi-color-text-2)' }} />,
key: 'split-col-0',
children: [
{
label: '按钮组件',
icon: <IconComponentStroked style={{ marginRight: 8, color: 'var(--semi-color-text-2)' }} />,
key: 'btn-0',
},
{
label: '按钮组件',
icon: <IconComponentStroked style={{ marginRight: 8, color: 'var(--semi-color-text-2)' }} />,
key: 'btn-1',
},
],
},
],
},
{
label: '模块',
icon: <IconSectionStroked style={{ marginRight: 8, color: 'var(--semi-color-text-2)' }} />,
key: 'module-1',
children: [
{
label: '自定义组件',
icon: <IconComponentStroked style={{ marginRight: 8, color: 'var(--semi-color-text-2)' }} />,
key: 'cus-0',
},
],
},
];
const [treeData, setTreeData] = useState<TreeNodeData[]>(defaultTreeData);
const onDrop = (info: TreeOnDragProps) => {
const { dropToGap, node, dragNode } = info;
const dropKey = node.key;
const dragKey = dragNode.key;
const dropPos = node.pos.split('-');
const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
const data = [...treeData];
const loop = (data: TreeNodeData[], key: string | undefined, callback: LoopCallback) => {
data.forEach((item, ind, arr) => {
if (item.key === key) return callback(item, ind, arr);
if (item.children) return loop(item.children, key, callback);
});
};
let dragObj: TreeNodeData = {};
loop(data, dragKey, (item, ind, arr) => {
arr.splice(ind, 1);
dragObj = item;
});
if (!dropToGap) {
loop(data, dropKey, (item) => {
item.children = item.children || [];
item.children.push(dragObj);
});
} else if (dropPosition === 1 && node.children && node.expanded) {
loop(data, dropKey, (item) => {
item.children = item.children || [];
item.children.unshift(dragObj);
});
} else {
let dropNodeInd = 0;
let dropNodePosArr: TreeNodeData[] = [];
loop(data, dropKey, (_item, ind, arr) => {
dropNodePosArr = arr;
dropNodeInd = ind;
});
if (dropPosition === -1) {
dropNodePosArr.splice(dropNodeInd, 0, dragObj);
} else {
dropNodePosArr.splice(dropNodeInd + 1, 0, dragObj);
}
}
setTreeData(data);
};
const findDescendantKeys = (node: TreeNodeData) => {
const res = [node.key];
const findChild = (item?: TreeNodeData) => {
if (!item) return;
const { children } = item;
if (children && children.length) {
children.forEach((child) => {
res.push(child.key);
findChild(child);
});
}
};
findChild(node);
return res;
};
const handleSelect: TreeProps['onSelect'] = (key, _bool, node) => {
setSelected(new Set([key]));
const descendantKeys = findDescendantKeys(node);
setSelectedThroughParent(new Set(descendantKeys));
};
const renderLabel: TreeProps['renderFullLabel'] = ({ className, data, onClick, expandIcon }) => {
const { label, icon, key } = data;
const isLeaf = !(data.children && data.children.length);
const style = {
backgroundColor: selected.has(key) ? 'var(--semi-blue-0)' : selectedThroughParent.has(key) ? 'rgb(from var(--semi-blue-0) r g b / .5)' : 'transparent',
};
return (
<li className={className} role="treeitem" onClick={onClick} style={style}>
{isLeaf ? <span style={{ width: 24 }} /> : expandIcon}
{icon}
<span>{label}</span>
</li>
);
};
const treeStyle = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)',
};
return <Tree treeData={treeData} draggable onDrop={onDrop} renderFullLabel={renderLabel} onSelect={handleSelect} style={treeStyle} defaultExpandAll />;
}API参考
Tree
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| aria-label | 树的可访问名称,设在 role="tree" 的元素上;空状态下不渲染该元素。 | string | - |
| autoExpandParent | expandedKeys 变化时是否自动展开其中节点的父节点;挂载时总会展开。开启后,展开受控时要先收起所有已展开的子节点才能收起父节点。 | boolean | false |
| autoExpandWhenDragEnter | 拖拽到收起的节点上时是否自动展开该节点。 | boolean | true |
| autoMergeValue | 父节点被选中时,值不再包含它的子孙节点;leafOnly 为 false 时生效。 | boolean | true |
| blockNode | 节点显示为整行,悬浮和选中高亮整行;关闭时只高亮节点的 label。 | boolean | true |
| checkRelation | 多选时节点之间选中状态的关系:related 为父子联动,unRelated 为互不影响。 | CheckRelation | "related" |
| children | 当前实现不渲染 children,树节点请通过 treeData 提供。 | ReactNode | - |
| className | 根元素的类名。 | string | - |
| defaultExpandAll | 初始化时展开所有节点。之后 treeData 或 treeDataSimpleJson 的变化不再影响展开状态,需要时使用 expandAll。 | boolean | false |
| defaultExpandedKeys | 默认展开的节点,显示其直接子级。 | string[] | - |
| defaultValue | 指定默认选中的条目,只在挂载时读取。 | Value | - |
| directory | 目录树模式,节点默认显示文件夹或文件图标;节点数据中的 icon 与 icon 属性可以覆盖它。 | boolean | - |
| disabled | 禁用整棵树。节点数据中的 disabled: false 不会解除禁用。 | boolean | false |
| disableStrictly | 严格禁用:禁用的节点不能通过父级或子级的选中关系改变状态。 | boolean | false |
| draggable | 是否允许拖拽节点,配合 onDrop 等拖拽回调使用。 | boolean | false |
| emptyContent | 没有节点,或开启 showFilteredOnly 后搜索无结果时显示的内容,未设置时取语言包中的空状态文案;为 null 时不渲染空状态。 | ReactNode | - |
| expandAction | 展开逻辑:false 仅点击展开箭头时展开,click 单击节点展开,doubleClick 双击节点展开。键盘不使用该逻辑。 | ExpandAction | false |
| expandAll | 展开所有节点,数据变化后仍然生效。 | boolean | false |
| expandedKeys | (受控)展开的节点。传入后展开状态完全受控,配合 onExpand 使用。 | string[] | - |
| expandIcon | 自定义展开图标。传入元素时,Tree 为它加上展开的点击处理与展开图标的类名;传入函数时,参数为 onClick、className 和 expanded,返回整个图标;其他 ReactNode 原样渲染,不绑定展开点击。 | ExpandIcon | - |
| filterTreeNode | 是否开启搜索。传 true 时按 treeNodeFilterProp 匹配;传函数时自定义匹配,第三个参数为当前节点数据。 | Typeboolean | ((inputValue: string, treeNodeString: string, data?: TreeNodeData) => boolean) | false |
| hideDraggingNode | 拖拽时隐藏被拖拽节点的拖拽图像。 | boolean | - |
| icon | 自定义节点图标。传入元素时所有节点使用同一图标;传入函数时每行调用一次,参数为该行的属性(含节点数据),返回该行的图标。节点数据中的 icon 优先于此属性。 | TypeReactNode | ((props: TreeNodeProps) => ReactNode) | - |
| keyMaps | 自定义节点中 key、label、value 与 children 的字段名。映射了 label 且开启搜索时,需把 treeNodeFilterProp 设为 treeData 的键之一,或自定义 filterTreeNode。 | KeyMapProps | - |
| labelEllipsis | label 超出时是否省略;未设置时跟随 virtualize,开启虚拟化则省略。 | boolean | - |
| leafOnly | 多选时,值和 onChange 的参数只包含叶子节点。 | boolean | false |
| loadData | 异步加载子节点,返回 Promise。叶子节点需在数据中用 isLeaf 标明;设置后未标明 isLeaf 的节点显示展开箭头。 | Type(treeNode?: TreeNodeData) => Promise<void> | - |
| loadedKeys | (受控)已加载的节点,配合 loadData 使用。 | string[] | - |
| motion | 是否开启展开收起的动画;虚拟化时动画关闭。 | boolean | true |
| multiple | 是否多选。多选时所有子项都被选择会自动勾选其父项。 | boolean | false |
| onChange | 选中状态变化时调用,参数为当前选中的值(多选时为数组);onChangeWithObject 为 true 时参数为节点对象。 | Type(value: Value) => void | - |
| onChangeWithObject | 是否把选中节点的其他属性一并给 onChange。为 true 时参数由 value 变为包含 value、label 及其他属性的节点对象;受控的 value 与 defaultValue 也要传对象,且必须含 value。 | boolean | false |
| onContextMenu | 在节点上右键时调用,参数为事件和节点数据。 | Type(e: MouseEvent, node: TreeNodeData) => void | - |
| onDoubleClick | 双击节点时调用,参数为事件和节点数据。 | Type(e: MouseEvent, node: TreeNodeData) => void | - |
| onDragEnd | 拖拽结束时调用,参数为 { event, node }。 | Type(dragProps: DragProps) => void | - |
| onDragEnter | 拖拽进入节点时调用,参数为 { event, node, expandedKeys };拖拽使收起的节点展开时,expandedKeys 为展开后的键。 | Type(dragEnterProps: DragEnterProps) => void | - |
| onDragLeave | 拖拽离开节点时调用,参数为 { event, node }。 | Type(dragProps: DragProps) => void | - |
| onDragOver | 拖拽经过节点时调用,参数为 { event, node }。 | Type(dragProps: DragProps) => void | - |
| onDragStart | 开始拖拽节点时调用,参数为 { event, node }。 | Type(dragProps: DragProps) => void | - |
| onDrop | 节点被放下时调用,参数为 { event, node, dragNode, dragNodesKeys, dropPosition, dropToGap }。 | Type(onDragProps: OnDragProps) => void | - |
| onExpand | 展开或收起节点时调用,参数为展开的节点键数组和 { expanded, node }。 | Type(expandedKeys: string[], expandedOtherProps: ExpandedOtherProps) => void | - |
| onLoad | 节点加载完毕时调用,参数为已加载节点的键集合和该节点数据。 | Type(loadedKeys?: Set<string>, treeNode?: TreeNodeData) => void | - |
| onSearch | 搜索框的值变化时调用,参数为搜索值和因搜索或 value、defaultValue 而展开的节点键数组;展开受控时可用它展开搜索结果。 | Type(searchValue: string, filteredExpandedKeys: string[]) => void | - |
| onSelect | 节点被选中或取消选中时调用,参数为节点键、是否选中和节点数据。 | Type(selectedKey: string, selected: boolean, selectedNode: TreeNodeData) => void | - |
| ref | 指向 TreeHandle 句柄,包含 search 与 scrollTo 方法,见下方 Methods。 | TypeReact.Ref<TreeHandle> | - |
| renderDraggingNode | 拖拽时浏览器显示的拖拽图像。参数为节点的 HTML 元素与节点数据,返回一个 HTMLElement。 | Type(nodeInstance: HTMLElement, node: TreeNodeData) => HTMLElement | - |
| renderFullLabel | 接管整行的渲染,每行调用一次,参数为对象,含 data、level、expandIcon、className、style、checkStatus、expandStatus、onClick、onCheck 等。返回的元素会被 Tree 加上 role="treeitem"、无障碍状态属性与 style;返回文本、数组或 Fragment 时由 li 包裹。className 需由返回的元素自行绑定。 | Type(renderFullLabelProps: RenderFullLabelProps) => ReactNode | - |
| renderLabel | 自定义节点 label 的渲染,参数为 label、节点数据和当前搜索词。设置后不再自动高亮搜索词;搜索仍按 treeData 中的 label 文本匹配。 | Type(label?: ReactNode, treeNode?: TreeNodeData, searchWord?: string) => ReactNode | - |
| searchClassName | 搜索框容器的类名。 | string | - |
| searchPlaceholder | 搜索框的占位文字,未设置时取语言包中的文案。 | string | - |
| searchRender | 自定义搜索框,参数为对象,包含 className、prefix、value、onChange、showClear 和 placeholder,返回搜索框元素。onChange 的参数为输入的字符串。为 false 时隐藏内置搜索框。 | Type((searchRenderProps: SearchRenderProps) => ReactNode) | false | - |
| searchStyle | 搜索框容器的样式。 | React.CSSProperties | - |
| showClear | 搜索框是否显示清除按钮。 | boolean | true |
| showFilteredOnly | 搜索时只展示过滤后的结果。 | boolean | false |
| showLine | 显示节点之间的连接线。 | boolean | false |
| style | 根元素的样式,类型为 CSSProperties。 | React.CSSProperties | - |
| treeData | 节点数据,key 在整棵树内唯一。只有传入新数组才会更新,原地修改数组或节点不会生效。 | TreeNodeData[] | - |
| treeDataSimpleJson | 简单 JSON 形式的节点数据,返回值为包含选中节点的 JSON 数据。 | TreeDataSimpleJson | - |
| treeNodeFilterProp | 搜索时用于匹配输入的节点属性。 | string | "label" |
| value | (受控)当前选中节点的 value(没有 value 时为 key)。传入后即为受控组件,配合 onChange 使用;多选时为数组。 | Value | - |
| virtualize | 列表虚拟化,用于大量节点:height、width、itemSize(必传)。开启后动画关闭。 | Virtualize | - |
Methods
| 方法 | 说明 | 类型 |
|---|---|---|
| scrollTo | 在虚拟化的 Tree 中,把指定的已展开节点滚动到视图内;参数为 { key, align },align 默认 center。 | Type(scrollData: ScrollData) => void |
| search | 手动触发搜索,效果同在搜索框中输入;需要同时设置 filterTreeNode。 | Type(value: string) => void |
aria-label、aria-labelledby、aria-describedby 与 tabIndex 落在 role="tree" 的元素上,空状态下没有该元素,这些属性被丢弃;其余未列出的原生属性(如 id、data-*)落在根元素 .semi-tree-wrapper 上。
TreeNodeData
不同
TreeNodeData的 key 值要求必填且唯一。label允许重复。value 值非必填。此时 onChange, value, defaultValue 及 onChangeWithObject 中所取的 value 属性值将改为 key 值。为了保证行为的符合预期,treeData 中的 value 值或者全部不填写,或者全部填写且唯一,不建议混写。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| value | 属性值 | string|number | - |
| label | 展示的文本 | string|ReactNode | - |
| icon | 自定义图标 | ReactNode | - |
| disabled | 是否禁用,Tree 的 disabled 为 true 时整棵树都禁用,节点的 disabled: false 不会解除 | boolean | false |
| key | required且要求唯一 | string | - |
| isLeaf | 设置节点为叶子节点,在异步加载数据的情况即传入 loadData 时有效 | boolean | - |
Virtualize Object
itemSize必传。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| height | 高度值,如果为 string 必须保证有计算高度,即其父节点有 offsetHeight | number|string | '100%' |
| itemSize | 每行的treeNode的高度,必传 | number | - |
| width | 宽度值 | number|string | '100%' |
Methods
绑定在组件实例上的方法,可以通过 ref 调用实现某些特殊交互。方法说明见上方 Tree 的 API 表格。
import { useRef } from 'react';
import { Tree, type TreeHandle } from '@fibrous/semi-ui-react';
const tree = useRef<TreeHandle>(null);
const search = (value: string) => tree.current?.search(value);
<Tree ref={tree} filterTreeNode treeData={treeData} />;
Accessibility
ARIA
- Tree 支持传入
aria-label、aria-labelledby、aria-describedby来命名和描述该 Tree,它们都设置在role="tree"的元素上; - Tree 会自动为每个节点设置
aria-level、aria-setsize、aria-posinset、aria-disabled,可展开的节点设置aria-expanded;单选时用aria-selected表示选中,多选时用aria-checked表示勾选(半选为mixed); - Tree 会自动为对应部分分别设置
role为tree、treeitem。
renderFullLabel返回的元素同样会被设置role="treeitem"和上述状态;返回文本、数组、Fragment 等不能承载这些属性的内容时,由带这些属性的li包住。
- 节点前的展开箭头和多选复选框只是状态的图示,对读屏软件隐藏,也不接收焦点。
键盘交互
Tree 按 WAI-ARIA Tree View 处理键盘:整棵树是一个 Tab 停靠点,焦点停在树上,aria-activedescendant 指向当前节点,当前节点显示焦点环。
ArrowDown、ArrowUp:移到下一个、上一个可见节点;Home、End:移到第一个、最后一个可见节点ArrowRight:展开收起的节点;节点已展开时移到它的第一个子节点(RTL 下与ArrowLeft互换)ArrowLeft:收起展开的节点;否则移到父节点Enter、Space:选中当前节点,多选时勾选或取消勾选;禁用节点不响应- 输入字符:移到文本以输入内容开头的下一个节点
用鼠标选中、勾选、展开、右键或双击的节点,同时成为键盘的当前节点,之后的方向键从它开始移动。
键盘不触发 expandAction,展开、收起只用方向键。
节点内容里的按钮、链接(如 renderLabel 返回的控件)可以用 Tab 到达,焦点在它们上面时,按键由控件自己处理,树不响应。
示例:
<Tree
/* 其他属性 */
aria-label='example tree'
/>
文案规范
- 尽量使用短语,首字母大写
- 平级之间保持用语形式一致,例如全是地名或者是国家名
设计变量
| Sass 变量 | 值 | 用法 |
|---|---|---|
| $transition_duration-tree_option-bg | var(--semi-transition_duration-none) | 树选项-背景色-动画持续时间 |
| $transition_function-tree_option-bg | var(--semi-transition_function-easeIn) | 树选项-背景色-过渡曲线 |
| $transition_delay-tree_option-bg | var(--semi-transition_delay-none) | 树选项-背景色-延迟时间 |
| $transform_scale-tree-option | var(--semi-transform_scale-none) | 树选项-放大 |
| $color-tree_option-text-default | var(--semi-color-text-0) | 树选项文字颜色 |
| $color-tree_option-icon-default | var(--semi-color-text-2) | 树选项图标颜色 |
| $color-tree_option-bg-hover | var(--semi-color-fill-0) | 树选项背景色 - 悬停 |
| $color-tree_option_selected-bg-default | var(--semi-color-fill-1) | 树选项背景色 - 按下 |
| $color-tree_option-bg-active | var(--semi-color-primary-light-default) | 树选项背景色 - 选中 |
| $color-tree_option_disabled-text-default | var(--semi-color-disabled-text) | 禁用树选项文字颜色 |
| $color-tree_option-icon-hover | var(--semi-color-text-0) | 树选项图标颜色 - 悬浮 |
| $color-tree_option-icon-active | var(--semi-color-black) | 树选项图标颜色 - 按下 |
| $color-tree_option_loading-icon-default | var(--semi-color-primary) | 树选项加载 spin 颜色 |
| $color-tree_option_draggable_insert-border-default | var(--semi-color-primary) | 树选项加载 spin 颜色 |
| $font-tree_option_hightlight-fontWeight | $font-weight-bold | 树选项高亮文本字重 |
| $color-tree_option_hightlight-text | var(--semi-color-primary) | 树选项高亮文本颜色 |
| $radius-tree_checkbox_addon | var(--semi-border-radius-small) | 多选树 checkbox 圆角 |
| $spacing-tree_search_wrapper-paddingX | 12px | 树搜索框水平内边距 |
| $spacing-tree_search_wrapper-paddingY | 8px | 树搜索框垂直内边距 |
| $spacing-tree_option_level-paddingLeft | 20px | 树选项缩进增量 |
| $spacing-tree_option_level1-paddingLeft | 8px | 树选项最上层左侧内边距 |
| $spacing-tree_optionList-paddingX | 0 | 树选项列表水平内边距 |
| $spacing-tree_optionList-paddingY | 8px | 树选项列表垂直内边距 |
| $spacing-tree_option-paddingTop | 4px | 树选项顶部内边距 |
| $spacing-tree_option-paddingBottom | 4px | 树选项底部内边距 |
| $spacing-tree_icon-marginRight | 8px | 树选项图标右侧外边距 |
| $spacing-tree_label_withIcon-marginRight | 8px | 树选项图标右侧外边距 |
| $spacing-tree_option_draggable-paddingY | 2px | 可拖拽的树选项垂直内边距 |
| $spacing-tree_option_draggable-paddingX | 0 | 可拖拽的树选项水平内边距 |
| $width-tree_emptyIcon | $width-icon-small | 树选项空图标宽度 |
| $width-tree_spinIcon | $width-icon-small | 树选项加载 spin 宽度 |
| $width-tree_option_draggable-border | 2px | 可拖拽的树标示线宽度 |
| $width-tree_option_line | 1px | showline展示线宽度 |