Input · Transfer

Transfer

A more intuitive and efficient multiple-selection selector, which can reveal more information about options, and supports search functions. The disadvantage is that it takes up more space

Demos

How to import

import { Avatar, Button, ButtonGroup, Checkbox, Highlight, Input, Spin, Transfer } from '@fibrous/semi-ui-solid';

Basic Usage

dataSource should have value、label、key.

总个数:100
选项名称 0
选项名称 1
选项名称 2
选项名称 3
选项名称 4
选项名称 5
选项名称 6
选项名称 7
选项名称 8
选项名称 9
选项名称 10
选项名称 11
选项名称 12
选项名称 13
选项名称 14
选项名称 15
选项名称 16
选项名称 17
选项名称 18
选项名称 19
选项名称 20
选项名称 21
选项名称 22
选项名称 23
选项名称 24
选项名称 25
选项名称 26
选项名称 27
选项名称 28
选项名称 29
选项名称 30
选项名称 31
选项名称 32
选项名称 33
选项名称 34
选项名称 35
选项名称 36
选项名称 37
选项名称 38
选项名称 39
选项名称 40
选项名称 41
选项名称 42
选项名称 43
选项名称 44
选项名称 45
选项名称 46
选项名称 47
选项名称 48
选项名称 49
选项名称 50
选项名称 51
选项名称 52
选项名称 53
选项名称 54
选项名称 55
选项名称 56
选项名称 57
选项名称 58
选项名称 59
选项名称 60
选项名称 61
选项名称 62
选项名称 63
选项名称 64
选项名称 65
选项名称 66
选项名称 67
选项名称 68
选项名称 69
选项名称 70
选项名称 71
选项名称 72
选项名称 73
选项名称 74
选项名称 75
选项名称 76
选项名称 77
选项名称 78
选项名称 79
选项名称 80
选项名称 81
选项名称 82
选项名称 83
选项名称 84
选项名称 85
选项名称 86
选项名称 87
选项名称 88
选项名称 89
选项名称 90
选项名称 91
选项名称 92
选项名称 93
选项名称 94
选项名称 95
选项名称 96
选项名称 97
选项名称 98
选项名称 99
已选个数:0
暂无内容,可从左侧勾选
Code
import { Transfer } from '@fibrous/semi-ui-solid';

const data = Array.from({ length: 100 }, (_, i) => {
    return {
        label: `选项名称 ${i}`,
        value: i,
        disabled: false,
        key: `key-${i}`,
    };
});

export default function TransferDemo() {
    return <Transfer style={{ width: '568px', height: '416px' }} dataSource={data} onChange={(values, items) => console.log(values, items)} />;
}

Grouped

Set type to groupList

For grouped dataSource, the first-level child elements must have title and children attributes, structure reference <GroupItem>

Does not support multi-level nesting

总个数:13
类别A
A-1
A-2
A-3
类别B
B-1
B-2
B-3(disabled)
类别C
C-1
C-2
C-3
C-4
C-5
C-6
C-7
已选个数:1
B-3(disabled)
Code
import { Transfer } from '@fibrous/semi-ui-solid';

const dataWithGroup = [
    {
        title: '类别A',
        children: [
            { label: 'A-1', value: 1, disabled: false, key: 1 },
            { label: 'A-2', value: 2, disabled: false, key: 2 },
            { label: 'A-3', value: 3, disabled: false, key: 3 },
        ],
    },
    {
        title: '类别B',
        children: [
            { label: 'B-1', value: 4, disabled: false, key: 4 },
            { label: 'B-2', value: 5, disabled: false, key: 5 },
            { label: 'B-3(disabled)', value: 6, disabled: true, key: 6 },
        ],
    },
    {
        title: '类别C',
        children: [
            { label: 'C-1', value: 7, disabled: false, key: 7 },
            { label: 'C-2', value: 8, disabled: false, key: 8 },
            { label: 'C-3', value: 9, disabled: false, key: 9 },
            { label: 'C-4', value: 10, disabled: false, key: 10 },
            { label: 'C-5', value: 11, disabled: false, key: 11 },
            { label: 'C-6', value: 12, disabled: false, key: 12 },
            { label: 'C-7', value: 13, disabled: false, key: 13 },
        ],
    },
];

export default function TransferDemo() {
    return <Transfer type="groupList" defaultValue={[6]} style={{ width: '568px' }} dataSource={dataWithGroup} onChange={(values, items) => console.log(values, items)} />;
}

Custom filtering logic, custom option data rendering

Use filter to customize the search logic. When it returns true, it means that the current item meets the filter rules and keeps the display of the current item in the list. If it returns false, it means it does not match, and the current item will be hidden.
When type is treeList, if you need to customize search logic, you need to set filter to true and set a custom search function through filterTreeNode of treeProps.

Using renderSourceItem, you can customize the rendering structure of each source data on the left. For example, combine it with the Highlight component to highlight the matched search text.
Using renderSelectedItem you can customize the rendering structure of each selected item on the right. Both are called once per item; the checked in the argument is a getter, read it in the returned JSX.

总个数:6
夏
夏可漫
申
申悦
文
文嘉茂
曲
曲晨一
二
曲晨二
三
曲晨三
已选个数:2
夏
夏可漫
申
申悦
Code
import { createSignal } from 'solid-js';
import { IconClose } from '@fibrous/semi-icons-solid';
import { Transfer, Button, Checkbox, Avatar, Highlight, type TransferDataItem, type TransferRenderSelectedItemProps, type TransferRenderSourceItemProps } from '@fibrous/semi-ui-solid';

const data = [
    { label: '夏可漫', value: 'xiakeman@example.com', abbr: '夏', color: 'amber', area: 'US', key: 1 },
    { label: '申悦', value: 'shenyue@example.com', abbr: '申', color: 'indigo', area: 'UK', key: 2 },
    { label: '文嘉茂', value: 'wenjiamao@example.com', abbr: '文', color: 'cyan', area: 'HK', key: 3 },
    { label: '曲晨一', value: 'quchenyi@example.com', abbr: '曲', color: 'blue', area: 'India', key: 4 },
    { label: '曲晨二', value: 'quchener@example.com', abbr: '二', color: 'blue', area: 'India', key: 5 },
    { label: '曲晨三', value: 'quchensan@example.com', abbr: '三', color: 'blue', area: 'India', key: 6 },
];

const customFilter = (searchValue: string, item: TransferDataItem) => {
    return String(item.value).includes(searchValue) || String(item.label).includes(searchValue);
};

