展示类

列表

基础列表组件

代码演示

如何引入

import { Avatar, Button, ButtonGroup, Checkbox, CheckboxGroup, Descriptions, Input, List, Pagination, Radio, RadioGroup, Rating, Skeleton, Spin } from '@fibrous/semi-ui-solid';

基本用法

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

Default Size

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

Small Size

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

Large Size

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

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

export default function ListDemo() {
    return (
        <div>
            <div style={{ 'margin-right': '16px' }}>
                <h3 style={{ 'margin-bottom': '16px' }}>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={{ 'margin-right': '16px' }}>
                <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={{ 'margin-right': '16px' }}>
                <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 { JSX } from 'solid-js';
import { Avatar, Button, ButtonGroup, List } from '@fibrous/semi-ui-solid';

// Each text carries its own style; a JSX element is a DOM node that can only sit in one place, so the <p> is built per row
const data: { text: string; style: JSX.CSSProperties }[] = [
    {
        text: 'Semi Design 是由抖音前端团队与 UED 团队共同设计开发并维护的设计系统。设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。',
        style: { color: 'var(--semi-color-text-2)', margin: '4px 0', width: '420px', 'white-space': 'nowrap', overflow: 'hidden', 'text-overflow': 'ellipsis' },
    },
    {
        text: 'Semi Design 是由抖音前端团队与 UED 团队共同设计开发并维护的设计系统。设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。',
        style: { color: 'var(--semi-color-text-2)', margin: '4px 0', width: '500px' },
    },
    {
        text: 'Semi Design 以用户中心、内容优先、设计人性化的设计系统,打造一致、好看、好用、高效的用户体验。',
        style: { color: 'var(--semi-color-text-2)', margin: '4px 0', width: '500px' },
    },
];

export default function ListDemo() {
    return (
        <div style={{ padding: '12px', border: '1px solid var(--semi-color-border)', margin: '12px' }}>
            <List
                dataSource={data}
                renderItem={(item) => (
                    <List.Item
                        header={<Avatar color="blue">SE</Avatar>}
                        main={
                            <div>
                                <span style={{ color: 'var(--semi-color-text-0)', 'font-weight': '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 { Avatar, List, type AvatarColor } from '@fibrous/semi-ui-solid';

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

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',
    },
];

export default function ListDemo() {
    return (
        <div style={{ padding: '12px', border: '1px solid var(--semi-color-border)', margin: '12px' }}>
            <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)', 'font-weight': '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 { Button, ButtonGroup, Descriptions, type DescriptionsData, List, Rating } from '@fibrous/semi-ui-solid';
    
    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)',
        'background-color': 'var(--semi-color-bg-2)',
        'border-radius': '3px',
        'padding-left': '20px',
    };
    
    const renderStat = (stat: DescriptionsData, index: number) => (index === 0 ? <Rating allowHalf size="small" value={typeof stat.value === 'number' ? stat.value : undefined} /> : stat.value);
    
    export default function ListDemo() {
        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)', 'font-weight': '500' }}>{item.title}</h3>
                                <Descriptions
                                    align="center"
                                    size="small"
                                    row
                                    data={[
                                        { key: '满意度', value: item.rating },
                                        { key: '反馈数', value: item.feedbacks },
                                    ]}
                                    renderValue={renderStat}
                                />
                                <div style={{ margin: '12px 0', display: 'flex', 'justify-content': 'flex-end' }}>
                                    <ButtonGroup theme="borderless" style={{ 'margin-top': '8px' }}>
                                        <Button>编辑</Button>
                                        <Button>更多</Button>
                                    </ButtonGroup>
                                </div>
                            </div>
                        </List.Item>
                    )}
                />
            </div>
        );
    }

    响应式的栅格列表

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

  • 审核管理平台

    满意度
    反馈数124
  • 扁鹊

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

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

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

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

    满意度
    反馈数156
  • 代码
    import { Button, ButtonGroup, Descriptions, type DescriptionsData, List, Rating } from '@fibrous/semi-ui-solid';
    
    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)',
        'background-color': 'var(--semi-color-bg-2)',
        'border-radius': '3px',
        'padding-left': '20px',
        margin: '8px 2px',
    };
    
    const renderStat = (stat: DescriptionsData, index: number) => (index === 0 ? <Rating allowHalf size="small" value={typeof stat.value === 'number' ? stat.value : undefined} /> : stat.value);
    
    export default function ListDemo() {
        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)', 'font-weight': '500' }}>{item.title}</h3>
                                <Descriptions
                                    align="center"
                                    size="small"
                                    row
                                    data={[
                                        { key: '满意度', value: item.rating },
                                        { key: '反馈数', value: item.feedbacks },
                                    ]}
                                    renderValue={renderStat}
                                />
                                <div style={{ margin: '12px 0', display: 'flex', 'justify-content': 'flex-end' }}>
                                    <ButtonGroup theme="borderless" style={{ 'margin-top': '8px' }}>
                                        <Button>编辑</Button>
                                        <Button>更多</Button>
                                    </ButtonGroup>
                                </div>
                            </div>
                        </List.Item>
                    )}
                />
            </div>
        );
    }

    加载更多

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

    代码
    import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js';
    import { Avatar, Button, List, Skeleton, type AvatarColor } from '@fibrous/semi-ui-solid';
    
    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[] = [{ loading: true }, { loading: true }, { loading: true }];
    
    const fetchData = (page: number) => {
        return new Promise<Book[]>((res) => {
            setTimeout(() => {
                res(data.slice(page * count, page * count + count));
            }, 1000);
        });
    };
    
    function Placeholder() {
        return (
            <div
                style={{
                    display: 'flex',
                    'align-items': 'flex-start',
                    padding: '12px',
                    'border-bottom': '1px solid var(--semi-color-border)',
                }}
            >
                <Skeleton.Avatar style={{ 'margin-right': '12px' }} />
                <div>
                    <Skeleton.Title style={{ width: '120px', 'margin-bottom': '12px', 'margin-top': '12px' }} />
                    <Skeleton.Paragraph style={{ width: '600px' }} rows={2} />
                </div>
            </div>
        );
    }
    
    export default function ListDemo() {
        let page = 0;
        const [loading, setLoading] = createSignal(true);
        const [dataSource, setDataSource] = createSignal<Book[]>([]);
        const [noMore, setNoMore] = createSignal(false);
        const list = createMemo(() => (loading() ? [...dataSource(), ...placeholders] : dataSource()));
    
        let disposed = false;
        onCleanup(() => {
            disposed = true;
        });
    
        const load = (index: number) => {
            void fetchData(index).then((newDataSource) => {
                if (disposed) return;
                setDataSource((prevData) => [...prevData, ...newDataSource]);
                setLoading(false);
                setNoMore(!newDataSource.length);
            });
        };
    
        onMount(() => {
            load(0);
        });
    
        const onLoadMore = () => {
            setLoading(true);
            load(++page);
        };
    
        return (
            <List
                loading={loading()}
                loadMore={
                    <Show when={!loading() && !noMore()}>
                        <div
                            style={{
                                'text-align': 'center',
                                'margin-top': '12px',
                                height: '32px',
                                'line-height': '32px',
                            }}
                        >
                            <Button onClick={onLoadMore}>显示更多</Button>
                        </div>
                    </Show>
                }
                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)', 'font-weight': '500' }}>{item.title}</span>
                                    <p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}>Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
                                </div>
                            }
                        />
                    </Skeleton>
                )}
            />
        );
    }

    滚动加载

    可以监听滚动容器的 scroll 事件,在接近底部时加载下一页。交互建议符合 semi 交互设计规范,这里采用三次滚加载后出现 load more 按钮的形式。

    暂无数据
    代码
    import { createSignal, onCleanup, onMount, Show } from 'solid-js';
    import { Avatar, Button, List, Spin } from '@fibrous/semi-ui-solid';
    
    interface Book {
        title: string;
    }
    
    const count = 5;
    const dataList: Book[] = Array.from({ length: 100 }, (_, i) => ({ title: `Semi Design Title ${i}` }));
    
    const fetchData = (page: number) =>
        new Promise<Book[]>((res) => {
            setTimeout(() => {
                res(dataList.slice(page * count, page * count + count));
            }, 1000);
        });
    
    export default function ListDemo() {
        const [page, setPage] = createSignal(0);
        const [loading, setLoading] = createSignal(true);
        const [dataSource, setDataSource] = createSignal<Book[]>([]);
        const [hasMore, setHasMore] = createSignal(true);
    
        let disposed = false;
        onCleanup(() => {
            disposed = true;
        });
    
        const showLoadMore = () => (page() + 1) % 4 === 0;
    
        const load = (index: number) => {
            setLoading(true);
            void fetchData(index).then((newDataSource) => {
                if (disposed) return;
                setDataSource((prevData) => [...prevData, ...newDataSource]);
                setLoading(false);
                setHasMore(!!newDataSource.length);
            });
        };
    
        onMount(() => {
            load(0);
        });
    
        const onLoadMore = () => {
            const next = page() + 1;
            setPage(next);
            load(next);
        };
    
        // Near the bottom of the scroll container, load the next page unless the load-more button takes over
        const onScroll = (event: { currentTarget: HTMLElement }) => {
            const el = event.currentTarget;
            if (loading() || !hasMore() || showLoadMore()) return;
            if (el.scrollHeight - el.scrollTop - el.clientHeight <= 20) onLoadMore();
        };
    
        return (
            <section aria-label="可滚动列表" class="light-scrollbar" tabIndex={0} style={{ height: '420px', overflow: 'auto', border: '1px solid var(--semi-color-border)', padding: '10px' }} onScroll={onScroll}>
                <List
                    loadMore={
                        <Show when={!loading() && hasMore() && showLoadMore()}>
                            <div style={{ 'text-align': 'center', 'margin-top': '12px', height: '32px', 'line-height': '32px' }}>
                                <Button onClick={onLoadMore}>显示更多</Button>
                            </div>
                        </Show>
                    }
                    dataSource={dataSource()}
                    renderItem={(item) => (
                        <List.Item
                            header={<Avatar color="grey">SE</Avatar>}
                            main={
                                <div>
                                    <span style={{ color: 'var(--semi-color-text-0)', 'font-weight': '500' }}>{item.title}</span>
                                    <p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}>Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
                                </div>
                            }
                        />
                    )}
                />
                <Show when={loading() && hasMore()}>
                    <div style={{ 'text-align': 'center' }}>
                        <Spin />
                    </div>
                </Show>
            </section>
        );
    }

    滚动加载无限长列表

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

    代码
    import { createEffect, For } from 'solid-js';
    import { createStore } from 'solid-js/store';
    import { createVirtualizer } from '@tanstack/solid-virtual';
    import { Avatar, List, Spin } from '@fibrous/semi-ui-solid';
    
    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() {
        let scroller: HTMLElement | undefined;
        const [loaded, setLoaded] = createStore<Record<number, boolean>>({});
        // A requested index is never fetched again, even while pending
        const requested = new Set<number>();
        const virtualizer = createVirtualizer({ count: rowCount, getScrollElement: () => scroller ?? null, estimateSize: () => rowHeight, overscan: 2 });
    
        createEffect(() => {
            const items = virtualizer.getVirtualItems();
            if (!items.length) return;
            const wanted: number[] = [];
            for (let i = items[0].index; i <= Math.min(items[items.length - 1].index + prefetch, rowCount - 1); i++) {
                if (!requested.has(i)) {
                    requested.add(i);
                    wanted.push(i);
                }
            }
            if (!wanted.length) return;
            setTimeout(() => {
                for (const i of wanted) setLoaded(i, true);
            }, 1000);
        });
    
        // Indexes rather than virtual items: For keys rows by value, and the items are new objects on every scroll
        const rows = () => virtualizer.getVirtualItems().flatMap((item) => (loaded[item.index] ? [item.index] : []));
    
        return (
            <div style={{ border: '1px solid var(--semi-color-border)', padding: '10px' }}>
                <section ref={(node) => (scroller = node)} aria-label="虚拟滚动列表" tabIndex={0} style={{ height: '500px', overflow: 'auto' }}>
                    <List style={{ position: 'relative', height: `${virtualizer.getTotalSize()}px` }} emptyContent={<Spin />}>
                        <For each={rows()}>
                            {(index) => (
                                <List.Item
                                    style={{ position: 'absolute', 'inset-inline': '0', top: `${index * rowHeight}px`, height: `${rowHeight}px` }}
                                    header={<Avatar color="grey">SE</Avatar>}
                                    main={
                                        <div>
                                            <span style={{ color: 'var(--semi-color-text-0)', 'font-weight': '500' }}>{dataList[index].title}</span>
                                            <p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}>Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
                                        </div>
                                    }
                                />
                            )}
                        </For>
                    </List>
                </section>
            </div>
        );
    }

    拖拽排序

    使用 dnd-kit 的 Solid 版(@dnd-kit/solid)可轻松实现拖拽排序。被拖动的就是 List.Item 渲染的 li。

    • 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 { createSignal } from 'solid-js';
    import { RestrictToVerticalAxis } from '@dnd-kit/abstract/modifiers';
    import { Accessibility } from '@dnd-kit/dom';
    import { arrayMove } from '@dnd-kit/helpers';
    import { DragDropProvider } from '@dnd-kit/solid';
    import { isSortable, useSortable } from '@dnd-kit/solid/sortable';
    import { List, Avatar, type AvatarColor } from '@fibrous/semi-ui-solid';
    
    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 {
        item: Item;
        index: number;
    }
    
    function SortableListItem(props: SortableListItemProps) {
        const { ref, isDragging } = useSortable({
            get id() {
                return props.item.id;
            },
            get index() {
                return props.index;
            },
        });
    
        return (
            <List.Item
                ref={(node) => ref(node)}
                class={isDragging() ? 'isDragging' : undefined}
                style={{
                    border: '1px solid var(--semi-color-border)',
                    'margin-bottom': '12px',
                    cursor: 'grabbing',
                    ...(isDragging() ? { 'z-index': 999, position: 'relative', 'background-color': 'var(--semi-color-bg-0)' } : {}),
                }}
                header={<Avatar color={props.item.color}>SE</Avatar>}
                main={
                    <div>
                        <span style={{ color: 'var(--semi-color-text-0)', 'font-weight': '500' }}>{props.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] = createSignal(data);
    
        return (
            <div style={{ padding: '12px', border: '1px solid var(--semi-color-border)', margin: '12px' }}>
                <DragDropProvider
                    plugins={(defaults) => defaults.filter((plugin) => plugin !== Accessibility)}
                    modifiers={[RestrictToVerticalAxis]}
                    onDragEnd={(event) => {
                        const { source } = event.operation;
                        if (!event.canceled && isSortable(source)) {
                            setListItems((items) => arrayMove(items, source.initialIndex, source.index));
                        }
                    }}
                >
                    <List dataSource={listItems()} renderItem={(item, index) => <SortableListItem item={item} index={index} />} />
                </DragDropProvider>
            </div>
        );
    }

    带分页器

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

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

    带筛选器

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

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

    添加删除项

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

    单选或多选

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

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

    响应键盘事件

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

    • 围城
    • 平凡的世界(全三册)
    • 三体(全集)
    • 雪中悍刀行(全集)
    • 撒哈拉的故事
    • 明朝那些事
    • 一禅小和尚
    • 沙丘
    • 被讨厌的勇气
    • 罪与罚
    代码
    import { createSignal, onCleanup, onMount } from 'solid-js';
    import { List } from '@fibrous/semi-ui-solid';
    
    const list = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚'];
    
    export default function ListDemo() {
        const [hoverIndex, setHoverIndex] = createSignal(-1);
    
        const changeIndex = (offset: number) => {
            setHoverIndex((currentIndex) => {
                let index = currentIndex + offset;
                if (index < 0) {
                    index = list.length - 1;
                }
                if (index >= list.length) {
                    index = 0;
                }
                return index;
            });
        };
    
        const keydownHandler = (event: KeyboardEvent) => {
            switch (event.key) {
                case 'ArrowUp':
                    event.preventDefault();
                    changeIndex(-1);
                    break;
                case 'ArrowDown':
                    event.preventDefault();
                    changeIndex(1);
                    break;
                default:
                    break;
            }
        };
    
        onMount(() => {
            window.addEventListener('keydown', keydownHandler);
            onCleanup(() => window.removeEventListener('keydown', keydownHandler));
        });
    
        return (
            <div>
                <div style={{ 'margin-right': '16px', width: '280px', display: 'flex', 'flex-wrap': 'wrap', border: '1px solid var(--semi-color-border)' }}>
                    <List
                        class="component-list-demo-booklist"
                        dataSource={list}
                        split={false}
                        size="small"
                        style={{ 'flex-basis': '100%', 'flex-shrink': '0', 'border-bottom': '1px solid var(--semi-color-border)' }}
                        renderItem={(item, index) => <List.Item class={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
    dataSource列表数据源,每一项通过 renderItem 渲染。T[]-
    emptyContent没有数据时的展示内容,缺省为语言包中的空文案。JSX.Element-
    footer列表底部。JSX.Element-
    grid栅格配置:Row 的 align、gutter、justify、type 与 Col 的 span、offset、order、push、pull、响应式断点、class、style,class 与 style 落在每项外层的 Col 上。Grid-
    header列表头部。JSX.Element-
    layout列表布局,可选 vertical 或 horizontal。'vertical' | 'horizontal'"vertical"
    loading是否处于加载中,为 true 时显示 Spin。booleanfalse
    loadMore加载更多的按钮。JSX.Element-
    onClick列表项的点击回调,作用于每个没有自己 onClick 的项;有处理函数的项渲染为原生按钮。ListItemEventHandler-
    onRightClick列表项的右键(上下文菜单)回调,作用于每个没有自己 onRightClick 的项,这些项同样渲染为原生按钮。ListItemEventHandler-
    renderItem渲染 dataSource 中的每一项,接收数据项与下标,返回 JSX 元素,通常是 List.Item。Type(item: T, ind: number) => JSX.Element-
    size列表尺寸,可选 small、default 或 large。Size"default"
    split是否展示分割线。booleantrue

    class、style、ref 与原生属性(如 data-*)落在根元素上;传入 aria-label 或 aria-labelledby 且没有 role 时,根元素带 role="group"。onClick、onRightClick 的参数为原生 MouseEvent,作用于列表项而不是根元素。

    List grid props

    grid 只接受 Row 的 align、gutter、justify、type 与 Col 的 span、offset、order、push、pull、响应式断点、class、style,不接受其他属性;class 与 style 落在每项外层的 Col 上。参数含义请参考 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-start' | 'flex-end' | 'center' | 'baseline' | 'stretch'-
    extra列表项附加内容,渲染在按钮外,其中的交互元素可正常聚焦与点击,点击不会触发整行的点击。JSX.Element-
    header列表项头部内容。JSX.Element-
    main列表项主体内容。JSX.Element-
    onClick点击回调,优先于 List 的 onClick;有处理函数时项渲染为原生按钮。ListItemEventHandler-
    onRightClick右键(上下文菜单)回调,优先于 List 的 onRightClick;只设置它时项同样渲染为原生按钮。ListItemEventHandler-

    class、style、ref 与原生属性(如 data-*)落在 li 上,不落在 grid 模式下外层的 Col 上;可点击时 aria-* 与 tabIndex 落在内部的按钮上,ref.focus() 转到该按钮。

    文案规范

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

    设计变量

    下表是组件内部的 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 额外内容左侧外边距