导航类

树形控件

树型结构列表。

代码演示

如何引入

import { Tree } from '@fibrous/semi-ui-solid';

基本用法

最简单的用法,默认为单选模式,每一级菜单项均可选择。

  • 亚洲
  • 中国
  • 北京
  • 上海
  • 北美洲
  • 代码
    import { Tree, type TreeNodeData } from '@fibrous/semi-ui-solid';
    
    const treeData: TreeNodeData[] = [
        {
            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: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        return <Tree treeData={treeData} defaultExpandAll style={style} />;
    }

    多选

    设置 multiple,可以进行多选。多选情况下所有子项都被选择时,自动勾选显示其父项。

  • Asia
  • China
  • Beijing
  • Shanghai
  • Chengdu
  • Japan
  • Osaka
  • North America
  • United States
  • Canada
  • 代码
    import { Tree, type TreeNodeData } from '@fibrous/semi-ui-solid';
    
    const treeData: TreeNodeData[] = [
        {
            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: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        return <Tree treeData={treeData} multiple defaultExpandAll style={style} />;
    }

    可搜索的

    通过设置 filterTreeNode 属性可支持搜索功能。默认对 label 值进行搜索,可通过 treeNodeFilterProp 更改。如果只希望展示过滤后的结果,可以设置 showFilteredOnly 。

    showFilteredOnly

    代码
    import { createSignal } from 'solid-js';
    import { Switch, Tree, type TreeNodeData } from '@fibrous/semi-ui-solid';
    
    const treeData: TreeNodeData[] = [
        {
            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: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        const [showFilteredOnly, setShowFilteredOnly] = createSignal(false);
    
        return (
            <>
                <span id="tree-searchable-filtered-only">showFilteredOnly</span>
                <Switch aria-labelledby="tree-searchable-filtered-only" checked={showFilteredOnly()} onChange={(checked) => setShowFilteredOnly(checked)} size="small" />
                <br />
                <Tree treeData={treeData} multiple filterTreeNode showFilteredOnly={showFilteredOnly()} style={style} />
            </>
        );
    }

    设置 filterTreeNode 属性开启搜索后,可以通过设置 searchRender 自定义搜索框的渲染方法,设置为 false 时可以隐藏搜索框。searchRender 的参数是活对象,在返回的 JSX 中读取,不要解构。

    Search
    代码
    import { Input, Tree, type TreeNodeData } from '@fibrous/semi-ui-solid';
    
    const treeData: TreeNodeData[] = [
        {
            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',
                },
            ],
        },
    ];
    
    export default function TreeDemo() {
        // spread keeps the live props; the prefix written after it replaces the icon the tree passes
        return <Tree filterTreeNode searchRender={(searchProps) => <Input {...searchProps} prefix="Search" />} treeData={treeData} />;
    }

    手动触发搜索

    可以通过 ref 获取 Tree 的根元素,调用它的 search 方法进行搜索。注意需要同时设置 filterTreeNode 开启搜索,如果搜索框在 Tree 外部,可以通过设置 searchRender={false} 隐藏 Tree 内部的搜索框。

    简单 JSON 格式的数据

    可以通过 treeDataSimpleJson 传入 JSON 形式的 treeNodes 数据。此时 key-value 键值对中的 key 值将作为 TreeNodeData 的 key 和 label,value 值将作为 TreeNodeData 的 value。返回值为包含选中节点的 JSON 数据。

  • Node2
  • 代码
    import { Tree, type TreeDataSimpleJson } from '@fibrous/semi-ui-solid';
    
    const json: TreeDataSimpleJson = {
        Node1: {
            'Child Node1': '0-0-1',
            'Child Node2': '0-0-2',
        },
        Node2: '0-1',
    };
    
    const style = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        return <Tree treeDataSimpleJson={json} multiple onChange={(e) => console.log('当前所有选中项: ', e)} onSelect={(e) => console.log('当前选项: ', e)} style={style} />;
    }

    行显示节点

    可以通过设置 blockNode 使节点显示为整行,此时悬浮选中高亮状态都会显示整行。默认打开。关闭时只高亮节点 label。

  • Asia
  • China
  • Beijing
  • Shanghai

  • Asia
  • China
  • Beijing
  • Shanghai
  • 代码
    import { Tree, type TreeNodeData } from '@fibrous/semi-ui-solid';
    
    const treeData: TreeNodeData[] = [
        {
            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',
                },
            ],
        },
    ];
    
    export default function TreeDemo() {
        return (
            <div>
                <Tree treeData={treeData} defaultValue="Shanghai" blockNode={false} />
                <br />
                <Tree treeData={treeData} defaultValue="Shanghai" multiple blockNode={false} />
            </div>
        );
    }

    自定义节点内容

    TreeNodeData 的 label 只接受字符串或数字,自定义节点内容使用 renderLabel。renderLabel 接收 label、节点数据和搜索词,返回节点内容,任一参数变化时重新调用。搜索仍按 treeData 中的 label 文本匹配。

    节点内容最好只用于展示。节点里的按钮、链接虽然可以用 Tab 到达(见 Accessibility),但读屏软件会把它们当作节点名称的一部分,方向键浏览时也不会经过它们。需要对节点执行的操作请放在树外,例如 onContextMenu 打开的菜单或树旁的工具栏。

    代码
    import { Tag, Tree, type TreeNodeData } from '@fibrous/semi-ui-solid';
    
    const rowStyle = {
        display: 'flex',
        'justify-content': 'space-between',
        'align-items': 'center',
    };
    
    const treeStyle = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        const treeData: TreeNodeData[] = [
            {
                label: '亚洲',
                value: 'yazhou',
                key: 'yazhou',
                children: [
                    {
                        label: '中国',
                        value: 'zhongguo',
                        key: 'zhongguo',
                    },
                    {
                        label: '日本',
                        value: 'riben',
                        key: 'riben',
                    },
                ],
            },
        ];
    
        // Node content is display-only: focus stays on the tree, so buttons and links inside are keyboard-unreachable
        return (
            <Tree
                treeData={treeData}
                style={treeStyle}
                renderLabel={(label, node) => (
                    <div style={rowStyle}>
                        <span>{label}</span>
                        <Tag size="small">{node?.children ? '大洲' : '国家'}</Tag>
                    </div>
                )}
            />
        );
    }

    过长省略。可以在 renderLabel 中返回设置了 ellipsis 属性的 Typography.Text,实现文本过长省略的效果。

    代码
    import { Tree, Typography, type TreeNodeData } from '@fibrous/semi-ui-solid';
    
    const treeDataWithNode: TreeNodeData[] = [
        {
            label: '亚洲亚洲亚洲亚洲亚洲亚洲亚洲亚洲亚洲亚洲亚洲亚洲',
            value: 'yazhou',
            key: 'yazhou',
            children: [
                {
                    label: '中国中国中国中国中国中国中国中国中国中国中国中国',
                    value: 'zhongguo',
                    key: 'zhongguo',
                },
                {
                    label: '日本日本日本日本日本日本日本日本日本日本日本日本',
                    value: 'riben',
                    key: 'riben',
                },
            ],
        },
    ];
    
    const treeStyle = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    const renderLabel = (label?: string | number) => (
        <Typography.Text ellipsis={{ showTooltip: true }} style={{ width: '100%' }}>
            {label}
        </Typography.Text>
    );
    
    export default function TreeDemo() {
        return <Tree treeData={treeDataWithNode} renderLabel={renderLabel} style={treeStyle} />;
    }

    自定义图标

    通过设置 icon 属性可添加自定义图标。可以传入元素,也可以传入函数:函数每行调用一次,参数为该行的属性(含节点数据),返回该行的图标,按节点数据区分图标时用函数形式。TreeNodeData 没有 icon 字段。

    代码
    import { IconMapPin } from '@fibrous/semi-icons-solid';
    import { Tree, type TreeNodeData } from '@fibrous/semi-ui-solid';
    
    const style = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        const treeData: TreeNodeData[] = [
            {
                label: 'Asia',
                value: 'Asia',
                key: '0',
                children: [
                    {
                        label: 'China',
                        value: 'China',
                        key: '0-0',
                    },
                    {
                        label: 'Japan',
                        value: 'Japan',
                        key: '0-1',
                    },
                ],
            },
        ];
        return <Tree treeData={treeData} style={style} icon={<IconMapPin style={{ color: 'var(--semi-color-text-2)' }} />} />;
    }

    目录树模式

    通过设置 directory 属性可显示为目录树模式。目录树模式下自带目录图标,可以通过自定义图标覆盖。

    代码
    import { Tree, type TreeNodeData } from '@fibrous/semi-ui-solid';
    
    const treeData: TreeNodeData[] = [
        {
            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: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        return <Tree treeData={treeData} directory style={style} />;
    }

    禁用

    可以使用 disableStrictly 来开启严格禁用。开启严格禁用后,当节点是 disabled 的时候,则不能通过子级或者父级的关系改变选中状态。

  • Asia
  • China
  • Beijing
  • Shanghai
  • 代码
    import { Tree, type TreeNodeData } from '@fibrous/semi-ui-solid';
    
    const treeData: TreeNodeData[] = [
        {
            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: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        return <Tree treeData={treeData} defaultValue="Shanghai" multiple disableStrictly style={style} />;
    }

    节点选中关系

    版本:>= 2.5.0

    多选时,可以使用 checkRelation 来设置节点选中关系的类型,可选:'related'(默认)、'unRelated'。当选中关系为 'unRelated',意味着节点之间的选中互不影响。

  • Asia
  • China
  • Beijing
  • Shanghai
  • Chengdu
  • Japan
  • Osaka
  • North America
  • United States
  • Canada
  • 代码
    import { Tree, type TreeNodeData } from '@fibrous/semi-ui-solid';
    
    const treeData: TreeNodeData[] = [
        {
            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: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        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 仍然生效。

    defaultExpandAll
  • Node1
  • Child Node1
  • Child Node2
  • Node2
  • expandAll
  • Node1
  • Child Node1
  • Child Node2
  • Node2
  • 代码
    import { createSignal } from 'solid-js';
    import { Button, Tree, type TreeDataSimpleJson } from '@fibrous/semi-ui-solid';
    
    const json: TreeDataSimpleJson = {
        Node1: {
            'Child Node1': '0-0-1',
            'Child Node2': '0-0-2',
        },
        Node2: '0-1',
    };
    
    const json2: TreeDataSimpleJson = {
        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 = {
        'margin-right': '20px',
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        const [tree, setTree] = createSignal<TreeDataSimpleJson>(json);
        const handleClick = () => {
            setTree(json2);
        };
        return (
            <>
                <Button onClick={handleClick} style={{ 'margin-bottom': '10px' }}>
                    点击更新 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 来实现展开受控时的搜索展开效果。

    受控

    传入 value 时即为受控组件,可以配合 onChange 使用。

  • 亚洲
  • 中国
  • 北京
  • 上海
  • 北美洲
  • 代码
    import { createSignal } from 'solid-js';
    import { Tree, type TreeNodeData } from '@fibrous/semi-ui-solid';
    
    const treeData: TreeNodeData[] = [
        {
            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: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        const [value, setValue] = createSignal('Shanghai');
    
        return (
            <Tree
                treeData={treeData}
                value={value()}
                onChange={(next) => {
                    if (typeof next === 'string') {
                        setValue(next);
                    }
                }}
                style={style}
            />
        );
    }

    自动展开父节点

    在展开受控的情况下,当开启了 autoExpandParent ,如果想要收起父元素,则需要把它的所有子元素均收起后才可以。默认情况下,autoExpandParent 为 false,即:父元素收起不受到子元素的影响。

    需要先将“中国”节点收起后,才能够收起“亚洲”节点

  • 亚洲
  • 中国
  • 北京
  • 上海
  • 北美洲
  • 代码
    import { createSignal } from 'solid-js';
    import { Tree, type TreeNodeData } from '@fibrous/semi-ui-solid';
    
    const treeData: TreeNodeData[] = [
        {
            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: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        const [expandedKeys, setExpandedKeys] = createSignal<string[]>(['0', '0-0']);
    
        return (
            <>
                <div>需要先将“中国”节点收起后,才能够收起“亚洲”节点</div>
                <br />
                <Tree autoExpandParent treeData={treeData} onExpand={(keys) => setExpandedKeys(keys)} expandedKeys={expandedKeys()} style={style} />
            </>
        );
    }

    自定义展开 Icon

    通过 expandIcon 自定义展开 Icon,支持传入元素或函数。传入元素时,该元素自动获得展开图标的 class 与展开的点击处理;传入函数时,函数参数为 onClick、class 和 expanded,由函数返回整个图标。expanded 是活的属性,在 JSX 中读取,不要解构。

    expandIcon: JSX.Element | ((props: { onClick: (e: MouseEvent) => void; class: string; expanded: boolean }) => JSX.Element);

    示例如下:

    expandIcon 是元素

  • 亚洲
  • 日本
  • 北美洲

  • expandIcon 是函数

  • 亚洲
  • 日本
  • 北美洲
  • 代码
    import { Show } from 'solid-js';
    import { IconChevronDown, IconMinus, IconPlus } from '@fibrous/semi-icons-solid';
    import { Tree, type TreeExpandIconProps, type TreeNodeData } from '@fibrous/semi-ui-solid';
    
    const treeData: TreeNodeData[] = [
        {
            label: '亚洲',
            key: 'yazhou',
            children: [
                {
                    label: '中国',
                    key: 'zhongguo',
                    children: [
                        {
                            label: '北京',
                            key: 'beijing',
                        },
                        {
                            label: '上海',
                            key: 'shanghai',
                        },
                    ],
                },
                {
                    label: '日本',
                    key: 'riben',
                },
            ],
        },
        {
            label: '北美洲',
            key: 'beimeizhou',
        },
    ];
    
    // The tree adds the class and the click to an element, but a function renders the icon itself, so the icon reads
    // `expanded` inside Show: a plain `if` would keep the first value
    const expandIconFunc = (iconProps: TreeExpandIconProps) => (
        <Show when={iconProps.expanded} fallback={<IconPlus size="small" class={iconProps.class} onClick={iconProps.onClick} />}>
            <IconMinus size="small" class={iconProps.class} onClick={iconProps.onClick} />
        </Show>
    );
    
    const style = {
        width: '260px',
        height: '200px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        return (
            <>
                <p>expandIcon 是元素</p>
                <Tree expandIcon={<IconChevronDown size="small" class="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 开始支持

    showLine
  • parent-0
  • leaf-0-0
  • leaf-0-0-0
  • leaf-0-0-1
  • leaf-0-0-2
  • leaf-0-1
  • parent-1
  • 代码
    import { createSignal } from 'solid-js';
    import { Switch, Tree, type TreeNodeData } from '@fibrous/semi-ui-solid';
    
    const treeData: TreeNodeData[] = [
        {
            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: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        const [show, setShow] = createSignal(true);
    
        return (
            <>
                <div style={{ display: 'flex', 'align-items': 'center', 'column-gap': '5px', 'margin-bottom': '5px' }}>
                    <strong id="tree-line-show-line">showLine </strong>
                    <Switch aria-labelledby="tree-line-show-line" checked={show()} onChange={(checked) => setShow(checked)} />
                </div>
                <Tree showLine={show()} defaultExpandAll treeData={treeData} style={style} />
            </>
        );
    }

    虚拟化

    列表虚拟化,用于大量树节点的情况。开启后,动画效果将被关闭。

    virtualize 是一个包含下列值的对象:

    • height: 高度值,如果为 string 必须有计算高度才能被渲染出来,即其父节点有 offsetHeight
    • width: 宽度值,默认 100%
    • itemSize: 每行的treeNode的高度,必传

    如果带搜索框,建议开启 showFilteredOnly 减少多余节点的渲染。

    共 0 个节点

    代码
    import { createSignal, Show } from 'solid-js';
    import { Button, Tree, type TreeNodeData } from '@fibrous/semi-ui-solid';
    
    const style = {
        width: '260px',
        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] = createSignal<TreeNodeData[]>([]);
        const [total, setTotal] = createSignal(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 />
                <Show when={gData().length}>
                    <Tree
                        treeData={gData()}
                        filterTreeNode
                        showFilteredOnly
                        style={style}
                        virtualize={{
                            // A set Tree height fills the remaining space
                            height: 300,
                            itemSize: 28,
                        }}
                    />
                </Show>
            </div>
        );
    }

    动态更新数据

    treeData 只支持整体替换:传入新数组才会更新,原地修改数组或节点不会更新树。

  • item-0

  • 代码
    import { createSignal } from 'solid-js';
    import { Button, Tree, type TreeNodeData } from '@fibrous/semi-ui-solid';
    
    const style = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        const [treeData, setTreeData] = createSignal<TreeNodeData[]>([
            {
                key: '0',
                label: 'item-0',
                value: '0',
            },
        ]);
    
        const add = () => {
            const itemLength = Math.floor(Math.random() * 5) + 1;
            const nextTreeData = Array.from({ length: itemLength }).map((_, i) => {
                const length = Math.floor(Math.random() * 3);
                const children = Array.from({ length }).map((_child, 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: '20px' }}>
                    动态改变数据
                </Button>
            </div>
        );
    }

    异步加载数据

    通过设置 loadData 可以动态加载数据,此时需要在数据中传入 isLeaf 标明叶子节点。

  • Leaf Node
  • 代码
    import { createSignal } from 'solid-js';
    import { Tree, type TreeNodeData } from '@fibrous/semi-ui-solid';
    
    const initialData: TreeNodeData[] = [
        {
            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,
        },
    ];
    
    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;
        });
    }
    
    export default function TreeDemo() {
        const [treeData, setTreeData] = createSignal<TreeNodeData[]>(initialData);
    
        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: ({ event, node: TreeDragNode }) => void
    - onDragEnter: ({ event, node: TreeDragNode, expandedKeys?: string[] }) => void
    - onDragLeave: ({ event, node: TreeDragNode }) => void
    - onDragOver: ({ event, node: TreeDragNode }) => void
    - onDragStart: ({ event, node: TreeDragNode }) => void
    - onDrop: ({ event, node: TreeDragNode, dragNode: TreeDragNode, dragNodesKeys: string[], dropPosition: number, dropToGap: boolean }) => void

    数据类型 TreeDragNode,除了包含 TreeNodeData 所有属性外,还包含 expanded 和 pos 属性:

    interface TreeDragNode extends TreeNodeData {
        expanded: boolean;
        pos: string;
    }
    • pos 指的是当前节点在整个 treeData 中的位置关系,如第0层第1个节点的第2个节点的第0个节点:'0-1-2-0'
    • dropPosition 指的是被拖拽节点在当前层级中被 drop 的位置,如插入在同级的第0个节点前则为 -1,在第0个节点后则为 1,落在其上则为 0,以此类推。配合 dropToGap 可以得到更完整的判断。
    • dropToGap 指的是被拖拽节点是否被 drop 在节点之间的位置,如果为 false 则是 drop 在某个节点上方
  • Europe
  • 代码
    import { createSignal } from 'solid-js';
    import { Tree, type TreeNodeData, type TreeOnDragProps } from '@fibrous/semi-ui-solid';
    
    type LoopCallback = (item: TreeNodeData, ind: number, arr: TreeNodeData[]) => void;
    
    export default function TreeDemo() {
        const initialData: TreeNodeData[] = [
            {
                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] = createSignal<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 来接管整行的渲染。每行调用一次,参数是活对象:字段在返回的 JSX 中读取,不要解构。

    renderFullLabel 参数类型如下:

    type RenderFullLabelProps = {
        /* 节点数据 */
        data: TreeNodeData;
        /* 层级 */
        level: number;
        /* 样式类名,包括内置样式,如缩进、选中、焦点、拖拽状态等 */
        class: string;
        /* 创建内置的展开按钮,每次调用都会创建新元素,在要放置按钮的位置调用 */
        expandIcon: () => JSX.Element;
        /* 选中状态 */
        checkStatus: {
            /* 是否选中 */
            checked: boolean;
            /* 是否半选 */
            halfChecked: boolean;
        };
        /* 展开状态 */
        expandStatus: {
            /* 是否展开 */
            expanded: boolean;
            /* 是否加载中 */
            loading: boolean;
        };
        /* 该节点是否符合筛选条件 */
        filtered: boolean;
        /* 当前搜索框输入值 */
        searchWord: string;
        /* 点击整行的回调:单选时选中,多选时勾选 */
        onClick: (e: MouseEvent) => void;
        /* 多选点击回调 */
        onCheck: (e: MouseEvent) => void;
        /* 展开回调 */
        onExpand: (e: MouseEvent) => void;
        /* 右键点击回调 */
        onContextMenu: (e: MouseEvent) => void;
        /* 二次点击回调 */
        onDoubleClick: (e: MouseEvent) => void;
    };

    Tree 自己提供带 id、role="treeitem" 和行状态的行元素(li),返回的内容放在它里面,虚拟化时的定位样式也在这个元素上。把参数中的 class 绑到内容元素上,该元素就获得与内置行相同的缩进、选中、焦点等样式和行内边距。

    <Tree treeData={treeData} renderFullLabel={(row) => <div class={row.class}>{row.data.label}</div>} />

    这里给几个常见的高级用法的 demo。

    第一个是针对 “希望只有叶子节点可以选中,父节点只起到分组作用” 的场景。

    • 你只需要渲染叶子节点前的 Checkbox,并且点击父节点时不触发选中,点击叶子节点触发。
    • 同时开启 leafOnly 可以使 onChange 的回调入参都是叶子节点。注意:renderFullLabel 只接管了渲染效果,并不影响内部的数据逻辑。但是你可以选取需要的逻辑进行渲染,或者配合受控来实现更复杂的需求。
    代码
    import { Show } from 'solid-js';
    import { Checkbox, Tree, type TreeNodeData, type TreeRenderFullLabelProps } from '@fibrous/semi-ui-solid';
    
    const isLeaf = (row: TreeRenderFullLabelProps) => !(row.data.children && row.data.children.length);
    
    const renderLabel = (row: TreeRenderFullLabelProps) => (
        // Focus stays on the tree (arrows expand, Enter/Space check), so the inline checkbox is display-only
        <div class={row.class} onClick={(e) => (isLeaf(row) ? row.onCheck(e) : row.onExpand(e))}>
            <Show when={!isLeaf(row)}>{row.expandIcon()}</Show>
            <Show when={isLeaf(row)}>
                <span aria-hidden="true" style={{ 'margin-right': '8px' }}>
                    <Checkbox tabIndex={-1} indeterminate={row.checkStatus.halfChecked} checked={row.checkStatus.checked} />
                </span>
            </Show>
            <span>{row.data.label}</span>
        </div>
    );
    
    const treeData: TreeNodeData[] = [
        {
            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: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        return <Tree treeData={treeData} renderFullLabel={renderLabel} multiple leafOnly style={treeStyle} />;
    }

    第二个是针对 “希望只有叶子节点可以单选,父节点只起到分组作用” 的场景。

    • 你只需要点击父节点时不触发选中,点击叶子节点触发。
    代码
    import { Tree, type TreeNodeData, type TreeRenderFullLabelProps } from '@fibrous/semi-ui-solid';
    
    const isLeaf = (row: TreeRenderFullLabelProps) => !(row.data.children && row.data.children.length);
    
    const renderLabel = (row: TreeRenderFullLabelProps) => (
        <div class={row.class} onClick={(e) => (isLeaf(row) ? row.onClick(e) : row.onExpand(e))}>
            {row.expandIcon()}
            <span>{row.data.label}</span>
        </div>
    );
    
    const treeData: TreeNodeData[] = [
        {
            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: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        return <Tree treeData={treeData} renderFullLabel={renderLabel} style={treeStyle} onChange={(...args) => console.log('change', ...args)} />;
    }

    第三个是针对 “单选选中父节点同时也高亮子节点” 的场景。

  • 黑色固定按钮
  • 模块
  • 可自由摆放的组件
  • 分栏容器
  • 按钮组件
  • 按钮组件
  • 模块
  • 自定义组件
  • 代码
    import { createSignal } from 'solid-js';
    import { Dynamic } from 'solid-js/web';
    import { IconAbsoluteStroked, IconComponentStroked, IconFixedStroked, IconInnerSectionStroked, IconSectionStroked } from '@fibrous/semi-icons-solid';
    import { Tree, type TreeNodeData, type TreeRenderFullLabelProps } from '@fibrous/semi-ui-solid';
    
    const icons: Record<string, typeof IconFixedStroked> = {
        'fix-btn-0': IconFixedStroked,
        'module-0': IconSectionStroked,
        'free-compo-0': IconAbsoluteStroked,
        'split-col-0': IconInnerSectionStroked,
        'btn-0': IconComponentStroked,
        'btn-1': IconComponentStroked,
        'module-1': IconSectionStroked,
        'cus-0': IconComponentStroked,
    };
    
    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 treeStyle = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        const [selected, setSelected] = createSignal(new Set<string | undefined>());
        const [selectedThroughParent, setSelectedThroughParent] = createSignal(new Set<string | undefined>());
        const iconStyle = { 'margin-right': '8px', color: 'var(--semi-color-text-2)' };
        const treeData: TreeNodeData[] = [
            {
                label: '黑色固定按钮',
                key: 'fix-btn-0',
            },
            {
                label: '模块',
                key: 'module-0',
                children: [
                    {
                        label: '可自由摆放的组件',
                        key: 'free-compo-0',
                    },
                    {
                        label: '分栏容器',
                        key: 'split-col-0',
                        children: [
                            {
                                label: '按钮组件',
                                key: 'btn-0',
                            },
                            {
                                label: '按钮组件',
                                key: 'btn-1',
                            },
                        ],
                    },
                ],
            },
            {
                label: '模块',
                key: 'module-1',
                children: [
                    {
                        label: '自定义组件',
                        key: 'cus-0',
                    },
                ],
            },
        ];
    
        const handleSelect = (key: string, _bool: boolean, node: TreeNodeData) => {
            setSelected(new Set([key]));
            const descendantKeys = findDescendantKeys(node);
            setSelectedThroughParent(new Set(descendantKeys));
        };
    
        const backgroundOf = (key?: string) => (selected().has(key) ? 'var(--semi-blue-0)' : selectedThroughParent().has(key) ? 'rgb(from var(--semi-blue-0) r g b / .5)' : 'transparent');
    
        const renderLabel = (row: TreeRenderFullLabelProps) => (
            <div class={row.class} onClick={row.onClick} style={{ 'background-color': backgroundOf(row.data.key) }}>
                {row.data.children && row.data.children.length ? row.expandIcon() : <span style={{ width: '24px' }} />}
                <Dynamic component={icons[row.data.key ?? '']} style={iconStyle} />
                <span>{row.data.label}</span>
            </div>
        );
    
        return <Tree treeData={treeData} renderFullLabel={renderLabel} onSelect={handleSelect} style={treeStyle} defaultExpandAll />;
    }

    可拖拽的高级定制

    可拖拽(draggable)和高级定制(renderFullLabel)可以同时使用。

  • 黑色固定按钮
  • 模块
  • 可自由摆放的组件
  • 分栏容器
  • 按钮组件
  • 按钮组件
  • 模块
  • 自定义组件
  • 代码
    import { createSignal } from 'solid-js';
    import { Dynamic } from 'solid-js/web';
    import { IconAbsoluteStroked, IconComponentStroked, IconFixedStroked, IconSectionStroked } from '@fibrous/semi-icons-solid';
    import { Tree, type TreeNodeData, type TreeOnDragProps, type TreeRenderFullLabelProps } from '@fibrous/semi-ui-solid';
    
    const icons: Record<string, typeof IconFixedStroked> = {
        'fix-btn-0': IconFixedStroked,
        'module-0': IconSectionStroked,
        'free-compo-0': IconAbsoluteStroked,
        'split-col-0': IconSectionStroked,
        'btn-0': IconComponentStroked,
        'btn-1': IconComponentStroked,
        'module-1': IconSectionStroked,
        'cus-0': IconComponentStroked,
    };
    
    type LoopCallback = (item: TreeNodeData, ind: number, arr: TreeNodeData[]) => void;
    
    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 treeStyle = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    
    export default function TreeDemo() {
        const [selected, setSelected] = createSignal(new Set<string | undefined>());
        const [selectedThroughParent, setSelectedThroughParent] = createSignal(new Set<string | undefined>());
        const iconStyle = { 'margin-right': '8px', color: 'var(--semi-color-text-2)' };
        const defaultTreeData: TreeNodeData[] = [
            {
                label: '黑色固定按钮',
                key: 'fix-btn-0',
            },
            {
                label: '模块',
                key: 'module-0',
                children: [
                    {
                        label: '可自由摆放的组件',
                        key: 'free-compo-0',
                    },
                    {
                        label: '分栏容器',
                        key: 'split-col-0',
                        children: [
                            {
                                label: '按钮组件',
                                key: 'btn-0',
                            },
                            {
                                label: '按钮组件',
                                key: 'btn-1',
                            },
                        ],
                    },
                ],
            },
            {
                label: '模块',
                key: 'module-1',
                children: [
                    {
                        label: '自定义组件',
                        key: 'cus-0',
                    },
                ],
            },
        ];
        const [treeData, setTreeData] = createSignal<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 handleSelect = (key: string, _bool: boolean, node: TreeNodeData) => {
            setSelected(new Set([key]));
            const descendantKeys = findDescendantKeys(node);
            setSelectedThroughParent(new Set(descendantKeys));
        };
    
        const backgroundOf = (key?: string) => (selected().has(key) ? 'var(--semi-blue-0)' : selectedThroughParent().has(key) ? 'rgb(from var(--semi-blue-0) r g b / .5)' : 'transparent');
    
        const renderLabel = (row: TreeRenderFullLabelProps) => (
            <div class={row.class} onClick={row.onClick} style={{ 'background-color': backgroundOf(row.data.key) }}>
                {row.data.children && row.data.children.length ? row.expandIcon() : <span style={{ width: '24px' }} />}
                <Dynamic component={icons[row.data.key ?? '']} style={iconStyle} />
                <span>{row.data.label}</span>
            </div>
        );
    
        return <Tree treeData={treeData()} draggable onDrop={onDrop} renderFullLabel={renderLabel} onSelect={handleSelect} style={treeStyle} defaultExpandAll />;
    }

    API参考

    Tree

    Props

    属性说明类型默认值
    autoExpandParentexpandedKeys 变化时是否自动展开其中节点的父节点;挂载时总会展开。开启后,展开受控时要先收起所有已展开的子节点才能收起父节点。booleanfalse
    autoExpandWhenDragEnter拖拽到收起的节点上时是否自动展开该节点。booleantrue
    autoMergeValue父节点被选中时,值不再包含它的子孙节点;leafOnly 为 false 时生效。booleantrue
    blockNode节点显示为整行,悬浮和选中高亮整行;关闭时只高亮节点的 label。booleantrue
    checkRelation多选时节点之间选中状态的关系:related 为父子联动,unRelated 为互不影响。CheckRelation"related"
    defaultExpandAll初始化时展开所有节点。之后 treeData 或 treeDataSimpleJson 的变化不再影响展开状态,需要时使用 expandAll。booleanfalse
    defaultExpandedKeys默认展开的节点,显示其直接子级。string[]-
    defaultValue指定默认选中的条目,只在挂载时读取。Value-
    directory目录树模式,自带目录图标,可用 icon 覆盖。boolean-
    disabled禁用整棵树。节点数据中的 disabled: false 不会解除禁用。booleanfalse
    disableStrictly严格禁用:禁用的节点不能通过父级或子级的选中关系改变状态。booleanfalse
    draggable是否允许拖拽节点,配合 onDrop 等拖拽回调使用。不能与 virtualize 同时使用。booleanfalse
    emptyContent没有节点,或开启 showFilteredOnly 后搜索无结果时显示的内容;为 null 时不渲染空状态。JSX.Element-
    expandAction展开逻辑:false 仅点击展开箭头时展开,click 单击节点展开,doubleClick 双击节点展开。键盘不触发该逻辑。ExpandActionfalse
    expandAll展开所有节点,数据变化后仍然生效。booleanfalse
    expandedKeys(受控)展开的节点。传入后展开状态完全受控,配合 onExpand 使用。string[]-
    expandIcon自定义展开图标。传入元素时,该元素获得展开图标的 class 与展开的点击处理;传入函数时,参数为 onClick、class 和 expanded,由函数返回整个图标。ExpandIcon-
    filterTreeNode是否开启搜索。传 true 时按 treeNodeFilterProp 匹配;传函数时自定义匹配,第三个参数为当前节点数据。Typeboolean | ((inputValue: string, treeNodeString: string, data?: TreeNodeData) => boolean)false
    hideDraggingNode拖拽时隐藏被拖拽节点的拖拽图像。boolean-
    icon自定义节点图标。传入元素时所有节点使用同一图标;传入函数时每行调用一次,参数为该行的属性(含节点数据),返回该行的图标。TypeJSX.Element | ((row: TreeRowProps) => JSX.Element)-
    keyMaps自定义节点中 key、label、value 的字段名。映射了 label 且开启搜索时,需把 treeNodeFilterProp 设为 treeData 的键之一,或自定义 filterTreeNode。KeyMapProps-
    labelEllipsislabel 超出时是否省略;虚拟化时默认开启。boolean-
    leafOnly多选时,值和 onChange 的参数只包含叶子节点。booleanfalse
    loadData异步加载子节点,返回 Promise。需要在数据中用 isLeaf 标明叶子节点;设置后未标明 isLeaf 的节点显示展开箭头。Type(treeNode?: TreeNodeData) => Promise<void>-
    loadedKeys(受控)已加载的节点,配合 loadData 使用。string[]-
    motion是否开启展开收起的动画;虚拟化时动画关闭。booleantrue
    multiple是否多选。多选时所有子项都被选择会自动勾选其父项。booleanfalse
    onChange选中状态变化时调用,参数为当前选中的值;onChangeWithObject 为 true 时参数为节点对象。Type(value: Value) => void-
    onChangeWithObject是否把选中节点的其他属性一并给 onChange。为 true 时参数由 value 变为包含 value、label 及其他属性的节点对象;受控的 value 与 defaultValue 也要传对象,且必须含 value。booleanfalse
    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根元素,带有 search 与 scrollTo 方法。TypeRef<TreeElement>-
    renderDraggingNode拖拽时浏览器显示的拖拽图像。参数为节点元素和节点数据,返回一个元素。Type(nodeInstance: HTMLElement, node: TreeNodeData) => HTMLElement-
    renderFullLabel接管整行的渲染。每行调用一次,参数为活对象:字段在返回的 JSX 中读取,不要解构。class 应绑到内容元素上;expandIcon 是创建展开按钮的渲染函数,在要放置按钮的位置调用;带 role 与状态的行元素(li)由 Tree 提供。Type(renderFullLabelProps: RenderFullLabelProps) => JSX.Element-
    renderLabel自定义节点 label 的渲染,参数为 label、节点数据和当前搜索词,其中任一变化时重新调用。搜索仍按 treeData 中的 label 文本匹配。Type(label?: string | number, treeNode?: TreeNodeData, searchWord?: string) => JSX.Element-
    searchClassName搜索框容器的类名。string-
    searchPlaceholder搜索框的占位文字。string-
    searchRender自定义搜索框,参数为活对象,包含 class、prefix、value、onChange、showClear 和 placeholder,返回搜索框元素。为 false 时隐藏内置搜索框。Type((searchRenderProps: SearchRenderProps) => JSX.Element) | false-
    searchStyle搜索框容器的样式。JSX.CSSProperties | string-
    showClear搜索框是否显示清除按钮。booleantrue
    showFilteredOnly搜索时只展示过滤后的结果。booleanfalse
    showLine显示节点之间的连接线。booleanfalse
    style根元素的样式,接受对象(键用 kebab-case,如 'margin-top')或字符串。JSX.CSSProperties | string-
    treeData节点数据,key 在整棵树内唯一。只有传入新数组才会更新,原地修改数组或节点不会生效。TreeNodeData[]-
    treeDataSimpleJson简单 JSON 形式的节点数据,返回值为包含选中节点的 JSON 数据。TreeDataSimpleJson-
    treeNodeFilterProp搜索时用于匹配输入的节点属性。string"label"
    value(受控)当前选中节点的 value(没有 value 时为 key)。传入后即为受控组件,配合 onChange 使用;多选时为数组。Value-
    virtualize列表虚拟化,用于大量节点:height、width、itemSize(必传)。开启后动画关闭。Virtualize-

    Methods

    方法说明类型
    scrollTo在虚拟化的 Tree 中,把指定的已展开节点滚动到视图内;align 默认 center。传入其他参数时执行原生的 scrollTo。Type(scrollData: ScrollData) => void
    search手动触发搜索,效果同在搜索框中输入;需要同时设置 filterTreeNode。Type(value: string) => void

    class、style 和 ref 以及未声明的原生属性(如 data-*)落在根元素 .semi-tree-wrapper 上,aria-* 与 tabIndex 落在 role="tree" 的元素上。空状态下没有 role="tree" 的元素,aria-* 与 tabIndex 被丢弃。

    TreeNodeData

    不同 TreeNodeData 的 key 值要求必填且唯一。label 允许重复。value 值非必填,不填时 onChange、value、defaultValue 及 onChangeWithObject 中所取的 value 属性值将改为 key 值。为了保证行为的符合预期,treeData 中的 value 值或者全部不填写,或者全部填写且唯一,不建议混写。

    属性说明类型默认值
    value属性值string | number-
    label展示的文本,自定义内容使用 renderLabelstring | number-
    disabled是否禁用,Tree 的 disabled 为 true 时整棵树都禁用,节点的 disabled: false 不会解除booleanfalse
    key必填且唯一string-
    isLeaf设置节点为叶子节点,在异步加载数据的情况即传入 loadData 时有效boolean-

    Virtualize Object

    itemSize 必传。

    属性说明类型默认值
    height高度值,如果为 string 必须保证有计算高度,即其父节点有 offsetHeightnumber|string'100%'
    itemSize每行的treeNode的高度,必传number-
    width宽度值number|string'100%'

    Methods

    Tree 的 ref 指向根元素,它带有 search 和 scrollTo 两个方法,类型为 TreeElement,方法说明见上方 Tree 的 API 表格。

    import { Tree, type TreeElement } from '@fibrous/semi-ui-solid';
    
    let tree: TreeElement | undefined;
    const search = (value: string) => tree?.search(value);
    
    <Tree ref={(element) => (tree = element)} 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 返回的内容由 Tree 自己的 li 包住,这个 li 带有 id、role="treeitem" 和上述状态。
    • 节点前的展开箭头和多选复选框只是状态的图示,对读屏软件隐藏,也不接收焦点。

    键盘交互

    Tree 按 WAI-ARIA Tree View 处理键盘:整棵树是一个 Tab 停靠点,焦点停在树上,aria-activedescendant 指向当前节点,当前节点显示焦点环。

    • ArrowDown、ArrowUp:移到下一个、上一个可见节点;Home、End:移到第一个、最后一个可见节点
    • ArrowRight:展开收起的节点;节点已展开时移到它的第一个子节点(RTL 下与 ArrowLeft 互换)
    • ArrowLeft:收起展开的节点;否则移到父节点
    • Enter、Space:选中当前节点,多选时勾选或取消勾选;禁用节点不响应
    • 输入字符:移到文本以输入内容开头的下一个节点

    用鼠标选中、勾选、展开、右键或双击的节点,同时成为键盘的当前节点,之后的方向键从它开始移动。

    键盘不触发 expandAction,展开、收起只用方向键。

    节点内容里的按钮、链接(如 renderLabel 或 renderFullLabel 中的控件)可以用 Tab 到达,焦点在它们上面时,按键由控件自己处理,树不响应。

    示例:

    <Tree aria-label="example tree" treeData={treeData} />

    文案规范

    • 尽量使用短语,首字母大写
    • 平级之间保持用语形式一致,例如全是地名或者是国家名

    设计变量

    下表是组件内部的 Sass 变量,不能覆写。定制样式请覆写 --semi-* CSS 变量(见 Design Token 页),或写不分层的 CSS 规则。
    Sass 变量值用法
    $transition_duration-tree_option-bgvar(--semi-transition_duration-none)树选项-背景色-动画持续时间
    $transition_function-tree_option-bgvar(--semi-transition_function-easeIn)树选项-背景色-过渡曲线
    $transition_delay-tree_option-bgvar(--semi-transition_delay-none)树选项-背景色-延迟时间
    $transform_scale-tree-optionvar(--semi-transform_scale-none)树选项-放大
    $color-tree_option-text-defaultvar(--semi-color-text-0)树选项文字颜色
    $color-tree_option-icon-defaultvar(--semi-color-text-2)树选项图标颜色
    $color-tree_option-bg-hovervar(--semi-color-fill-0)树选项背景色 - 悬停
    $color-tree_option_selected-bg-defaultvar(--semi-color-fill-1)树选项背景色 - 按下
    $color-tree_option-bg-activevar(--semi-color-primary-light-default)树选项背景色 - 选中
    $color-tree_option_disabled-text-defaultvar(--semi-color-disabled-text)禁用树选项文字颜色
    $color-tree_option-icon-hovervar(--semi-color-text-0)树选项图标颜色 - 悬浮
    $color-tree_option-icon-activevar(--semi-color-black)树选项图标颜色 - 按下
    $color-tree_option_loading-icon-defaultvar(--semi-color-primary)树选项加载 spin 颜色
    $color-tree_option_draggable_insert-border-defaultvar(--semi-color-primary)树选项加载 spin 颜色
    $font-tree_option_hightlight-fontWeight$font-weight-bold树选项高亮文本字重
    $color-tree_option_hightlight-textvar(--semi-color-primary)树选项高亮文本颜色
    $radius-tree_checkbox_addonvar(--semi-border-radius-small)多选树 checkbox 圆角
    $spacing-tree_search_wrapper-paddingX12px树搜索框水平内边距
    $spacing-tree_search_wrapper-paddingY8px树搜索框垂直内边距
    $spacing-tree_option_level-paddingLeft20px树选项缩进增量
    $spacing-tree_option_level1-paddingLeft8px树选项最上层左侧内边距
    $spacing-tree_optionList-paddingX0树选项列表水平内边距
    $spacing-tree_optionList-paddingY8px树选项列表垂直内边距
    $spacing-tree_option-paddingTop4px树选项顶部内边距
    $spacing-tree_option-paddingBottom4px树选项底部内边距
    $spacing-tree_icon-marginRight8px树选项图标右侧外边距
    $spacing-tree_label_withIcon-marginRight8px树选项图标右侧外边距
    $spacing-tree_option_draggable-paddingY2px可拖拽的树选项垂直内边距
    $spacing-tree_option_draggable-paddingX0可拖拽的树选项水平内边距
    $width-tree_emptyIcon$width-icon-small树选项空图标宽度
    $width-tree_spinIcon$width-icon-small树选项加载 spin 宽度
    $width-tree_option_draggable-border2px可拖拽的树标示线宽度
    $width-tree_option_line1pxshowline展示线宽度