展示类

列表

基础列表组件

代码演示

如何引入

import { List } from '@fibrous/semi-ui-react';

基本用法

列表的基本用法。可以通过 size 设置尺寸,支持large, default, small。可设置 header 和 footer,来自定义列表头部和尾部。

Default Size

Header
  • 从明天起,做一个幸福的人
  • 喂马,劈柴,周游世界
  • 从明天起,关心粮食和蔬菜
  • 我有一所房子,面朝大海,春暖花开

Small Size

Header
  • 从明天起,做一个幸福的人
  • 喂马,劈柴,周游世界
  • 从明天起,关心粮食和蔬菜
  • 我有一所房子,面朝大海,春暖花开

Large Size

Header
  • 从明天起,做一个幸福的人
  • 喂马,劈柴,周游世界
  • 从明天起,关心粮食和蔬菜
  • 我有一所房子,面朝大海,春暖花开
代码
import { List } from '@fibrous/semi-ui-react';

export default function ListDemo() {
    const data = ['从明天起,做一个幸福的人', '喂马,劈柴,周游世界', '从明天起,关心粮食和蔬菜', '我有一所房子,面朝大海,春暖花开'];

    return (
        <div>
            <div style={{ marginRight: 16 }}>
                <h3 style={{ marginBottom: 16 }}>Default Size</h3>
                <List header={<div>Header</div>} footer={<div>Footer</div>} bordered dataSource={data} renderItem={(item) => <List.Item>{item}</List.Item>} />
            </div>
            <div style={{ marginRight: 16 }}>
                <h3 style={{ margin: '16px 0' }}>Small Size</h3>
                <List size="small" header={<div>Header</div>} footer={<div>Footer</div>} bordered dataSource={data} renderItem={(item) => <List.Item>{item}</List.Item>} />
            </div>
            <div style={{ marginRight: 16 }}>
                <h3 style={{ margin: '16px 0' }}>Large Size</h3>
                <List size="large" header={<div>Header</div>} footer={<div>Footer</div>} bordered dataSource={data} renderItem={(item) => <List.Item>{item}</List.Item>} />
            </div>
        </div>
    );
}

模板用法

列表的 List.Item 内置了简单的结构包含:header,main 和 extra 。其中 header 和 main 的对齐方式可以通过 align 属性设置,支持 flex-start(默认), flex-end, center, baseline, 和 stretch 。

  • SE
    示例标题

    Semi Design 是由抖音前端团队与 UED 团队共同设计开发并维护的设计系统。设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。

  • SE
    示例标题

    Semi Design 是由抖音前端团队与 UED 团队共同设计开发并维护的设计系统。设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。

  • SE
    示例标题

    Semi Design 以用户中心、内容优先、设计人性化的设计系统,打造一致、好看、好用、高效的用户体验。

代码
import type { CSSProperties } from 'react';
import { List, Avatar, ButtonGroup, Button } from '@fibrous/semi-ui-react';

const text = 'Semi Design 是由抖音前端团队与 UED 团队共同设计开发并维护的设计系统。设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。';

const data: { text: string; style: CSSProperties }[] = [
    {
        text,
        style: { color: 'var(--semi-color-text-2)', margin: '4px 0', width: 420, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
    },
    { text, style: { color: 'var(--semi-color-text-2)', margin: '4px 0', width: 500 } },
    {
        text: 'Semi Design 以用户中心、内容优先、设计人性化的设计系统,打造一致、好看、好用、高效的用户体验。',
        style: { color: 'var(--semi-color-text-2)', margin: '4px 0', width: 500 },
    },
];

export default function ListDemo() {
    return (
        <div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}>
            <List
                dataSource={data}
                renderItem={(item) => (
                    <List.Item
                        header={<Avatar color="blue">SE</Avatar>}
                        main={
                            <div>
                                <span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>示例标题</span>
                                <p style={item.style}>{item.text}</p>
                            </div>
                        }
                        extra={
                            <ButtonGroup theme="borderless">
                                <Button>编辑</Button>
                                <Button>更多</Button>
                            </ButtonGroup>
                        }
                    />
                )}
            />
        </div>
    );
}

布局

通过 layout 属性可以设置列表的布局,支持vertical(默认)和horizontal。

  • SE
    Semi Design Title 1

    Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。

  • SE
    Semi Design Title 2

    Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。

  • SE
    Semi Design Title 3

    Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。

代码
import { List, Avatar, type AvatarColor } from '@fibrous/semi-ui-react';

interface Item {
    title: string;
    color: AvatarColor;
}

