代码演示
如何引入
import { List } from '@fibrous/semi-ui-react';
基本用法
列表的基本用法。可以通过 size 设置尺寸,支持large, default, small。可设置 header 和 footer,来自定义列表头部和尾部。
Default Size
- 从明天起,做一个幸福的人
- 喂马,劈柴,周游世界
- 从明天起,关心粮食和蔬菜
- 我有一所房子,面朝大海,春暖花开
Small Size
- 从明天起,做一个幸福的人
- 喂马,劈柴,周游世界
- 从明天起,关心粮食和蔬菜
- 我有一所房子,面朝大海,春暖花开
Large Size
- 从明天起,做一个幸福的人
- 喂马,劈柴,周游世界
- 从明天起,关心粮食和蔬菜
- 我有一所房子,面朝大海,春暖花开
代码
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示例标题
- 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。
- SESemi Design Title 1
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi Design Title 2
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi 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 可轻松实现拖拽排序。
- SESemi Design Title 1
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi Design Title 2
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi Design Title 3
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi Design Title 4
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi 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 | 是否显示边框。 | boolean | false |
| 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。 | boolean | false |
| 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 | 是否展示分割线。 | boolean | true |
| 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 | 栅格间隔 | number | 0 |
| 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
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| align | header 与 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> | - |
| ref | li 元素的引用。 | TypeReact.Ref<HTMLLIElement> | - |
| style | li 元素的样式;使用 grid 时落在 li 上,不落在外层的 Col 上。 | React.CSSProperties | - |
其他原生属性(如 data-*)落在 li 上;可点击时 aria-* 与 tabIndex 落在内部的按钮上。onClick 作用于 li,优先于 List 的 onClick。
文案规范
- 首字母大写
- 结尾不跟随标点符号
- 语法平行:如主动态与被动态、陈述句与祈使句混合使用
设计变量
| Sass 变量 | 值 | 用法 |
|---|---|---|
| $color-list_default-border-default | var(--semi-color-border) | 列表描边颜色 |
| $color-list_empty-text-default | var(--semi-color-text-2) | 空状态下列表文字颜色 |
| $spacing-list_empty-paddingX | 0px | 空状态列表水平内边距 |
| $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 额外内容左侧外边距 |