export default function TransferDemo() {
    const [searchText, setSearchText] = createSignal('');

    const renderSourceItem = (item: TransferRenderSourceItemProps) => {
        return (
            <div class="components-transfer-demo-source-item">
                <Checkbox
                    onChange={() => {
                        item.onChange?.();
                    }}
                    checked={item.checked}
                    style={{ height: '52px', 'align-items': 'center' }}
                >
                    <Avatar color={item.color} size="small">
                        {item.abbr}
                    </Avatar>
                    <div class="info">
                        <div class="name">
                            <Highlight sourceString={String(item.label)} searchWords={[searchText()]} />
                        </div>
                        <div class="email">
                            <Highlight sourceString={String(item.value)} searchWords={[searchText()]} />
                        </div>
                    </div>
                </Checkbox>
            </div>
        );
    };

    const renderSelectedItem = (item: TransferRenderSelectedItemProps) => {
        return (
            <div class="components-transfer-demo-selected-item">
                <Avatar color={item.color} size="small">
                    {item.abbr}
                </Avatar>
                <div class="info">
                    <div class="name">{item.label}</div>
                    <div class="email">{item.value}</div>
                </div>
                <Button class="remove" theme="borderless" type="tertiary" size="small" icon={<IconClose />} aria-label={`移除 ${String(item.label)}`} onClick={() => item.onRemove?.()} />
            </div>
        );
    };

    return (
        <Transfer
            style={{ width: '568px' }}
            dataSource={data}
            filter={customFilter}
            defaultValue={['xiakeman@example.com', 'shenyue@example.com']}
            renderSelectedItem={renderSelectedItem}
            renderSourceItem={renderSourceItem}
            inputProps={{ placeholder: '搜索姓名或邮箱' }}
            onSearch={(text) => setSearchText(text)}
            onChange={(values, items) => console.log(values, items)}
        />
    );
}
.components-transfer-demo-selected-item {
    .remove {
        opacity: 0;
    }
    &:hover,
    &:focus-within {
        .remove {
            opacity: 1;
        }
    }
}

.components-transfer-demo-selected-item,
.components-transfer-demo-source-item {
    height: 52px;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 12px;
    &:hover {
        background-color: var(--semi-color-fill-0);
    }
    .info {
        margin-left: 8px;
        flex-grow: 1;
    }
    .name {
        font-size: 14px;
        line-height: 20px;
    }
    .email {
        font-size: 12px;
        line-height: 16px;
        color: var(--semi-color-text-2);
    }
}

Disabled

总个数:20
选项名称 0
选项名称 1
选项名称 2
选项名称 3
选项名称 4
选项名称 5
选项名称 6
选项名称 7
选项名称 8
选项名称 9
选项名称 10
选项名称 11
选项名称 12
选项名称 13
选项名称 14
选项名称 15
选项名称 16
选项名称 17
选项名称 18
选项名称 19
已选个数:2
选项名称 2
选项名称 4
Code
import { Transfer } from '@fibrous/semi-ui-solid';

const data = Array.from({ length: 20 }, (_, i) => {
    return {
        label: `选项名称 ${i}`,
        value: i,
        disabled: false,
        key: `key-${i}`,
    };
});

export default function TransferDemo() {
    return <Transfer style={{ width: '568px', height: '416px' }} disabled dataSource={data} defaultValue={[2, 4]} onChange={(values, items) => console.log(values, items)} />;
}

Drag and drop sort

Set draggable to true to enable the drag sort function.

总个数:30
选项名称 0
选项名称 1
选项名称 2
选项名称 3
选项名称 4
选项名称 5
选项名称 6
选项名称 7
选项名称 8
选项名称 9
选项名称 10
选项名称 11
选项名称 12
选项名称 13
选项名称 14
选项名称 15
选项名称 16
选项名称 17
选项名称 18
选项名称 19
选项名称 20
选项名称 21
选项名称 22
选项名称 23
选项名称 24
选项名称 25
选项名称 26
选项名称 27
选项名称 28
选项名称 29
已选个数:2
选项名称 2
选项名称 4
Code
import { Transfer } from '@fibrous/semi-ui-solid';

const data = Array.from({ length: 30 }, (_, i) => {
    return {
        label: `选项名称 ${i}`,
        value: i,
        disabled: false,
        key: `key-${i}`,
    };
});

export default function TransferDemo() {
    return <Transfer style={{ width: '568px', height: '416px' }} dataSource={data} defaultValue={[2, 4]} draggable onChange={(values, items) => console.log(values, items)} />;
}

Left panel pagination

When there are many options on the left, you can enable pagination through the pagination prop.

pagination accepts an object with the following properties:

  • pageSize: Number of items per page, default is 10
  • currentPage: Current page number (controlled mode)
  • defaultCurrentPage: Default current page number (uncontrolled mode)
  • onPageChange: Callback when page changes
总个数:100
选项名称 0
选项名称 1
选项名称 2
选项名称 3
选项名称 4
选项名称 5
选项名称 6
选项名称 7
选项名称 8
选项名称 9
已选个数:0
暂无内容,可从左侧勾选
Code
import { Transfer } from '@fibrous/semi-ui-solid';

const data = Array.from({ length: 100 }, (_, i) => {
    return {
        label: `选项名称 ${i}`,
        value: i,
        disabled: false,
        key: `key-${i}`,
    };
});

export default function TransferDemo() {
    return (
        <Transfer
            style={{ width: '568px', height: '416px' }}
            dataSource={data}
            pagination={{
                pageSize: 10,
            }}
            onChange={(values, items) => console.log(values, items)}
        />
    );
}

Left panel pagination + controlled page

You can control the current page number through pagination.currentPage.

当前页码: 1
总个数:100
选项名称 0
选项名称 1
选项名称 2
选项名称 3
选项名称 4
选项名称 5
选项名称 6
选项名称 7
选项名称 8
选项名称 9
已选个数:0
暂无内容,可从左侧勾选
Code
import { createSignal } from 'solid-js';
import { Transfer, ButtonGroup, Button } from '@fibrous/semi-ui-solid';

const data = Array.from({ length: 100 }, (_, i) => {
    return {
        label: `选项名称 ${i}`,
        value: i,
        disabled: false,
        key: `key-${i}`,
    };
});

export default function TransferDemo() {
    const [currentPage, setCurrentPage] = createSignal(1);
    return (
        <div>
            <ButtonGroup style={{ 'margin-bottom': '12px' }}>
                <Button onClick={() => setCurrentPage(1)}>第1页</Button>
                <Button onClick={() => setCurrentPage(2)}>第2页</Button>
                <Button onClick={() => setCurrentPage(5)}>第5页</Button>
                <Button onClick={() => setCurrentPage(10)}>第10页</Button>
            </ButtonGroup>
            <div>当前页码: {currentPage()}</div>
            <Transfer
                style={{ width: '568px', height: '416px' }}
                dataSource={data}
                pagination={{
                    pageSize: 10,
                    currentPage: currentPage(),
                    onPageChange: (page) => setCurrentPage(page),
                }}
                onChange={(values, items) => console.log(values, items)}
            />
        </div>
    );
}

Drag and drop + custom selected rendering

