代码演示
如何引入
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
- 从明天起,做一个幸福的人
- 喂马,劈柴,周游世界
- 从明天起,关心粮食和蔬菜
- 我有一所房子,面朝大海,春暖花开
Small Size
- 从明天起,做一个幸福的人
- 喂马,劈柴,周游世界
- 从明天起,关心粮食和蔬菜
- 我有一所房子,面朝大海,春暖花开
Large Size
- 从明天起,做一个幸福的人
- 喂马,劈柴,周游世界
- 从明天起,关心粮食和蔬菜
- 我有一所房子,面朝大海,春暖花开
代码
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示例标题
- 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。
- SESemi Design Title 1
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi Design Title 2
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi 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。
- 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 { 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 | 是否显示边框。 | boolean | false |
| 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。 | boolean | false |
| 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 | 是否展示分割线。 | boolean | true |
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 | 栅格间隔 | 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-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 变量 | 值 | 用法 |
|---|---|---|
| $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 额外内容左侧外边距 |