元素
Pagination
分页用于在一组内容的各页之间移动,比如搜索结果、表格的行、归档列表。它依次显示上一页、当前页附近的页码(第一页和最后一页总会显示,每段被省略的页显示一个省略号)、应用的子元素和下一页。它负责行为:显示哪些页、按压、键盘、两端时禁用上一页和下一页、导航地标,以及让屏幕阅读器知道当前页。外观由应用决定:各项没有自己的颜色、尺寸或文字,由应用设置样式或自己绘制。
import { mount, signal, Text, View } from "nsh";
import { Pagination } from "nsh/base";
import { desktop, Log, Panel, Segmented, Split, ui } from "./palette";
// Search results paged with a count, and a compact pager beside them. The
// look is all `itemClass` over the component's own numbers and labels:
// quiet items until hovered, the current page lit in the brand color
// (`selected:`) with `transition-colors` (still with reduced motion), the
// ends dimmed (`disabled:`).
const itemClass =
"min-w-8 h-8 px-2 items-center justify-center rounded-md text-sm text-foreground " +
"transition-colors duration-200 motion-reduce:transition-none hover:bg-hover active:opacity-70 " +
"selected:bg-brand selected:text-on-brand disabled:opacity-35 focus:outline-2 focus:outline-ring";
const total = 238;
const page = signal(1);
const perPage = signal<"10" | "25">("10");
const chapter = signal(1);
const events = signal<string[]>([]);
const pages = () => Math.ceil(total / Number(perPage()));
const first = () => (page() - 1) * Number(perPage()) + 1;
const last = () => Math.min(page() * Number(perPage()), total);
const changed = (next: number) => {
page.set(next);
events.set([`onChange({ page: ${next} })`, ...events()].slice(0, 4));
};
function App() {
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>pagination</Text>
<Text style={{ color: ui().muted }}>{desktop ? "Click a page, or Tab to one and press Return" : "Tap a page"}</Text>
<Split
gap={14}
inspector={
<>
<Panel title="page · totalPages">
<Text style={{ fontSize: 13, color: ui().text }}>{`${page()} of ${pages()}`}</Text>
</Panel>
<Log title="onChange({ page })" lines={events()} minHeight={110} />
</>
}
>
<View style={{ ...ui().card, borderRadius: 16, padding: desktop ? 16 : 18, gap: 14 }}>
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 8 }}>
<Text style={{ fontSize: 13, color: ui().muted }}>{`Showing ${first()}–${last()} of ${total} results`}</Text>
<Segmented
size="small"
options={[
{ value: "10", label: "10" },
{ value: "25", label: "25" },
]}
value={perPage()}
onChange={(next) => {
const row = first();
perPage.set(next);
page.set(Math.floor((row - 1) / Number(next)) + 1);
}}
/>
</View>
<Pagination
page={page()}
totalPages={pages()}
visiblePages={desktop ? 7 : 5}
accessibilityLabel="Search results pages"
previousAccessibilityLabel="Previous"
nextAccessibilityLabel="Next"
class="gap-1 flex-wrap"
itemClass={itemClass}
onChange={({ page: next }) => changed(next)}
/>
<View style={{ height: 1, backgroundColor: ui().line }} />
<Pagination
compact
page={chapter()}
totalPages={12}
accessibilityLabel="Chapters"
previousAccessibilityLabel="Previous chapter"
nextAccessibilityLabel="Next chapter"
class="items-center justify-between"
itemClass={`${itemClass} px-3 border border-border`}
onChange={({ page: next }) => chapter.set(next)}
>
<Text style={{ fontSize: 13, color: ui().muted }}>{`Chapter ${chapter()} of 12`}</Text>
</Pagination>
</View>
</Split>
</View>
);
}
mount(App);use nsh_base::{pagination, theme};
use nsh_core::prelude::*;
fn results() -> impl IntoElement {
let page = signal(1u32);
pagination()
.page(move || page.get())
.total_pages(24)
.accessibility_label("Search results")
.previous_accessibility_label("Previous")
.next_accessibility_label("Next")
.item_class("min-w-9 h-9 px-2 items-center justify-center rounded-md hover:bg-accent selected:bg-primary selected:text-primary-foreground disabled:opacity-50")
.gap_1()
.on_change(move |p: &u32| page.set(*p))
}import { mount, signal } from "nsh";
import { Pagination } from "nsh/base";
function App() {
const page = signal(1);
return (
<Pagination
page={page()}
totalPages={24}
accessibilityLabel="Search results"
previousAccessibilityLabel="Previous"
nextAccessibilityLabel="Next"
class="gap-1"
itemClass="min-w-9 h-9 px-2 items-center justify-center rounded-md hover:bg-accent selected:bg-primary selected:text-primary-foreground disabled:opacity-50"
onChange={({ page: next }) => page.set(next)}
/>
);
}
mount(App);Pagination 来自 nsh/base,在 Rust 中是 nsh-base crate。Rust 中 pagination() 创建一个分页,每个 prop 有一个 setter(.page(..)、.total_pages(..)),.on_change(..) 接收用户选择的页,行用带类型的样式方法设置,应用的内容用 .child(..) 放入。
#当前页
当前页由应用持有:page 是当前页,从 1 开始;用户按下某一页、上一页或下一页时,onChange({ page }) 报告所选的页。应用设置它、加载那一页并显示其中的内容。totalPages 是总页数;超过最后一页的 page 显示为最后一页。按下当前页不会报告任何事件,第一页时的上一页和最后一页时的下一页也一样。
#显示的页
visiblePages(至少 5,也是默认值)页放得下时,所有页都会显示。超过时,显示第一页和最后一页、当前页及其两侧各 (visiblePages - 3) / 2 页,每段被省略的页显示一个省略号:
共 20 页时的 page,visiblePages={7} | 显示 |
|---|---|
| 1 | 1 2 3 4 5 … 20 |
| 6 | 1 … 4 5 6 7 8 … 20 |
| 20 | 1 … 16 17 18 19 20 |
一万页时显示的也是同样的七项。奇数的 visiblePages 让当前页居中。
#各项
每一项都使用应用的 itemClass 和 itemStyle:当前页带 selected:,不接受按压的项带 disabled:,获得键盘焦点的项带 focus:。整行的布局由应用的 class 决定(默认是一行;gap-*、justify-*)。
没有 renderItem 时,页显示页码,省略号显示 …,上一页和下一页显示它们的无障碍标签(没有标签时显示 ‹ 和 ›)。renderItem 根据 PaginationItemState 绘制每一项的内容:
| 字段 | 类型 | 说明 |
|---|---|---|
kind | "previous" | "page" | "ellipsis" | "next" | 这一项是什么。 |
page | number | 页的页码;上一页或下一页要去的页(两端时为当前页);省略号省略的第一页。 |
start、end | number | 省略号省略的页:包含 start,不包含 end(下一个显示的页)。 |
current | boolean | 是否是当前页。 |
disabled | boolean | 不接受按压:第一页时的上一页、最后一页时的下一页、禁用的分页中的所有项。 |
<Pagination
page={page()}
totalPages={pages()}
previousAccessibilityLabel="Previous page"
nextAccessibilityLabel="Next page"
itemClass="size-9 items-center justify-center rounded-md selected:bg-primary"
renderItem={(item) =>
item.kind === "previous" ? <Icon name="chevron-left" /> :
item.kind === "next" ? <Icon name="chevron-right" /> :
item.kind === "ellipsis" ? <JumpMenu from={item.start!} to={item.end!} onPick={(p) => page.set(p)} /> :
<Text class={item.current ? "font-semibold text-primary-foreground" : "text-foreground"}>{item.page}</Text>
}
onChange={({ page: next }) => page.set(next)}
/>
省略号自己不接受按压:它做什么由应用决定。像上面的 JumpMenu 那样,在里面绘制一个 Menu,就能列出它省略的页。
#紧凑
compact 只显示上一页、子元素和下一页:适合窄屏的翻页器,当前页由子元素说明。
<Pagination compact page={page()} totalPages={12} onChange={({ page: next }) => page.set(next)} class="items-center gap-3">
<Text class="text-sm text-muted-foreground">{`${page()} of 12`}</Text>
</Pagination>
两种形式下,子元素都位于页码之后、下一页之前:比如页数,或跳转到某页的输入框。
#Props
| Prop | Rust | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
page | .page(..) | number | 必填 | 当前页,从 1 开始;应用在 onChange 中设置它。 |
totalPages | .total_pages(..) | number | 必填 | 总页数,至少为 1。 |
visiblePages | .visible_pages(..) | number | 5 | 同时显示的最多页数,包括第一页和最后一页;至少为 5。 |
compact | .compact(..) | boolean | false | 只显示上一页、子元素和下一页。 |
disabled | .disabled(..) | boolean | false | 所有项都不接受按压和键盘焦点;disabled: 样式生效,屏幕阅读器读作已变暗。 |
accessibilityLabel | .accessibility_label(..) | string | 无 | 屏幕阅读器对这个导航的称呼:它分页的是什么。 |
previousAccessibilityLabel、nextAccessibilityLabel | .previous_accessibility_label(..)、.next_accessibility_label(..) | string | 无 | 屏幕阅读器对上一页和下一页的称呼,也是没有 renderItem 时它们的内容。 |
itemClass、itemStyle | .item_class(..)、.item_style(|s| ..) | string、StyleProp<TextStyle> | 无 | 每一项的外观:当前页 selected:、disabled:、focus:;文字样式传给其中的内容。 |
renderItem | .render_item(|s: &PaginationItemState| ..) | (item: PaginationItemState) => JSX.Element | 无 | 根据状态绘制每一项的内容。 |
children | .child(..) | JSX.Element | 无 | 应用的内容,位于页码之后、下一页之前。 |
class、style | 样式方法 | string、StyleProp<ViewStyle> | 无 | 整行的外观和布局。 |
#事件
| 事件 | Rust | 说明 |
|---|---|---|
onChange | .on_change(|page: &u32| ..) | { page }:用户通过某一页、上一页或下一页选择的页。当前页永远不会被报告。 |
#动效
当前页按应用的 selected: 类从一项移到另一项:在 itemClass 上加 transition-colors,高亮就会从离开的页淡入到到达的页。换页时每一项都留在原位,只改变它显示的内容,所以整行不会闪烁;开启减少动态效果时,应用的过渡会停住(motion-reduce:transition-none)。
#无障碍
- 整行是一个导航地标,由
accessibilityLabel命名。 - 每一页、上一页和下一页都是按钮。当前页读作当前页(ARIA 的
aria-current="page"),并且处于选中状态;第一页时的上一页和最后一页时的下一页读作已变暗。 - 在 macOS 和 Web 上,每一页、上一页和下一页都是键盘焦点的一站,Return 和 Space 按下它。用键盘按下的页在应用显示它之后保留焦点,无论它落在行中的哪个位置;上一页或下一页到达第一页或最后一页时,焦点也会移到那一页。
关于标签、角色和播报的一般说明,见无障碍。
#平台说明
- iOS、Android 和 Mac Catalyst:按压是在某一项上结束的轻点。
- 桌面(macOS、Windows、Linux、GPUI)和 Web:按压是点击;在 macOS 和 Web 上,Tab 可以到达各项。
- 每个平台都绘制应用设置的样式。