Set draggable to true to enable the drag and drop sorting function; use renderSelectedItem to customize the rendering of the selected items on the right. sortableHandle takes a component that wraps the drag icon and returns the drag handle component; render it in the returned JSX. The handle is a native button. Without draggable, sortableHandle is not provided.

总个数:6
夏
夏可漫
申
申悦
文
文嘉茂
曲
曲晨一
二
曲晨二
三
曲晨三
已选个数:2
夏
夏可漫
申
申悦
Code
import { IconHandle, IconClose } from '@fibrous/semi-icons-solid';
import { Transfer, Button, Checkbox, Avatar, type TransferDataItem, type TransferRenderSelectedItemProps, type TransferRenderSourceItemProps } from '@fibrous/semi-ui-solid';

const renderSourceItem = (item: TransferRenderSourceItemProps) => {
    return (
        <div class="components-transfer-demo-source-item">
            <Checkbox
                onChange={() => {
                    item.onChange?.();
                }}
                checked={item.checked}
                style={{ height: '52px', 'align-items': 'center' }}
            >
                <Avatar color={item.color} size="small">
                    {item.abbr}
                </Avatar>
                <div class="info">
                    <div class="name">{item.label}</div>
                    <div class="email">{item.value}</div>
                </div>
            </Checkbox>
        </div>
    );
};

const renderSelectedItem = (item: TransferRenderSelectedItemProps) => {
    // the handle exists only with `draggable`, which this demo sets
    const DragHandle = item.sortableHandle?.(() => <IconHandle class="semi-right-item-drag-handler" />);
    return (
        <div class="components-transfer-demo-selected-item">
            {DragHandle && <DragHandle />}
            <Avatar color={item.color} size="small">
                {item.abbr}
            </Avatar>
            <div class="info">
                <div class="name">{item.label}</div>
                <div class="email">{item.value}</div>
            </div>
            <Button class="remove" theme="borderless" type="tertiary" size="small" icon={<IconClose />} aria-label={`移除 ${String(item.label)}`} onClick={() => item.onRemove?.()} />
        </div>
    );
};

const customFilter = (searchValue: string, item: TransferDataItem) => {
    return String(item.value).includes(searchValue) || String(item.label).includes(searchValue);
};

const data = [
    { label: '夏可漫', value: 'xiakeman@example.com', abbr: '夏', color: 'amber', area: 'US', key: 1 },
    { label: '申悦', value: 'shenyue@example.com', abbr: '申', color: 'indigo', area: 'UK', key: 2 },
    { label: '文嘉茂', value: 'wenjiamao@example.com', abbr: '文', color: 'cyan', area: 'HK', key: 3 },
    { label: '曲晨一', value: 'quchenyi@example.com', abbr: '曲', color: 'blue', area: 'India', key: 4 },
    { label: '曲晨二', value: 'quchener@example.com', abbr: '二', color: 'blue', area: 'India', key: 5 },
    { label: '曲晨三', value: 'quchensan@example.com', abbr: '三', color: 'blue', area: 'India', key: 6 },
];

export default function TransferDemo() {
    return (
        <Transfer
            draggable
            style={{ width: '568px' }}
            dataSource={data}
            filter={customFilter}
            defaultValue={['xiakeman@example.com', 'shenyue@example.com']}
            renderSelectedItem={renderSelectedItem}
            renderSourceItem={renderSourceItem}
            inputProps={{ placeholder: '搜索姓名或邮箱' }}
            onChange={(values, items) => console.log(values, items)}
        />
    );
}

Custom rendering header information in panel

renderSourceHeader and renderSelectedHeader receive live objects, typed as TransferSourceHeaderProps and TransferSelectedHeaderProps, and each panel calls them once. Read their fields in the JSX; destructuring takes only the values of the first render:

type SourceHeaderProps = {
    num: number; // The total number of data or the number of filtered results
    showButton: boolean; // Whether to show select all/unselect all buttons
    allChecked: boolean; // Whether the current data has been selected
    onAllClick: () => void; // Function that should be called after clicking the select/unselect all button
    leafOnlyNum?: number; // Only provided when type is treeList, represents the number of leaf nodes
};

type SelectedHeaderProps = {
    num: number; // The total number of selected data
    showButton: boolean; // Whether to show the clear button
    onClear: () => void; // Function that should be called after clicking the clear button
};

The example is as follows:

共 30 项
选项名称 0
选项名称 1
选项名称 2
选项名称 3
选项名称 4
选项名称 5
选项名称 6
选项名称 7
选项名称 8
选项名称 9
选项名称 10
选项名称 11
选项名称 12
选项名称 13
选项名称 14
选项名称 15
选项名称 16
选项名称 17
选项名称 18
选项名称 19
选项名称 20
选项名称 21
选项名称 22
选项名称 23
选项名称 24
选项名称 25
选项名称 26
选项名称 27
选项名称 28
选项名称 29
0 项已选
暂无内容,可从左侧勾选
Code
import { Transfer, Button, type TransferSelectedHeaderProps, type TransferSourceHeaderProps } from '@fibrous/semi-ui-solid';

const data = Array.from({ length: 30 }, (_, i) => {
    return {
        label: `选项名称 ${i}`,
        value: i,
        disabled: false,
        key: `key-${i}`,
    };
});

// the headers read their live objects in the JSX, so each renders once and follows the state
const renderSourceHeader = (header: TransferSourceHeaderProps) => {
    return (
        <div style={{ margin: '10px 0 0 10px', height: '24px', display: 'flex', 'align-items': 'center' }}>
            <span>共 {header.num} 项</span>
            {header.showButton && (
                <Button theme="borderless" type="tertiary" size="small" onClick={() => header.onAllClick()}>
                    {header.allChecked ? '取消全选' : '全选'}
                </Button>
            )}
        </div>
    );
};

const renderSelectedHeader = (header: TransferSelectedHeaderProps) => {
    return (
        <div style={{ margin: '10px 0 0 10px', height: '24px', display: 'flex', 'align-items': 'center' }}>
            <span>{header.num} 项已选</span>
            {header.showButton && (
                <Button theme="borderless" type="tertiary" size="small" onClick={() => header.onClear()}>
                    清空
                </Button>
            )}
        </div>
    );
};

export default function TransferDemo() {
    return <Transfer style={{ width: '568px', height: '416px' }} dataSource={data} renderSourceHeader={renderSourceHeader} renderSelectedHeader={renderSelectedHeader} />;
}

Fully custom rendering

renderSourcePanel and renderSelectedPanel let you fully customize the rendering of the left and right panels, and reuse the filtering and selection logic inside Transfer. Both arguments are live objects, typed as TransferSourcePanelProps and TransferSelectedPanelProps, and each panel calls its render function once; read the fields in the JSX.

