Elementos
Pagination
Uma paginação navega entre as páginas de um conjunto: resultados de busca, as linhas de uma tabela, um arquivo. Ela mostra Anterior, as páginas ao redor da atual (a primeira e a última sempre, e reticências para cada trecho omitido), os filhos do app e Próxima. Ela é responsável pelo comportamento: quais páginas aparecem, os acionamentos, o teclado, Anterior e Próxima desabilitados nas extremidades, o marco de navegação e a página atual para leitores de tela. A aparência fica a cargo do app: os itens não têm cor, tamanho ou rótulo próprios, e o app os estiliza ou os desenha.
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 vem de nsh/base, o crate nsh-base em Rust. Em Rust, pagination() cria uma paginação com um setter por prop (.page(..), .total_pages(..)), .on_change(..) para a página que o usuário escolhe, os métodos de estilo tipados para a linha e .child(..) para o conteúdo do app.
#A página
A página pertence ao app: page é a página atual, a partir de 1, e onChange({ page }) informa a página que o usuário escolheu com um acionamento em uma página, em Anterior ou em Próxima. O app a define, carrega essa página e mostra suas linhas. totalPages é a quantidade de páginas; um page além da última mostra a última. A página atual não informa nada quando acionada, e o mesmo vale para Anterior na primeira página e Próxima na última.
#As páginas exibidas
Com visiblePages (no mínimo 5, o padrão), a linha mostra todas as páginas enquanto couberem. Acima disso, ela mostra a primeira e a última página, a atual com quantas páginas couberem de cada lado, (visiblePages - 3) / 2, e reticências para cada trecho de páginas omitido:
page de 20, visiblePages={7} |
Exibidas |
|---|---|
| 1 | 1 2 3 4 5 … 20 |
| 6 | 1 … 4 5 6 7 8 … 20 |
| 20 | 1 … 16 17 18 19 20 |
Dez mil páginas mostram os mesmos sete itens. Um visiblePages ímpar centraliza a página atual.
#Itens
Cada item recebe o itemClass e o itemStyle do app: a página atual é selected:, um item que não aceita acionamento é disabled:, e o item com o foco do teclado é focus:. O app organiza a linha com class (uma linha por padrão; gap-*, justify-*).
Sem renderItem, uma página mostra seu número, as reticências mostram …, e Anterior e Próxima mostram seus rótulos de acessibilidade (‹ e › se não houver rótulos). renderItem desenha o conteúdo de cada item a partir de um PaginationItemState:
| Campo | Tipo | Descrição |
|---|---|---|
kind |
"previous" | "page" | "ellipsis" | "next" |
O que o item é. |
page |
number |
O número de uma página; a página para a qual Anterior ou Próxima leva (a atual nas extremidades); a primeira página oculta de uma reticência. |
start, end |
number |
As páginas ocultas de uma reticência: start incluída, end (a próxima página exibida) não. |
current |
boolean |
A página atual. |
disabled |
boolean |
Não aceita acionamento: Anterior na primeira página, Próxima na última, tudo em uma paginação desabilitada. |
<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)}
/>
Uma reticência não aceita acionamento por conta própria: o que ela faz fica a cargo do app. Um Menu desenhado nela lista as páginas que ela oculta, como JumpMenu acima.
#Compacto
compact mostra apenas Anterior, os filhos e Próxima: um paginador para telas estreitas, com a página indicada nos filhos.
<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>
Em ambas as formas, os filhos ficam depois das páginas e antes de Próxima: uma contagem de páginas, um campo para ir a uma página.
#Props
| Prop | Rust | Tipo | Padrão | Descrição |
|---|---|---|---|---|
page |
.page(..) |
number |
obrigatório | A página atual, a partir de 1; o app a define em onChange. |
totalPages |
.total_pages(..) |
number |
obrigatório | Quantas páginas existem, no mínimo 1. |
visiblePages |
.visible_pages(..) |
number |
5 |
O máximo de páginas exibidas de uma vez, incluindo a primeira e a última; no mínimo 5. |
compact |
.compact(..) |
boolean |
false |
Apenas Anterior, os filhos e Próxima. |
disabled |
.disabled(..) |
boolean |
false |
Nenhum item aceita acionamento ou foco do teclado; os estilos de disabled: se aplicam, e os leitores de tela informam que estão esmaecidos. |
accessibilityLabel |
.accessibility_label(..) |
string |
nenhum | Como os leitores de tela chamam a navegação: o que ela pagina. |
previousAccessibilityLabel, nextAccessibilityLabel |
.previous_accessibility_label(..), .next_accessibility_label(..) |
string |
nenhum | O que os leitores de tela dizem para Anterior e Próxima, e o conteúdo deles sem renderItem. |
itemClass, itemStyle |
.item_class(..), .item_style(|s| ..) |
string, StyleProp<TextStyle> |
nenhum | A aparência de cada item: selected: na página atual, disabled:, focus:; os estilos de texto são repassados ao conteúdo. |
renderItem |
.render_item(|s: &PaginationItemState| ..) |
(item: PaginationItemState) => JSX.Element |
nenhum | Desenha o conteúdo de cada item a partir de seu estado. |
children |
.child(..) |
JSX.Element |
nenhum | O conteúdo do app depois das páginas e antes de Próxima. |
class, style |
métodos de estilo | string, StyleProp<ViewStyle> |
nenhum | A aparência e o layout da linha. |
#Eventos
| Evento | Rust | Descrição |
|---|---|---|
onChange |
.on_change(|page: &u32| ..) |
{ page }: a página que o usuário escolheu com uma página, Anterior ou Próxima. A página atual nunca é informada. |
#Movimento
A página atual se move de item para item conforme as classes selected: do app determinam: transition-colors em itemClass faz o destaque esmaecer da página deixada para a página alcançada. Uma mudança de página mantém cada item no lugar e altera o que os itens mostram, de modo que a linha nunca pisca; as transições do app ficam paradas com movimento reduzido (motion-reduce:transition-none).
#Acessibilidade
- A linha é um marco de navegação nomeado por
accessibilityLabel. - Cada página, Anterior e Próxima é um botão. A página atual é anunciada como a página atual (
aria-current="page"do ARIA) e está selecionada; Anterior na primeira página e Próxima na última ficam esmaecidos. - No macOS e na web, cada página, Anterior e Próxima é uma parada do foco do teclado, e Return e Espaço a acionam. Uma página acionada pelo teclado mantém o foco assim que o app a exibe, onde quer que ela caia na linha, e o mesmo vale para a primeira ou a última página quando Anterior ou Próxima chega a ela.
Veja Acessibilidade para rótulos, papéis e anúncios em geral.
#Notas de plataforma
- iOS, Android e Mac Catalyst: um acionamento é um toque que termina em um item.
- Desktops (macOS, Windows, Linux, GPUI) e web: um acionamento é um clique; no macOS e na web, Tab alcança os itens.
- Todas as plataformas desenham o que o app estiliza.