export default function ListDemo() {
    const data: Item[] = [
        {
            title: 'Semi Design Title 1',
            color: 'light-blue',
        },
        {
            title: 'Semi Design Title 2',
            color: 'grey',
        },
        {
            title: 'Semi Design Title 3',
            color: 'light-green',
        },
    ];

    return (
        <div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}>
            <List
                dataSource={data}
                layout="horizontal"
                renderItem={(item) => (
                    <List.Item
                        header={<Avatar color={item.color}>SE</Avatar>}
                        main={
                            <div>
                                <span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</span>
                                <p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}>Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
                            </div>
                        }
                    />
                )}
            />
        </div>
    );
}

栅格列表

通过 grid 属性可以实现栅格列表,span 可设置每项的占格数,gutter可设置栅格间隔。

  • Semi UI

    满意度
    反馈数124
  • Semi DSM

    满意度
    反馈数108
  • Semi D2C

    满意度
    反馈数244
  • 代码
    import { List, Descriptions, ButtonGroup, Rating, Button } from '@fibrous/semi-ui-react';
    
    export default function ListDemo() {
        const data = [
            {
                title: 'Semi UI',
                rating: 4.5,
                feedbacks: 124,
            },
            {
                title: 'Semi DSM',
                rating: 4,
                feedbacks: 108,
            },
            {
                title: 'Semi D2C',
                rating: 4.5,
                feedbacks: 244,
            },
        ];
    
        const style = {
            border: '1px solid var(--semi-color-border)',
            backgroundColor: 'var(--semi-color-bg-2)',
            borderRadius: '3px',
            paddingLeft: '20px',
        };
    
        return (
            <div>
                <List
                    grid={{
                        gutter: 12,
                        span: 6,
                    }}
                    dataSource={data}
                    renderItem={(item) => (
                        <List.Item style={style}>
                            <div>
                                <h3 style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</h3>
                                <Descriptions
                                    align="center"
                                    size="small"
                                    row
                                    data={[
                                        { key: '满意度', value: <Rating allowHalf size="small" value={item.rating} /> },
                                        { key: '反馈数', value: item.feedbacks },
                                    ]}
                                />
                                <div style={{ margin: '12px 0', display: 'flex', justifyContent: 'flex-end' }}>
                                    <ButtonGroup theme="borderless" style={{ marginTop: 8 }}>
                                        <Button>编辑</Button>
                                        <Button>更多</Button>
                                    </ButtonGroup>
                                </div>
                            </div>
                        </List.Item>
                    )}
                />
            </div>
        );
    }

    响应式的栅格列表

    响应式的栅格列表。响应尺寸与 Grid 保持一致。

  • 审核管理平台

    满意度
    反馈数124
  • 扁鹊

    满意度
    反馈数108
  • 直播审核平台

    满意度
    反馈数244
  • 抖音安全测试

    满意度
    反馈数189
  • 内容平台

    满意度
    反馈数128
  • 策略平台

    满意度
    反馈数156
  • 代码
    import { List, Descriptions, Rating, Button, ButtonGroup } from '@fibrous/semi-ui-react';
    
    export default function ListDemo() {
        const data = [
            {
                title: '审核管理平台',
                rating: 4.5,
                feedbacks: 124,
            },
            {
                title: '扁鹊',
                rating: 4,
                feedbacks: 108,
            },
            {
                title: '直播审核平台',
                rating: 3.5,
                feedbacks: 244,
            },
            {
                title: '抖音安全测试',
                feedbacks: 189,
            },
            {
                title: '内容平台',
                rating: 3,
                feedbacks: 128,
            },
            {
                title: '策略平台',
                rating: 4,
                feedbacks: 156,
            },
        ];
    
        const style = {
            border: '1px solid var(--semi-color-border)',
            backgroundColor: 'var(--semi-color-bg-2)',
            borderRadius: '3px',
            paddingLeft: '20px',
            margin: '8px 2px',
        };
    
        return (
            <div>
                <List
                    grid={{
                        gutter: 12,
                        xs: 0,
                        sm: 0,
                        md: 12,
                        lg: 8,
                        xl: 8,
                        xxl: 6,
                    }}
                    dataSource={data}
                    renderItem={(item) => (
                        <List.Item style={style}>
                            <div>
                                <h3 style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</h3>
                                <Descriptions
                                    align="center"
                                    size="small"
                                    row
                                    data={[
                                        { key: '满意度', value: <Rating allowHalf size="small" value={item.rating} /> },
                                        { key: '反馈数', value: item.feedbacks },
                                    ]}
                                />
                                <div style={{ margin: '12px 0', display: 'flex', justifyContent: 'flex-end' }}>
                                    <ButtonGroup theme="borderless" style={{ marginTop: 8 }}>
                                        <Button>编辑</Button>
                                        <Button>更多</Button>
                                    </ButtonGroup>
                                </div>
                            </div>
                        </List.Item>
                    )}
                />
            </div>
        );
    }

    加载更多

    可通过 loadMore 属性实现加载更多的功能。

    代码
    import { useState, useEffect } from 'react';
    import { List, Skeleton, Button, Avatar, type AvatarColor } from '@fibrous/semi-ui-react';
    
    interface Book {
        color?: AvatarColor;
        title?: string;
        loading: boolean;
    }
    
    const count = 3;
    const data: Book[] = [];
    for (let i = 0; i < 40; i++) {
        data.push({
            color: 'grey',
            title: `Semi Design Title ${i}`,
            loading: false,
        });
    }
    const placeholders: Book[] = [0, 1, 2].map(() => ({ loading: true }));
    
    const fetchData = (page: number) => {
        return new Promise<Book[]>((res) => {
            setTimeout(() => {
                res(data.slice(page * count, page * count + count));
            }, 1000);
        });
    };
    
    export default function ListDemo() {
        const [page, setPage] = useState(0);
        const [loading, setLoading] = useState(true);
        const [dataSource, setDataSource] = useState<Book[]>([]);
        const [noMore, setNoMore] = useState(false);
        const list = loading ? [...dataSource, ...placeholders] : dataSource;
    
        useEffect(() => {
            let ignore = false;
            void fetchData(page).then((newDataSource) => {
                if (!ignore) {
                    setDataSource((prevData) => [...prevData, ...newDataSource]);
                    setLoading(false);
                    setNoMore(!newDataSource.length);
                }
            });
            return () => {
                ignore = true;
            };
        }, [page]);
    
        const onLoadMore = () => {
            setLoading(true);
            setPage(page + 1);
        };
    
        const loadMore =
            !loading && !noMore ? (
                <div
                    style={{
                        textAlign: 'center',
                        marginTop: 12,
                        height: 32,
                        lineHeight: '32px',
                    }}
                >
                    <Button onClick={onLoadMore}>显示更多</Button>
                </div>
            ) : null;
    
        const placeholder = (
            <div
                style={{
                    display: 'flex',
                    alignItems: 'flex-start',
                    padding: 12,
                    borderBottom: '1px solid var(--semi-color-border)',
                }}
            >
                <Skeleton.Avatar style={{ marginRight: 12 }} />
                <div>
                    <Skeleton.Title style={{ width: 120, marginBottom: 12, marginTop: 12 }} />
                    <Skeleton.Paragraph style={{ width: 600 }} rows={2} />
                </div>
            </div>
        );
        return (
            <List
                loading={loading}
                loadMore={loadMore}
                dataSource={list}
                renderItem={(item) => (
                    <Skeleton placeholder={placeholder} loading={item.loading}>
                        <List.Item
                            header={<Avatar color={item.color}>SE</Avatar>}
                            main={
                                <div>
                                    <span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</span>
                                    <p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}>Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
                                </div>
                            }
                        />
                    </Skeleton>
                )}
            />
        );
    }

    滚动加载

    可以通过集成 react-infinite-scroller 来实现滚动加载的列表。交互建议符合 semi 交互设计规范,这里采用三次滚加载后出现 load more 按钮的形式。

    暂无数据
    代码
    import { useState, useEffect } from 'react';
    import InfiniteScroll from 'react-infinite-scroller';
    import { List, Button, Avatar, Spin } from '@fibrous/semi-ui-react';
    
    interface Book {
        title: string;
    }
    
    const count = 5;
    const dataList: Book[] = Array.from({ length: 100 }, (_, i) => ({ title: `Semi Design Title ${i}` }));
    
    const fetchData = (page: number) => {
        return new Promise<Book[]>((res) => {
            setTimeout(() => {
                res(dataList.slice(page * count, page * count + count));
            }, 1000);
        });
    };
    
    export default function ListDemo() {
        const [page, setPage] = useState(0);
        const [loading, setLoading] = useState(true);
        const [dataSource, setDataSource] = useState<Book[]>([]);
        const [hasMore, setHasMore] = useState(true);
    
        useEffect(() => {
            let ignore = false;
            void fetchData(page).then((newDataSource) => {
                if (!ignore) {
                    setDataSource((prevData) => [...prevData, ...newDataSource]);
                    setLoading(false);
                    setHasMore(!!newDataSource.length);
                }
            });
            return () => {
                ignore = true;
            };
        }, [page]);
    
        const onLoadMore = () => {
            setLoading(true);
            setPage(page + 1);
        };
    
        const showLoadMore = (page + 1) % 4 === 0;
        const loadMore =
            !loading && hasMore && showLoadMore ? (
                <div
                    style={{
                        textAlign: 'center',
                        marginTop: 12,
                        height: 32,
                        lineHeight: '32px',
                    }}
                >
                    <Button onClick={onLoadMore}>显示更多</Button>
                </div>
            ) : null;
    
        return (
            <section className="light-scrollbar" tabIndex={0} aria-label="可滚动列表" style={{ height: 420, overflow: 'auto', border: '1px solid var(--semi-color-border)', padding: 10 }}>
                <InfiniteScroll initialLoad={false} pageStart={0} threshold={20} loadMore={onLoadMore} hasMore={!loading && hasMore && !showLoadMore} useWindow={false}>
                    <List
                        loadMore={loadMore}
                        dataSource={dataSource}
                        renderItem={(item) => (
                            <List.Item
                                header={<Avatar color="grey">SE</Avatar>}
                                main={
                                    <div>
                                        <span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</span>
                                        <p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}>Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
                                    </div>
                                }
                            />
                        )}
                    />
                    {loading && hasMore && (
                        <div style={{ textAlign: 'center' }}>
                            <Spin />
                        </div>
                    )}
                </InfiniteScroll>
            </section>
        );
    }

    滚动加载无限长列表

    可以通过集成 TanStack Virtual 实现滚动加载无限长列表,带有虚拟化(virtualization)功能,只渲染可见的行,能够提高数据量大时候长列表的性能。

    代码
    import { useEffect, useRef, useState } from 'react';
    import { useVirtualizer } from '@tanstack/react-virtual';
    import { Avatar, List, Spin } from '@fibrous/semi-ui-react';
    
    interface Book {
        title: string;
    }
    
    const rowCount = 50;
    const rowHeight = 118;
    // Rows fetched ahead of the window so they arrive before they scroll into view
    const prefetch = 15;
    const dataList: Book[] = Array.from({ length: rowCount }, (_, i) => ({ title: `Semi Design Title ${i}` }));
    
    export default function ListDemo() {
        // useVirtualizer returns one mutable instance for the component's life; React Compiler's memoized reads of it would go stale
        'use no memo';
    
        const scrollRef = useRef<HTMLElement>(null);
        const [loaded, setLoaded] = useState<ReadonlySet<number>>(() => new Set());
        // A requested index is never fetched again, even while pending
        const requested = useRef(new Set<number>());
        const virtualizer = useVirtualizer({ count: rowCount, getScrollElement: () => scrollRef.current, estimateSize: () => rowHeight, overscan: 2 });
        const items = virtualizer.getVirtualItems();
        const start = items[0]?.index;
        const stop = items.at(-1)?.index;
    
        useEffect(() => {
            if (start === undefined || stop === undefined) return;
            const wanted: number[] = [];
            for (let i = start; i <= Math.min(stop + prefetch, rowCount - 1); i++) {
                if (!requested.current.has(i)) {
                    requested.current.add(i);
                    wanted.push(i);
                }
            }
            if (!wanted.length) return;
            setTimeout(() => setLoaded((prev) => new Set([...prev, ...wanted])), 1000);
        }, [start, stop]);
    
        return (
            <div style={{ border: '1px solid var(--semi-color-border)', padding: 10 }}>
                <section ref={scrollRef} aria-label="虚拟滚动列表" tabIndex={0} style={{ height: '500px', overflow: 'auto' }}>
                    <List style={{ position: 'relative', height: virtualizer.getTotalSize() }} emptyContent={<Spin />}>
                        {items
                            .filter((item) => loaded.has(item.index))
                            .map((item) => (
                                <List.Item
                                    key={item.index}
                                    style={{ position: 'absolute', insetInline: 0, top: item.start, height: rowHeight }}
                                    header={<Avatar color="grey">SE</Avatar>}
                                    main={
                                        <div>
                                            <span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{dataList[item.index].title}</span>
                                            <p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}>Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
                                        </div>
                                    }
                                />
                            ))}
                    </List>
                </section>
            </div>
        );
    }

    拖拽排序

    使用 dnd-kit 可轻松实现拖拽排序。

    • SE
      Semi Design Title 1

      Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。

    • SE
      Semi Design Title 2

      Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。

    • SE
      Semi Design Title 3

      Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。

    • SE
      Semi Design Title 4

      Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。

    • SE
      Semi Design Title 5

      Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。

    代码
    import { useState, type ComponentProps, type CSSProperties } from 'react';
    import { RestrictToVerticalAxis } from '@dnd-kit/abstract/modifiers';
    import { Accessibility } from '@dnd-kit/dom';
    import { move } from '@dnd-kit/helpers';
    import { DragDropProvider } from '@dnd-kit/react';
    import { useSortable } from '@dnd-kit/react/sortable';
    import { List, Avatar, type AvatarColor, type ListItemProps } from '@fibrous/semi-ui-react';
    
    interface Item {
        id: number;
        title: string;
        color: AvatarColor;
    }
    
    const data: Item[] = [
        {
            id: 1,
            title: 'Semi Design Title 1',
            color: 'red',
        },
        {
            id: 2,
            title: 'Semi Design Title 2',
            color: 'grey',
        },
        {
            id: 3,
            title: 'Semi Design Title 3',
            color: 'light-green',
        },
        {
            id: 4,
            title: 'Semi Design Title 4',
            color: 'light-blue',
        },
        {
            id: 5,
            title: 'Semi Design Title 5',
            color: 'pink',
        },
    ];
    
    interface SortableListItemProps extends Omit<ListItemProps, 'id' | 'title' | 'color'> {
        id: number;
        index: number;
        title: string;
        color: AvatarColor;
    }
    
    function ListItem({ id, index, title, color, ...props }: SortableListItemProps) {
        const { ref, isDragging } = useSortable({ id, index });
    
        const styles: CSSProperties = {
            ...props.style,
            border: '1px solid var(--semi-color-border)',
            marginBottom: 12,
            cursor: 'grabbing',
            ...(isDragging ? { zIndex: 999, position: 'relative', backgroundColor: 'var(--semi-color-bg-0)' } : {}),
        };
    
        // The li itself is dragged, so items stay direct children; no Accessibility plugin, so no button role or Tab stop
        // (pointer dragging only)
        return <List.Item {...props} ref={ref} style={styles} className={isDragging ? 'isDragging' : undefined} />;
    }
    
    function renderDraggable(item: Item, index: number) {
        return (
            <ListItem
                {...item}
                // dnd-kit reorders li nodes mid-drag; an index key would give a moved node another item's data
                key={item.id}
                index={index}
                header={<Avatar color={item.color}>SE</Avatar>}
                main={
                    <div>
                        <span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</span>
                        <p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}>Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
                    </div>
                }
            />
        );
    }
    
    export default function ListDemo() {
        const [listItems, setListItems] = useState(data);
    
        const handleDragEnd: ComponentProps<typeof DragDropProvider>['onDragEnd'] = (event) => {
            if (!event.canceled) {
                setListItems((items) => move(items, event));
            }
        };
    
        return (
            <div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}>
                <DragDropProvider plugins={(defaults) => defaults.filter((plugin) => plugin !== Accessibility)} modifiers={[RestrictToVerticalAxis]} onDragEnd={handleDragEnd}>
                    <List dataSource={listItems} renderItem={renderDraggable} />
                </DragDropProvider>
            </div>
        );
    }

    带分页器

    你可以组合使用 Pagination, 实现一个分页的 List

    • 围城
    • 平凡的世界(全三册)
    • 三体(全集)
    • 雪中悍刀行(全集)
    • 1/3
    代码
    import { useState } from 'react';
    import { List, Pagination } from '@fibrous/semi-ui-react';
    
    export default function ListDemo() {
        const data = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚', '月亮与六便士', '沉默的大多数'];
    
        const [page, onPageChange] = useState(1);
    
        let pageSize = 4;
    
        const getData = (page: number) => {
            let start = (page - 1) * pageSize;
            let end = page * pageSize;
            return data.slice(start, end);
        };
    
        return (
            <div>
                <div style={{ marginRight: 16, width: 280, display: 'flex', flexWrap: 'wrap' }}>
                    <List dataSource={getData(page)} split={false} size="small" className="component-list-demo-booklist" style={{ border: '1px solid var(--semi-color-border)', flexBasis: '100%', flexShrink: 0 }} renderItem={(item) => <List.Item className="list-item">{item}</List.Item>} />
                    <Pagination size="small" style={{ width: '100%', flexBasis: '100%', justifyContent: 'center' }} pageSize={pageSize} total={data.length} currentPage={page} onChange={(cPage) => onPageChange(cPage)} />
                </div>
            </div>
        );
    }

    带筛选器

    你可以通过组装 Input 使用,实现对 List 列表的筛选

    • 围城
    • 平凡的世界(全三册)
    • 三体(全集)
    • 雪中悍刀行(全集)
    • 撒哈拉的故事
    • 明朝那些事
    • 一禅小和尚
    • 沙丘
    • 被讨厌的勇气
    • 罪与罚
    代码
    import { useState } from 'react';
    import { IconSearch } from '@fibrous/semi-icons-react';
    import { List, Input } from '@fibrous/semi-ui-react';
    
    export default function ListDemo() {
        const data = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚'];
    
        const [list, setList] = useState(data);
    
        const onSearch = (string?: string) => {
            let newList: string[];
            if (string) {
                newList = data.filter((item) => item.includes(string));
            } else {
                newList = data;
            }
            setList(newList);
        };
    
        return (
            <div>
                <div
                    style={{
                        marginRight: 16,
                        width: 280,
                        display: 'flex',
                        flexWrap: 'wrap',
                        border: '1px solid var(--semi-color-border)',
                    }}
                >
                    <List
                        className="component-list-demo-booklist"
                        dataSource={list}
                        split={false}
                        header={<Input onCompositionEnd={(v) => onSearch((v.target as HTMLInputElement).value)} onChange={(v) => (!v ? onSearch() : null)} placeholder="搜索" prefix={<IconSearch />} />}
                        size="small"
                        style={{ flexBasis: '100%', flexShrink: 0, borderBottom: '1px solid var(--semi-color-border)' }}
                        renderItem={(item) => <List.Item className="list-item">{item}</List.Item>}
                    />
                </div>
            </div>
        );
    }

    添加删除项

    • 围城
    • 平凡的世界(全三册)
    • 三体(全集)
    • 雪中悍刀行(全集)
    • 撒哈拉的故事
    • 明朝那些事
    • 一禅小和尚
    • 沙丘
    新增书籍
    代码
    import { useState } from 'react';
    import { IconMinusCircle, IconPlusCircle } from '@fibrous/semi-icons-react';
    import { List, Button } from '@fibrous/semi-ui-react';
    
    export default function ListDemo() {
        const data = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚', '月亮与六便士', '沉默的大多数', '第一人称单数'];
    
        const [list, setList] = useState(data.slice(0, 8));
    
        const updateList = (item?: string) => {
            let newList: string[];
            if (item) {
                newList = list.filter((i) => item !== i);
            } else {
                newList = list.concat(data.slice(list.length, list.length + 1));
            }
            setList(newList);
        };
    
        return (
            <div>
                <div
                    style={{
                        marginRight: 16,
                        width: 280,
                        display: 'flex',
                        flexWrap: 'wrap',
                        border: '1px solid var(--semi-color-border)',
                    }}
                >
                    <List
                        className="component-list-demo-booklist"
                        dataSource={list}
                        split={false}
                        size="small"
                        style={{ flexBasis: '100%', flexShrink: 0, borderBottom: '1px solid var(--semi-color-border)' }}
                        renderItem={(item) => (
                            <li style={{ margin: 4 }} className="list-item">
                                <Button type="danger" theme="borderless" aria-label={`删除${item}`} icon={<IconMinusCircle />} onClick={() => updateList(item)} style={{ marginRight: 4 }} />
                                {item}
                            </li>
                        )}
                    />
                    <div style={{ margin: 4, fontSize: 14 }}>
                        <Button theme="borderless" aria-label="新增书籍" icon={<IconPlusCircle />} onClick={() => updateList()} style={{ marginRight: 4, color: 'var(--semi-color-info)' }} />
                        新增书籍
                    </div>
                </div>
            </div>
        );
    }

    单选或多选

    你可以通过组合使用 Radio 或 Checkbox 将 List 增强为一个列表选择器

    • 围城
    • 平凡的世界(全三册)
    • 三体(全集)
    • 雪中悍刀行(全集)
    • 撒哈拉的故事
    • 明朝那些事
    • 一禅小和尚
    • 沙丘
    代码
    import { useState } from 'react';
    import { List, Checkbox, Radio, RadioGroup, CheckboxGroup, type CheckboxValue } from '@fibrous/semi-ui-react';
    
    const data = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚', '月亮与六便士', '沉默的大多数', '第一人称单数'];
    
    const dataSource = data.slice(0, 8);
    
    export default function ListDemo() {
        const [checkboxVal, setCV] = useState<CheckboxValue[]>([data[0]]);
        const [radioVal, setRV] = useState(data[0]);
    
        return (
            <div style={{ display: 'flex' }}>
                <div style={{ marginRight: 16, width: 280, display: 'flex', flexWrap: 'wrap' }}>
                    <CheckboxGroup value={checkboxVal} onChange={(value) => setCV(value)}>
                        <List
                            dataSource={dataSource}
                            className="component-list-demo-booklist"
                            split={false}
                            size="small"
                            style={{ border: '1px solid var(--semi-color-border)', flexBasis: '100%', flexShrink: 0 }}
                            renderItem={(item) => (
                                <List.Item className="list-item">
                                    <Checkbox value={item}>{item}</Checkbox>
                                </List.Item>
                            )}
                        />
                    </CheckboxGroup>
                </div>
                <div style={{ marginRight: 16, width: 280, display: 'flex', flexWrap: 'wrap' }}>
                    <RadioGroup value={radioVal} onChange={(e) => setRV(e.target.value)}>
                        <List
                            className="component-list-demo-booklist"
                            dataSource={dataSource}
                            split={false}
                            size="small"
                            style={{ border: '1px solid var(--semi-color-border)', flexBasis: '100%', flexShrink: 0 }}
                            renderItem={(item) => (
                                <List.Item className="list-item">
                                    <Radio value={item}>{item}</Radio>
                                </List.Item>
                            )}
                        />
                    </RadioGroup>
                </div>
            </div>
        );
    }

    响应键盘事件

    你可以自行监听对应按键的键盘事件,实现不同 Item 的选择。如下面这个例子,可以使用上下方向键选择不同Item

    • 围城
    • 平凡的世界(全三册)
    • 三体(全集)
    • 雪中悍刀行(全集)
    • 撒哈拉的故事
    • 明朝那些事
    • 一禅小和尚
    • 沙丘
    • 被讨厌的勇气
    • 罪与罚
    代码
    import { useState, useEffect } from 'react';
    import { List } from '@fibrous/semi-ui-react';
    
    const list = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚'];
    
    export default function ListDemo() {
        const [hoverIndex, setHi] = useState(-1);
    
        useEffect(() => {
            let changeIndex = (offset: number) => {
                setHi((currentIndex) => {
                    let index = currentIndex + offset;
                    if (index < 0) {
                        index = list.length - 1;
                    }
                    if (index >= list.length) {
                        index = 0;
                    }
                    return index;
                });
            };
            let keydownHandler = (event: KeyboardEvent) => {
                switch (event.key) {
                    case 'ArrowUp':
                        event.preventDefault();
                        changeIndex(-1);
                        break;
                    case 'ArrowDown':
                        event.preventDefault();
                        changeIndex(1);
                        break;
                    default:
                        break;
                }
            };
            window.addEventListener('keydown', keydownHandler);
            return () => {
                window.removeEventListener('keydown', keydownHandler);
            };
        }, []);
    
        return (
            <div>
                <div
                    style={{
                        marginRight: 16,
                        width: 280,
                        display: 'flex',
                        flexWrap: 'wrap',
                        border: '1px solid var(--semi-color-border)',
                    }}
                >
                    <List
                        className="component-list-demo-booklist"
                        dataSource={list}
                        split={false}
                        size="small"
                        style={{ flexBasis: '100%', flexShrink: 0, borderBottom: '1px solid var(--semi-color-border)' }}
                        renderItem={(item, index) => <List.Item className={index === hoverIndex ? 'component-list-demo-booklist-active-item' : ''}>{item}</List.Item>}
                    />
                </div>
            </div>
        );
    }

    以上书单例子的Demo中涉及到的自定义样式如下

    .component-list-demo-booklist {
        .list-item {
            &:hover {
                background-color: var(--semi-color-fill-0);
            }
            &:active {
                background-color: var(--semi-color-fill-1);
            }
        }
    }
    
    .component-list-demo-booklist-active-item {
        background-color: var(--semi-color-fill-0);
    }

    无障碍

    List 支持传入 aria-label 或 aria-labelledby 来表示列表的作用,传入后根元素带 role="group"。

    设置了 onClick 或 onRightClick(包括 List 上的)的 List.Item 仍是 <li> 列表项,内部渲染一个原生 <button type="button"> 作为唯一的焦点目标,读屏会读成「按钮」。

    • 按钮包住 header 和 main(没有这两项时包住 children),点击区域铺满整行;Tab 聚焦后按 Enter 或空格触发 onClick,上下文菜单键触发 onRightClick
    • extra 渲染在按钮外面且层级更高,其中的按钮、链接可以正常聚焦和点击,点击它们不会触发 Item 的 onClick
    • 同时设置 header 或 main 和 children 时,children 在按钮外面,会被铺满整行的按钮覆盖,其中的交互内容请放进 extra
    • 没有点击回调的 List.Item 不渲染按钮,也不可聚焦

    List.Item 脱离 List 使用时按没有 grid 的普通列表项渲染。使用 grid 时,承载条目的 Row 带 role="list",包裹每个条目的 Col 带 role="none",读屏把条目读作这个列表的项。

    API 参考

    List

    Props

    属性说明类型默认值
    bordered是否显示边框。booleanfalse
    children直接写在列表中的子元素,渲染在 dataSource 生成的项之后;有子元素时不显示空状态。React.ReactNode-
    className根元素的类名。string-
    dataSource列表数据源,每一项通过 renderItem 渲染;未设置 renderItem 时不渲染任何项。T[]-
    emptyContent没有数据时的展示内容,缺省为语言包中的空文案。React.ReactNode-
    footer列表底部,渲染在列表内容之后。React.ReactNode-
    grid栅格配置:Row 的 align、gutter、justify、type 与 Col 的 span、offset、order、push、pull、响应式断点、className、style。className 与 style 落在每项外层的 Col 上。Grid-
    header列表头部,渲染在列表内容之前。React.ReactNode-
    layout列表布局,可选 vertical 或 horizontal。'vertical' | 'horizontal'"vertical"
    loading是否处于加载中,为 true 时在列表内容上显示 Spin。booleanfalse
    loadMore加载更多的按钮,渲染在列表根元素的末尾。React.ReactNode-
    onClick列表项的点击回调,作用于每个没有自己 onClick 的项;有处理函数的项渲染为原生按钮。TypeReact.MouseEventHandler<HTMLLIElement>-
    onRightClick列表项的右键(上下文菜单)回调,作用于每个没有自己 onRightClick 的项,这些项同样渲染为原生按钮。TypeReact.MouseEventHandler<HTMLLIElement>-
    ref根元素(div)的引用。TypeReact.Ref<HTMLDivElement>-
    renderItem渲染 dataSource 中的每一项,接收数据项与下标,返回 ReactNode,通常是 List.Item。Type(item: T, ind: number) => React.ReactNode-
    size列表尺寸,可选 small、default 或 large。Size"default"
    split是否展示分割线。booleantrue
    style根元素的样式。React.CSSProperties-

    其他原生属性(如 data-*、id)落在根元素上;传入 aria-label 或 aria-labelledby 且没有 role 时,根元素带 role="group"。onClick、onRightClick 作用于列表项而不是根元素。

    List grid props

    grid 只接受 Row 的 align、gutter、justify、type 与 Col 的 span、offset、order、push、pull、响应式断点、className、style,不接受其他 div 属性。参数含义请参考 Grid

    属性说明类型默认值
    span栅格占位格数number-
    gutter栅格间隔number0
    xs<576px 响应式栅格,可为栅格数或一个包含其他属性的对象number|object-
    sm≥576px 响应式栅格,可为栅格数或一个包含其他属性的对象number|object-
    md≥768px 响应式栅格,可为栅格数或一个包含其他属性的对象number|object-
    lg≥992px 响应式栅格,可为栅格数或一个包含其他属性的对象number|object-
    xl≥1200px 响应式栅格,可为栅格数或一个包含其他属性的对象number|object-
    xxl≥1600px 响应式栅格,可为栅格数或一个包含其他属性的对象number|object-

    List.Item

    Props

    属性说明类型默认值
    alignheader 与 main 的垂直对齐方式,可选 flex-start、flex-end、center、baseline 或 stretch。'flex-start' | 'flex-end' | 'center' | 'baseline' | 'stretch'"flex-start"
    children列表项内容。可点击的列表项同时设置 header 或 main 时,它渲染在按钮外面,会被铺满整行的按钮覆盖,交互内容请放进 extra。React.ReactNode-
    className列表项 li 的类名;使用 grid 时落在 li 上,不落在外层的 Col 上。string-
    extra列表项附加内容,渲染在按钮外,其中的交互元素可正常聚焦与点击,点击它们不会触发整行的点击。React.ReactNode-
    header列表项头部内容。React.ReactNode-
    main列表项主体内容。React.ReactNode-
    onRightClick右键(上下文菜单)回调,优先于 List 的 onRightClick;只设置它时列表项同样渲染为原生按钮。TypeReact.MouseEventHandler<HTMLLIElement>-
    refli 元素的引用。TypeReact.Ref<HTMLLIElement>-
    styleli 元素的样式;使用 grid 时落在 li 上,不落在外层的 Col 上。React.CSSProperties-

    其他原生属性(如 data-*)落在 li 上;可点击时 aria-* 与 tabIndex 落在内部的按钮上。onClick 作用于 li,优先于 List 的 onClick。

    文案规范

    • 首字母大写
    • 结尾不跟随标点符号
    • 语法平行:如主动态与被动态、陈述句与祈使句混合使用

    设计变量

    下表是组件内部的 Sass 变量,不能覆写。定制样式请覆写 --semi-* CSS 变量(见 Design Token 页),或写不分层的 CSS 规则。
    Sass 变量值用法
    $color-list_default-border-defaultvar(--semi-color-border)列表描边颜色
    $color-list_empty-text-defaultvar(--semi-color-text-2)空状态下列表文字颜色
    $spacing-list_empty-paddingX0px空状态列表水平内边距
    $spacing-list_empty-paddingY$spacing-base-tight空状态列表垂直内边距
    $spacing-list_footer-paddingX$spacing-base-tight列表 footer 水平内边距
    $spacing-list_footer-paddingY$spacing-loose列表 footer 垂直内边距
    $spacing-list_item-paddingX$spacing-base-tight列表 item 水平内边距 - 默认尺寸
    $spacing-list_item-paddingY$spacing-loose列表 item 垂直内边距 - 默认尺寸
    $spacing-list_small-paddingX$spacing-tight列表 item 水平内边距 - 小尺寸
    $spacing-list_small-paddingY$spacing-base列表 item 垂直内边距 - 小尺寸
    $spacing-list_large-paddingX$spacing-base列表 item 水平内边距 - 大尺寸
    $spacing-list_large-paddingY$spacing-loose列表 item 垂直内边距 - 大尺寸
    $spacing-list_header-marginRight$spacing-base-loose列表 header 右侧外边距
    $spacing-list_extra-marginLeft$spacing-super-loose列表 extra 额外内容左侧外边距