interface SourcePanelProps {
    value: Array<string | number>; // The values of all selected items
    loading?: boolean; // Whether it is loading
    noMatch: boolean; // Whether no item matches the current search value
    filterData: Array<Item>; // Items matching the current search value
    sourceData: Array<Item>; // All items
    propsDataSource: Array<Item> | Array<GroupItem> | Array<TreeItem>; // The dataSource of Transfer
    allChecked: boolean; // Whether all items are selected
    showNumber: number; // The number of filtered results
    inputValue: string; // The value of the search input
    onSearch: (searchString: string) => void; // Called when the search box changes, with the search value
    onAllClick: () => void; // Called when the select all button on the left is clicked
    onSelectOrRemove: (item: Item) => void; // Called when an item is selected or removed, with the current item
    onSelect: (value: Array<string | number>) => void; // Controlled batch selection, with the values of the selected items
    selectedItems: Map<string | number, Item>; // All selected items, keyed by item.key
}
interface SelectedPanelProps {
    length: number; // The number of selected items
    onClear: () => void; // Callback called when clearing
    onRemove: (item: Item) => void; // Called when a single item is removed, with the item
    onSortEnd: ({ oldIndex, newIndex }) => void; // Called when the results are reordered, with the positions before and after the drag
    selectedData: Array<Item>; // All selected items
}
门店列表
待选门店: 100

海底捞门店 0

海底捞门店 1

海底捞门店 2

海底捞门店 3

海底捞门店 4

海底捞门店 5

海底捞门店 6

海底捞门店 7

海底捞门店 8

海底捞门店 9

海底捞门店 10

海底捞门店 11

海底捞门店 12

海底捞门店 13

海底捞门店 14

海底捞门店 15

海底捞门店 16

海底捞门店 17

海底捞门店 18

海底捞门店 19

海底捞门店 20

海底捞门店 21

海底捞门店 22

海底捞门店 23

海底捞门店 24

海底捞门店 25

海底捞门店 26

海底捞门店 27

海底捞门店 28

海底捞门店 29

海底捞门店 30

海底捞门店 31

海底捞门店 32

海底捞门店 33

海底捞门店 34

海底捞门店 35

海底捞门店 36

海底捞门店 37

海底捞门店 38

海底捞门店 39

海底捞门店 40

海底捞门店 41

海底捞门店 42

海底捞门店 43

海底捞门店 44

海底捞门店 45

海底捞门店 46

海底捞门店 47

海底捞门店 48

海底捞门店 49

海底捞门店 50

海底捞门店 51

海底捞门店 52

海底捞门店 53

海底捞门店 54

海底捞门店 55

海底捞门店 56

海底捞门店 57

海底捞门店 58

海底捞门店 59

海底捞门店 60

海底捞门店 61

海底捞门店 62

海底捞门店 63

海底捞门店 64

海底捞门店 65

海底捞门店 66

海底捞门店 67

海底捞门店 68

海底捞门店 69

海底捞门店 70

海底捞门店 71

海底捞门店 72

海底捞门店 73

海底捞门店 74

海底捞门店 75

海底捞门店 76

海底捞门店 77

海底捞门店 78

海底捞门店 79

海底捞门店 80

海底捞门店 81

海底捞门店 82

海底捞门店 83

海底捞门店 84

海底捞门店 85

海底捞门店 86

海底捞门店 87

海底捞门店 88

海底捞门店 89

海底捞门店 90

海底捞门店 91

海底捞门店 92

海底捞门店 93

海底捞门店 94

海底捞门店 95

海底捞门店 96

海底捞门店 97

海底捞门店 98

海底捞门店 99

已选同步门店: 0
暂无数据,请从左侧筛选
Code
import { For, Show } from 'solid-js';
import { IconSearch } from '@fibrous/semi-icons-solid';
import { Transfer, Input, Spin, Button, type TransferSelectedPanelProps as SelectedPanelProps, type TransferSourcePanelProps as SourcePanelProps } from '@fibrous/semi-ui-solid';

const dataSource = Array.from({ length: 100 }, (_, i) => ({
    label: `海底捞门店 ${i}`,
    value: i,
    disabled: false,
    key: `key-${i}`,
}));

// the panels read their live objects in the JSX: the lists follow the state without a second render call
function renderSourcePanel(panel: SourcePanelProps) {
    return (
        <section class="source-panel">
            <div class="panel-header sp-font">门店列表</div>
            <div class="panel-main">
                <Input aria-label="搜索门店" style={{ width: '454px', margin: '12px 14px' }} prefix={<IconSearch />} onChange={(value) => panel.onSearch(value)} showClear />
                <div class="panel-controls sp-font">
                    <span>待选门店: {panel.filterData.length}</span>
                    <Button onClick={() => panel.onAllClick()} theme="borderless" size="small">
                        {panel.allChecked ? '取消全选' : '全选'}
                    </Button>
                </div>
                <div class="panel-list">
                    <Show when={!panel.loading} fallback={<Spin />}>
                        <Show when={!panel.noMatch} fallback={<div class="empty sp-font">{panel.inputValue ? '无搜索结果' : '暂无内容'}</div>}>
                            <For each={panel.filterData}>
                                {(item) => (
                                    <div class="semi-transfer-item panel-item">
                                        <p>{item.label}</p>
                                        <Button theme="borderless" type="primary" onClick={() => panel.onSelectOrRemove(item)} class="panel-item-remove" size="small">
                                            {panel.selectedItems.has(item.key) ? '删除' : '添加'}
                                        </Button>
                                    </div>
                                )}
                            </For>
                        </Show>
                    </Show>
                </div>
            </div>
        </section>
    );
}

function renderSelectedPanel(panel: SelectedPanelProps) {
    return (
        <section class="selected-panel">
            <div class="panel-header sp-font">
                <div>已选同步门店: {panel.length}</div>
                <Button theme="borderless" type="primary" onClick={() => panel.onClear()} size="small">
                    清空
                </Button>
            </div>
            <div class="panel-main">
                <Show when={panel.length > 0} fallback={<div class="empty sp-font">暂无数据,请从左侧筛选</div>}>
                    <For each={panel.selectedData}>
                        {(item) => (
                            <div class="semi-transfer-item panel-item">
                                <p>{item.label}</p>
                                <Button theme="borderless" type="primary" onClick={() => panel.onRemove(item)} class="panel-item-remove" size="small">
                                    删除
                                </Button>
                            </div>
                        )}
                    </For>
                </Show>
            </div>
        </section>
    );
}

