本頁由機器翻譯,可能有錯誤。 閱讀英文原文

元素

Pagination

Pagination 用於在一組頁面之間切換,例如搜尋結果、表格的資料列或封存內容。它會顯示「上一頁」、目前頁面周圍的頁碼(第一頁與最後一頁一律顯示,每一段被省略的頁碼以一個省略符號表示)、應用程式的子元素,以及「下一頁」。它負責處理行為:要顯示哪些頁碼、按下操作、鍵盤操作、在首尾頁停用「上一頁」與「下一頁」,以及提供給螢幕閱讀器的導覽地標與目前頁面資訊。外觀則由應用程式決定:各項目本身沒有顏色、尺寸或標籤,由應用程式為它們設定樣式或自行繪製。

el-pagination.tsx
el-pagination.tsxLive
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))
}

Pagination 來自 nsh/base,也就是 Rust 中的 nsh-base crate。在 Rust 中,pagination() 會建立一個 pagination:每個 prop 各有一個 setter(.page(..)、.total_pages(..)),以 .on_change(..) 處理使用者選取的頁面,以具型別的樣式方法設定資料列,並以 .child(..) 放入應用程式的內容。

#頁面

頁面由應用程式掌控:page 是目前頁面(從 1 起算),onChange({ page }) 會回報使用者透過按下某個頁碼、「上一頁」或「下一頁」所選取的頁面。應用程式負責設定它、載入該頁並顯示其資料列。totalPages 是總頁數;超出最後一頁的 page 會顯示最後一頁。按下目前頁面不會回報任何事件,在第一頁按下「上一頁」以及在最後一頁按下「下一頁」也一樣。

#顯示的頁碼

使用 visiblePages(至少為 5,此為預設值)時,只要放得下,資料列就會顯示所有頁碼。超過之後,它會顯示第一頁與最後一頁、目前頁面以及其兩側盡可能放得下的頁碼,(visiblePages - 3) / 2,並以一個省略符號代表每一段被省略的頁碼:

20 頁中的 page,visiblePages={7}顯示
11 2 3 4 5 … 20
61 … 4 5 6 7 8 … 20
201 … 16 17 18 19 20

即使有一萬頁,也同樣只顯示七個項目。奇數的 visiblePages 會讓目前頁面置中。

#項目

每個項目都會採用應用程式的 itemClass 與 itemStyle:目前頁面為 selected:,不接受按下操作的項目為 disabled:,擁有鍵盤焦點的項目為 focus:。資料列的版面由應用程式以 class 自行配置(預設為橫列;gap-*、justify-*)。

未設定 renderItem 時,頁碼項目顯示其數字,省略符號顯示 …,「上一頁」與「下一頁」則顯示其無障礙標籤(若無標籤則分別為 ‹ 與 ›)。renderItem 會根據 PaginationItemState 繪製每個項目的內容:

欄位型別說明
kind"previous" | "page" | "ellipsis" | "next"項目的種類。
pagenumber頁碼項目的數字;「上一頁」或「下一頁」要前往的頁面(在首尾頁時為目前頁面);省略符號所隱藏的第一頁。
start、endnumber省略符號所隱藏的頁面:包含 start,不包含 end(下一個顯示的頁面)。
currentboolean目前頁面。
disabledboolean不接受按下操作:第一頁的「上一頁」、最後一頁的「下一頁」,以及已停用的 pagination 中的所有項目。
<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)}
/>

省略符號本身不接受按下操作,其行為由應用程式決定。繪製於其中的 Menu 會列出它所隱藏的頁面,如上方的 JumpMenu 所示。

#精簡模式

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

PropRust型別預設值說明
page.page(..)number必填目前頁面,從 1 起算;由應用程式在 onChange 中設定。
totalPages.total_pages(..)number必填總頁數,至少為 1。
visiblePages.visible_pages(..)number5同時顯示的最多頁數,包含第一頁與最後一頁;至少為 5。
compact.compact(..)booleanfalse只顯示「上一頁」、子元素與「下一頁」。
disabled.disabled(..)booleanfalse沒有任何項目接受按下操作或鍵盤焦點;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: class 在項目之間移動:在 itemClass 上使用 transition-colors,會讓醒目標示從原本的頁面淡出並淡入到新到達的頁面。換頁時,每個項目都保持在原位,只改變項目所顯示的內容,因此資料列不會閃爍;啟用減少動態效果時,應用程式的轉場會停止(motion-reduce:transition-none)。

#無障礙

  • 資料列是一個導覽地標,其名稱由 accessibilityLabel 提供。
  • 每個頁碼、「上一頁」與「下一頁」都是按鈕。目前頁面會被朗讀為目前頁面(ARIA 的 aria-current="page")並處於選取狀態;第一頁的「上一頁」與最後一頁的「下一頁」呈現為停用。
  • 在 macOS 與網頁上,每個頁碼、「上一頁」與「下一頁」都是鍵盤焦點的停駐點,按下 Return 與空白鍵即可觸發。以鍵盤按下某個頁碼後,一旦應用程式顯示該頁,焦點會保留在該頁碼上,無論它在資料列中的位置為何;當「上一頁」或「下一頁」抵達第一頁或最後一頁時,焦點也會停留在該頁。

標籤、角色與朗讀通知的一般說明,請參閱 Accessibility。

#平台說明

  • iOS、Android 與 Mac Catalyst:按下操作是在某個項目上結束的輕點。
  • 桌面平台(macOS、Windows、Linux、GPUI)與網頁:按下操作是點擊;在 macOS 與網頁上,可用 Tab 鍵移到各項目。
  • 所有平台都會繪製應用程式所設定的樣式。