export default function TransferDemo() {
    return <Transfer onChange={(values) => console.log(values)} class="component-transfer-demo-custom-panel" renderSourcePanel={renderSourcePanel} renderSelectedPanel={renderSelectedPanel} dataSource={dataSource} />;
}
.component-transfer-demo-custom-panel {
    .sp-font {
        color: var(--semi-grey-9);
        font-size: 12px;
        font-weight: 500;
        line-height: 20px;
    }

    .empty {
        width: 100%;
        height: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    .panel-item {
        flex-shrink: 0;
        height: 56px;
        border-radius: 4px;
        padding: 8px 12px;
        flex-wrap: wrap;
        background-color: rgba(22, 24, 35, 0.03);

        &-main {
            flex-grow: 1;
        }

        p {
            margin: 0 12px;
            flex-basis: 100%;
        }

        .panel-item-remove {
            cursor: pointer;
            color: var(--semi-color-primary);
        }
    }

    .panel-header {
        padding: 10px 12px;
        border: 1px solid rgba(22, 24, 35, 0.16);
        border-radius: 4px 4px 0 0;
        height: 38px;
        box-sizing: border-box;
        background-color: var(--semi-color-tertiary-light-default);
        display: flex;
        align-items: center;
        justify-content: space-between;

        .clear {
            cursor: pointer;
            color: var(--semi-color-primary);
        }
    }

    .source-panel {
        display: flex;
        flex-direction: column;
        width: 482px;
        height: 353px;

        .panel-main {
            border: 1px solid var(--semi-color-border);
            border-top: none;

            .panel-list {
                display: flex;
                flex-wrap: wrap;
                row-gap: 8px;
                column-gap: 8px;
                overflow-y: auto;
                height: 214px;
                margin-left: 12px;
                margin-right: 12px;
                padding-bottom: 8px;
            }
        }

        .panel-controls {
            margin: 10px 12px;
            font-size: 12px;
            line-height: 20px;

            .semi-button {
                margin-left: 8px;
                font-size: 12px;
            }
        }

        .panel-item {
            width: 176px;
        }

        margin-right: 16px;
    }

    .selected-panel {
        width: 200px;
        height: 353px;

        .panel-main {
            display: flex;
            flex-direction: column;
            overflow-y: auto;
            padding: 12px;
            border: 1px solid var(--semi-color-border);
            border-top: none;
            height: 323px;
            box-sizing: border-box;
            row-gap: 8px;
        }
    }
}

Fully custom rendering, drag and drop sorting

In a completely custom rendering scene, since the rendering of the drag area has also been completely taken over by you, you do not need to declare draggable. But you need to implement the drag and drop logic yourself, You can use the drag-and-drop tool library dnd-kit to quickly realize the function.

Besides, To support drag sorting, you need to call onSortEnd with oldIndex and newIndex as the input parameters after the drag sorting is over

The dnd-kit example is as follows. The core dependency is @dnd-kit/solid: DragDropProvider provides the drag context, and useSortable exported from @dnd-kit/solid/sortable takes id and index. Both are passed as getters, so the index read after the list is reordered is up to date.

1. Purpose: get the information needed during dragging from the unique id and the current position index
2. Core input parameters:
    - id: unique identifier, a number or a string
    - index: the current position of the item in the list
3. Core return values:
    - ref: binds the dom node of a draggable item, used as ref={(node) => ref(node)}
    - handleRef: binds the dom node of the drag handle; only the handle drags
    - isDragSource: whether this node is being dragged

After dragging ends, in the onDragEnd of DragDropProvider, use initialIndex and index of event.operation.source to get the positions before and after the drag; event.canceled being true means the drag was canceled.

门店列表
待选门店: 100

海底捞门店 0

海底捞门店 1

海底捞门店 2

海底捞门店 3

海底捞门店 4

海底捞门店 5

海底捞门店 6

海底捞门店 7

海底捞门店 8

海底捞门店 9

海底捞门店 10

海底捞门店 11

海底捞门店 12

海底捞门店 13

海底捞门店 14

海底捞门店 15

海底捞门店 16

海底捞门店 17

海底捞门店 18

海底捞门店 19

海底捞门店 20

海底捞门店 21

海底捞门店 22

海底捞门店 23

海底捞门店 24

海底捞门店 25

海底捞门店 26

海底捞门店 27

海底捞门店 28

海底捞门店 29

海底捞门店 30

海底捞门店 31

海底捞门店 32

海底捞门店 33

海底捞门店 34

海底捞门店 35

海底捞门店 36

海底捞门店 37

海底捞门店 38

海底捞门店 39

海底捞门店 40

海底捞门店 41

海底捞门店 42

海底捞门店 43

海底捞门店 44

海底捞门店 45

海底捞门店 46

海底捞门店 47

海底捞门店 48

海底捞门店 49

海底捞门店 50

海底捞门店 51

海底捞门店 52

海底捞门店 53

海底捞门店 54

海底捞门店 55

海底捞门店 56

海底捞门店 57

海底捞门店 58

海底捞门店 59

海底捞门店 60

海底捞门店 61

海底捞门店 62

海底捞门店 63

海底捞门店 64

海底捞门店 65

海底捞门店 66

海底捞门店 67

海底捞门店 68

海底捞门店 69

海底捞门店 70

海底捞门店 71

海底捞门店 72

海底捞门店 73

海底捞门店 74

海底捞门店 75

海底捞门店 76

海底捞门店 77

海底捞门店 78

海底捞门店 79

海底捞门店 80

海底捞门店 81

海底捞门店 82

海底捞门店 83

海底捞门店 84

海底捞门店 85

海底捞门店 86

海底捞门店 87

海底捞门店 88

海底捞门店 89

海底捞门店 90

海底捞门店 91

海底捞门店 92

海底捞门店 93

海底捞门店 94

海底捞门店 95

海底捞门店 96

海底捞门店 97

海底捞门店 98

海底捞门店 99

已选同步门店: 2

海底捞门店 2

海底捞门店 4

Code
import { For, Show } from 'solid-js';
import { DragDropProvider, type DragDropProviderProps } from '@dnd-kit/solid';
import { isSortable, useSortable } from '@dnd-kit/solid/sortable';
import { IconSearch, IconHandle } from '@fibrous/semi-icons-solid';
import { Transfer, Input, Spin, Button, type TransferDataItem, type TransferOnSortEnd as OnSortEnd, type TransferSelectedPanelProps as SelectedPanelProps, type TransferSourcePanelProps as SourcePanelProps } from '@fibrous/semi-ui-solid';

interface SortableItemProps {
    id: string | number;
    index: number;
    items: TransferDataItem[];
    onRemove: (item: TransferDataItem) => void;
}

// Only the handle drags; the Accessibility plugin gives it a button role and Tab stop for keyboard sorting
function SortableItem(props: SortableItemProps) {
    const { ref, handleRef } = useSortable({
        get id() {
            return props.id;
        },
        get index() {
            return props.index;
        },
    });

    // the rows are keyed by id, so a new selectedData array re-reads the item instead of remounting the row mid-drag
    const item = () => props.items.find((entry) => entry.key === props.id);

    return (
        <div ref={(node) => ref(node)}>
            <Show keyed when={item()}>
                {(current) => (
                    <div class="semi-transfer-item panel-item">
                        <span ref={(node) => handleRef(node)} aria-label={String(current.label)} style={{ 'line-height': '0' }}>
                            <IconHandle class="pane-item-drag-handler" />
                        </span>
                        <div class="panel-item-main" style={{ 'flex-grow': '1' }}>
                            <p style={{ margin: '0 12px' }}>{current.label}</p>
                            <Button theme="borderless" type="primary" onClick={() => props.onRemove(current)} class="panel-item-remove" size="small">
                                删除
                            </Button>
                        </div>
                    </div>
                )}
            </Show>
        </div>
    );
}

function SortableList(props: { items: TransferDataItem[]; onSortEnd: OnSortEnd; onRemove: (item: TransferDataItem) => void }) {
    const onDragEnd: DragDropProviderProps['onDragEnd'] = (event) => {
        const { source } = event.operation;
        if (!event.canceled && isSortable(source) && source.initialIndex !== source.index) {
            props.onSortEnd({ oldIndex: source.initialIndex, newIndex: source.index });
        }
    };

    return (
        <DragDropProvider onDragEnd={onDragEnd}>
            <div style={{ overflow: 'auto', display: 'flex', 'flex-direction': 'column', 'row-gap': '8px' }}>
                <For each={props.items.map((item) => item.key)}>{(id, index) => <SortableItem id={id} index={index()} items={props.items} onRemove={props.onRemove} />}</For>
            </div>
        </DragDropProvider>
    );
}

const dataSource = Array.from({ length: 100 }, (_, i) => ({
    label: `海底捞门店 ${i}`,
    value: i,
    disabled: false,
    key: `key-${i}`,
}));

// the panels read their live objects in the JSX, as in the plain custom panel demo
function renderSourcePanel(panel: SourcePanelProps) {
    return (
        <section class="source-panel">
            <div class="panel-header sp-font">门店列表</div>
            <div class="panel-main">
                <Input aria-label="搜索门店" style={{ width: '454px', margin: '12px 14px' }} prefix={<IconSearch />} onChange={(value) => panel.onSearch(value)} showClear />
                <div class="panel-controls sp-font">
                    <span>待选门店: {panel.filterData.length}</span>
                    <Button onClick={() => panel.onAllClick()} theme="borderless" size="small">
                        {panel.allChecked ? '取消全选' : '全选'}
                    </Button>
                </div>
                <div class="panel-list">
                    <Show when={!panel.loading} fallback={<Spin />}>
                        <Show when={!panel.noMatch} fallback={<div class="empty sp-font">{panel.inputValue ? '无搜索结果' : '暂无内容'}</div>}>
                            <For each={panel.filterData}>
                                {(item) => (
                                    <div class="semi-transfer-item panel-item">
                                        <div class="panel-item-main" style={{ 'flex-grow': '1' }}>
                                            <p style={{ margin: '0 12px' }}>{item.label}</p>
                                            <Button theme="borderless" type="primary" onClick={() => panel.onSelectOrRemove(item)} class="panel-item-remove" size="small">
                                                {panel.selectedItems.has(item.key) ? '删除' : '添加'}
                                            </Button>
                                        </div>
                                    </div>
                                )}
                            </For>
                        </Show>
                    </Show>
                </div>
            </div>
        </section>
    );
}

function renderSelectedPanel(panel: SelectedPanelProps) {
    return (
        <section class="selected-panel">
            <div class="panel-header sp-font">
                <div>已选同步门店: {panel.length}</div>
                <Button theme="borderless" type="primary" onClick={() => panel.onClear()} size="small">
                    清空
                </Button>
            </div>
            <div class="panel-main" style={{ display: 'block' }}>
                <SortableList items={panel.selectedData} onSortEnd={panel.onSortEnd} onRemove={panel.onRemove} />
            </div>
        </section>
    );
}

export default function TransferDemo() {
    return <Transfer defaultValue={[2, 4]} onChange={(values) => console.log(values)} class="component-transfer-demo-custom-panel" renderSourcePanel={renderSourcePanel} renderSelectedPanel={renderSelectedPanel} dataSource={dataSource} />;
}

Tree Transfer

The input type is treeList, and the Tree component is used as a custom rendering list.

The properties of the default tree can be overridden by treeProps(TreeProps). The default properties of the tree on the left are

{
    multiple:true,
    disableStrictly:true,
    leafOnly:true,
    filterTreeNode:true,
    defaultExpandAll:true,
    searchRender:false,
}

The Tree's own search box is hidden by searchRender set to false, and filtering is driven by the search box of Transfer.

总个数:12
  • Asia
  • China
  • Beijing
  • Shanghai
  • Chengdu
  • Japan
  • Osaka
  • North America
  • United States
  • Canada
  • Mexico
  • Cuba
  • 已选个数:1
    Shanghai
    Code
    import { createSignal } from 'solid-js';
    import { Transfer } from '@fibrous/semi-ui-solid';
    
    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',
                },
                {
                    label: 'Mexico',
                    value: 'Mexico',
                    disabled: true,
                    key: '1-2',
                },
                {
                    label: 'Cuba',
                    value: 'Cuba',
                    key: '1-3',
                },
            ],
        },
    ];
    
    export default function TransferDemo() {
        const [value, setValue] = createSignal<Array<string | number>>(['Shanghai']);
    
        return (
            <div style={{ margin: '10px', padding: '10px', width: '600px' }}>
                <Transfer dataSource={treeData} type="treeList" value={value()} onChange={(values) => setValue(values)} />
            </div>
        );
    }

    Tree Transfer with Custom Header Showing Leaf Node Count

    When type is treeList, the SourceHeaderProps argument of renderSourceHeader additionally provides the leafOnlyNum field, which is the number of leaf nodes. This is useful in scenarios such as file selection, where the header can show only the number of files instead of the total count including folders.

    文件总数:7 个文件(总节点数:10)
  • 文件夹 1
  • 文件 1-1
  • 文件 1-2
  • 子文件夹 1
  • 文件 1-1-1
  • 文件 1-1-2
  • 文件夹 2
  • 文件 2-1
  • 文件 2-2
  • 独立文件
  • 已选个数:0
    暂无内容,可从左侧勾选
    Code
    import { createSignal, Show } from 'solid-js';
    import { Transfer, Button, type TransferSourceHeaderProps } from '@fibrous/semi-ui-solid';
    
    const treeData = [
        {
            label: '文件夹 1',
            value: 'folder1',
            key: 'folder1',
            children: [
                {
                    label: '文件 1-1',
                    value: 'file1-1',
                    key: 'file1-1',
                },
                {
                    label: '文件 1-2',
                    value: 'file1-2',
                    key: 'file1-2',
                },
                {
                    label: '子文件夹 1',
                    value: 'subfolder1',
                    key: 'subfolder1',
                    children: [
                        {
                            label: '文件 1-1-1',
                            value: 'file1-1-1',
                            key: 'file1-1-1',
                        },
                        {
                            label: '文件 1-1-2',
                            value: 'file1-1-2',
                            key: 'file1-1-2',
                        },
                    ],
                },
            ],
        },
        {
            label: '文件夹 2',
            value: 'folder2',
            key: 'folder2',
            children: [
                {
                    label: '文件 2-1',
                    value: 'file2-1',
                    key: 'file2-1',
                },
                {
                    label: '文件 2-2',
                    value: 'file2-2',
                    key: 'file2-2',
                },
            ],
        },
        {
            label: '独立文件',
            value: 'file3',
            key: 'file3',
        },
    ];
    
    // the header reads its live object in the JSX, so it renders once and follows the selection
    const renderSourceHeader = (header: TransferSourceHeaderProps) => {
        return (
            <div
                style={{
                    display: 'flex',
                    'justify-content': 'space-between',
                    'align-items': 'center',
                    padding: '8px 12px',
                    'border-bottom': '1px solid var(--semi-color-border)',
                }}
            >
                <span>
                    <strong>文件总数:</strong>
                    {header.leafOnlyNum !== undefined ? header.leafOnlyNum : header.num} 个文件
                    <Show when={header.leafOnlyNum !== undefined}>
                        <span style={{ color: 'var(--semi-color-text-2)', 'margin-left': '8px', 'font-size': '12px' }}>(总节点数:{header.num})</span>
                    </Show>
                </span>
                <Show when={header.showButton}>
                    <Button theme="borderless" type="tertiary" size="small" onClick={() => header.onAllClick()}>
                        {header.allChecked ? '取消全选' : '全选'}
                    </Button>
                </Show>
            </div>
        );
    };
    
    export default function TransferDemo() {
        const [value, setValue] = createSignal<Array<string | number>>([]);
    
        return <Transfer style={{ width: '600px' }} dataSource={treeData} type="treeList" value={value()} onChange={(values) => setValue(values)} renderSourceHeader={renderSourceHeader} />;
    }

    Accessibility

    ARIA

    • The container of the search box is a native search element (the search landmark), named from the locale
    • When the root element has aria-label or aria-labelledby and no role, it gets role group
    • Add role list to the option list on the left and listitem to its options; in groupList every group is a listitem of the outer list, and its title names the nested list that holds the options
    • Add role list to the selected list on the right, add role listitem to the selected item; custom rendered items on either side are wrapped in an element with role listitem

    Keyboard

    • The default filter ignores case
    • When disabled, the two lists are Tab stops so the keyboard can scroll their content
    • With draggable, the drag handle is a native button with a name: Tab focuses the handle, Space picks up, the arrow keys move, Space drops; the sort is announced to screen readers. Mouse dragging is unchanged
    • Until the dnd-kit needed for sorting has loaded, the handle is a placeholder with aria-disabled="true"

    API Reference

    Transfer

    Props

    PropertyDescriptionTypeDefault
    dataSourceData source: an array of items for list, an array of groups for groupList, or tree data with children for treeList. The data is derived from the array reference, so an in-place change is not seen; pass a new array.DataSource[]
    defaultValueThe values selected initially, read once when the Transfer is created.TypeArray<string | number>[]
    disabledWhether the whole transfer is disabled.boolean-
    draggableEnables drag sorting of the selected items on the right. After a sort, the order of the array passed to onChange changes.boolean-
    emptyContentCustom empty-state content: left when the left list has no data, right when nothing is selected, search when the search matches nothing. A null entry hides that empty state; a missing one shows the locale text.EmptyContent{}
    filterFiltering logic. false hides the search box; a function replaces the default case-insensitive label match and returns true to keep an item. For a custom search in treeList, set it to true and pass the matching function as filterTreeNode in treeProps.Typeboolean | ((searchValue: string, item: DataItem) => boolean)-
    inputPropsProps passed to the search Input, see Input for the options. value and onChange belong to Transfer; to search from outside, call the search method on the ref.InputProps-
    loadingWhether the options on the left are loading; a Spin replaces the list while it is.boolean-
    onChangeCalled when the selection changes, including after a drag sort, with the array of values and the array of their items.Type(values: Array<string | number>, items: Array<DataItem>) => void-
    onDeselectCalled when an item is unchecked, with that item.Type(item: DataItem) => void-
    onSearchCalled when the text of the search box changes, with the search value. A search driven by the ref's search method does not call it.Type(searchValue: string) => void-
    onSelectCalled when an item is checked, with that item.Type(item: DataItem) => void-
    paginationPaging of the left panel, for list and groupList only. onPageChange lives inside the object.PaginationProps-
    refReceives the root div element, typed as TransferElement; the element carries the search method.TypeRef<TransferElement>-
    renderLabelRenders the content of each item in both lists and of each node in treeList; it receives the data item. Without it the label shows. The path with showPath stays label text.Type(item: DataItem) => JSX.Element-
    renderSelectedHeaderReplaces the header of the right panel; called once with a live object (SelectedHeaderProps).Type(headProps: SelectedHeaderProps) => JSX.Element-
    renderSelectedItemRenders one selected item on the right; called once per item. The argument is the item and also carries onRemove; with draggable it also carries sortableHandle. With treeList and showPath, fullPath holds the whole path.Type(item: RenderSelectedItemProps) => JSX.Element-
    renderSelectedPanelReplaces the whole right panel; called once with a live object (SelectedPanelProps). Read its fields in the JSX.Type(selectedPanelProps: SelectedPanelProps) => JSX.Element-
    renderSourceHeaderReplaces the header of the left panel; called once with a live object (SourceHeaderProps). leafOnlyNum is provided for treeList only.Type(headProps: SourceHeaderProps) => JSX.Element-
    renderSourceItemRenders one candidate on the left; called once per item. The argument is the item and also carries onChange and checked; checked is a getter, read it in the returned JSX.Type(item: RenderSourceItemProps) => JSX.Element-
    renderSourcePanelReplaces the whole left panel; called once with a live object (SourcePanelProps). Read its fields in the JSX: destructuring takes only the values of the first render.Type(sourcePanelProps: SourcePanelProps) => JSX.Element-
    showPathtreeList only: whether the selected items on the right show their path from the root.booleanfalse
    tabIndexSet on the search box, including a value injected by a wrapping Tooltip; the root element is not a focus target.number | string-
    treePropstreeList only: props passed to the Tree on the left, overriding the defaults multiple, disableStrictly, leafOnly, filterTreeNode, defaultExpandAll and searchRender (false). value, treeData, keyMaps, treeDataSimpleJson, onChangeWithObject, onChange and ref belong to Transfer and are ignored.TypeOmit<TreeProps, 'value' | 'ref' | 'onChange' | 'keyMaps' | 'treeData' | 'treeDataSimpleJson' | 'onChangeWithObject'>-
    typeTransfer type: list, groupList or treeList.Type"list"
    valueArray of the values of the selected items. Passing it makes the component controlled and its order is the order on the right; undefined returns to uncontrolled.TypeArray<string | number>-
    virtualizeVirtualizes the selected list on the right, only with the default right panel and draggable off. A numeric height applies in pixels; any other value is measured from the remaining panel height. itemSize is the height of each row.VirtualizeProps-

    Methods

    MethodDescriptionType
    searchSearches as if the value were typed into the search box, and puts the value there; onSearch is not called.Type(value: string) => void

    class, style, aria-* and other native attributes and events land on the root div. When the root has aria-label or aria-labelledby and no role, it gets role="group". ref receives the root element; attributes of the search box are passed with inputProps, and attributes of the left Tree in treeList with treeProps.

    Item Interface

    Data items can also carry any custom fields; they appear unchanged on the arguments of renderSourceItem and renderSelectedItem.

    PropertyDescriptionTypeDefault
    disabledWhether the item is disabledbooleanfalse
    keyRequired, unique identifier of each option, duplicates are not allowedstring | number
    labelText of the option, matched by the default search; use renderLabel for custom contentstring | number
    fullPathWhen type="treeList" and showPath is true, the array of nodes from the root node to the current node itselfArray<Item>
    valueThe value the option representsstring | number

    GroupItem Interface

    A group is not an item itself, it only has the following properties

    propsdescriptiondata typedefault
    childrenElements of the grouparray<Item>
    titleGroup Namestring

    TreeItem Interface

    TreeItem inherits all the properties of Item

    propsdescriptiondata typedefault
    childrenChildren Itemsarray<TreeItem>

    VirtualizeProps Interface

    propsdescriptiondata typedefault
    heightHeight of the virtualized list. If it is a number, it takes effect directly. If it is a string (e.g. 100%), it adapts to remaining heightnumber | string
    widthWidth of the virtualized listnumber | string
    itemSizeFixed height of each item rownumber

    PaginationProps Interface

    propsdescriptiondata typedefault
    currentPageCurrent page number (controlled mode)number
    defaultCurrentPageDefault current page number (uncontrolled mode)number1
    pageSizeNumber of items per pagenumber10
    onPageChangeCallback when page changes(currentPage: number) => void

    Methods

    Methods on the root element. Get the root element through ref; its type is TransferElement.

    let transfer: TransferElement | undefined;
    
    <Transfer ref={(element) => (transfer = element)} />;
    
    transfer?.search('选项名称 1');

    Design Tokens

    These are the component's internal Sass variables and cannot be overridden. To customize styles, override the --semi-* CSS variables (see the Design Token page) or write unlayered CSS rules.
    Sass variableValueUsage
    $transition_duration_transfer_item-bgvar(--semi-transition_duration-none)穿梭框条目-背景色-动画持续时间
    $transition_function_transfer_item-bgvar(--semi-transition_function-easeIn)穿梭框条目-背景色-过渡曲线
    $transition_delay_transfer_item-bgvar(--semi-transition_delay-none)穿梭框条目-背景色-延迟时间
    $color-transfer-bgvar(--semi-color-bg-1)穿梭框背景颜色 - 默认
    $color-transfer-bordervar(--semi-color-border)穿梭框描边颜色 - 默认
    $color-transfer_header-textvar(--semi-color-text-2)穿梭框 header 文字颜色
    $color-transfer_empty-textvar(--semi-color-text-2)穿梭框空状态文字颜色
    $color-transfer_group_title-textvar(--semi-color-text-2)穿梭框分组标题文字颜色
    $color-transfer_disabled-textvar(--semi-color-disabled-text)穿梭框禁用文本文字颜色
    $color-transfer_close_icon-iconvar(--semi-color-text-2)穿梭框关闭按钮颜色
    $color-transfer_item-textvar(--semi-color-text-1)穿梭框常规文字颜色
    $color-transfer_selected_item-textvar(--semi-color-text-0)穿梭框选中条目文字颜色
    $color-transfer_item-bg-hovervar(--semi-color-fill-0)穿梭框条目背景色 - 悬浮
    $color-transfer_item-bg-activevar(--semi-color-fill-1)穿梭框条目背景色 - 按下
    $width-transfer-border1px穿梭框描边宽度
    $width-transfer-minWidth402px穿梭框整体最小宽度
    $height-transfer400px穿梭框高度
    $height-transfer_header24px穿梭框 header 高度
    $height-transfer_item-minHeight36px穿梭框条目最小高度
    $width-transfer_item_close-icon$width-icon-small穿梭框选中条目删除按钮宽度
    $width-transfer_left50%穿梭框左侧面板宽度
    $width-transfer_right50%穿梭框左侧面板宽度
    $height-transfer_left_empty36px穿梭框空状态高度
    $width-transfer_left-border$border-thickness-control穿梭框双侧面板分割线宽度
    $height-transfer_right_header32px穿梭框 header 高度
    $height-transfer_right_empty100%穿梭框右侧空状态宽度
    $width-transfer_empty100%穿梭框整体空状态宽度
    $height-transfer_group_title28px穿梭框分组标题高度
    $spacing-transfer_header-marginTop12px穿梭框 header 顶部外边距
    $spacing-transfer_header-marginRight12px穿梭框 header 右侧外边距
    $spacing-transfer_header-marginBottom8px穿梭框 header 底部外边距
    $spacing-transfer_header-marginLeft12px穿梭框 header 左侧外边距
    $spacing-transfer_header_all-marginLeft$spacing-base穿梭框全选按钮左侧外边距
    $spacing-transfer_item-paddingTop$spacing-tight穿梭框条目顶部内边距
    $spacing-transfer_item-paddingRight9px穿梭框条目右侧内边距
    $spacing-transfer_item-paddingBottom$spacing-tight穿梭框条目底部内边距
    $spacing-transfer_item-paddingLeft$spacing-base-tight穿梭框条目左侧内边距
    $spacing-transfer_right_header-marginTop$spacing-base-tight穿梭框右侧面板 header 顶部外边距
    $spacing-transfer_right_header-marginBottom0穿梭框右侧面板 header 底部外边距
    $spacing-transfer_right_item_drag_handler-marginRight4px穿梭框拖动图标右侧外边距
    $spacing-transfer_filter-marginTop12px穿梭框搜索顶部外边距
    $spacing-transfer_filter-marginRight12px穿梭框搜索右侧外边距
    $spacing-transfer_filter-marginBottom0穿梭框搜索底部外边距
    $spacing-transfer_filter-marginLeft12px穿梭框搜索左侧外边距
    $spacing-transfer_group_title-paddingLeft$spacing-base-tight穿梭框分组标题左侧内边距
    $radius-transfervar(--semi-border-radius-medium)穿梭框圆角
    $font-transfer_header_all-fontWeight600穿梭框字重