Esta página se tradujo automáticamente y puede contener errores. Leer el original en inglés

Elementos

Paginación

Una paginación permite moverse entre las páginas de un conjunto: resultados de búsqueda, las filas de una tabla, un archivo. Muestra Anterior, las páginas alrededor de la actual (la primera y la última siempre, unos puntos suspensivos por cada tramo omitido), los hijos de la app y Siguiente. Se encarga del comportamiento: qué páginas se muestran, las pulsaciones, el teclado, Anterior y Siguiente desactivados en los extremos, el punto de referencia de navegación y la página actual para los lectores de pantalla. El aspecto es responsabilidad de la app: los elementos no tienen color, tamaño ni etiqueta propios, y la app los estiliza o los dibuja.

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 proviene de nsh/base, el crate nsh-base de Rust. En Rust, pagination() crea uno con un setter por prop (.page(..), .total_pages(..)), .on_change(..) para la página que elige el usuario, los métodos de estilo tipados para la fila y .child(..) para el contenido de la app.

#La página

La página pertenece a la app: page es la actual, desde 1, y onChange({ page }) informa de la página que el usuario eligió con una pulsación sobre una página, sobre Anterior o sobre Siguiente. La app la establece, carga esa página y muestra sus filas. totalPages es cuántas hay; un page mayor que la última muestra la última. La página actual no informa de nada al pulsarla, y tampoco lo hacen Anterior en la primera página ni Siguiente en la última.

#Las páginas mostradas

Con visiblePages (al menos 5, el valor por defecto) la fila muestra todas las páginas mientras quepan. Superado ese límite, muestra la primera y la última página, la actual con tantas páginas a cada lado como quepan, (visiblePages - 3) / 2, y unos puntos suspensivos por cada tramo de páginas omitido:

page de 20, visiblePages={7} Se muestra
1 1 2 3 4 5 … 20
6 1 … 4 5 6 7 8 … 20
20 1 … 16 17 18 19 20

Diez mil páginas muestran los mismos siete elementos. Un visiblePages impar centra la página actual.

#Elementos

Cada elemento recibe los itemClass y itemStyle de la app: la página actual es selected:, un elemento que no admite pulsación es disabled:, el elemento con el foco del teclado es focus:. La app es quien organiza la fila con class (una fila por defecto; gap-*, justify-*).

Sin renderItem, una página muestra su número, los puntos suspensivos muestran …, y Anterior y Siguiente muestran sus etiquetas de accesibilidad (‹ y › si no las hay). renderItem dibuja el contenido de cada elemento a partir de un PaginationItemState:

Campo Tipo Descripción
kind "previous" | "page" | "ellipsis" | "next" Qué es el elemento.
page number El número de una página; la página a la que lleva Anterior o Siguiente (la actual en los extremos); la primera página oculta de unos puntos suspensivos.
start, end number Las páginas ocultas de unos puntos suspensivos: start incluida, end (la siguiente página mostrada) no.
current boolean La página actual.
disabled boolean No admite pulsación: Anterior en la primera página, Siguiente en la última, todo en una paginación desactivada.
<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)}
/>

Unos puntos suspensivos no admiten pulsación por sí mismos: lo que hagan depende de la app. Un Menu dibujado en ellos enumera las páginas que ocultan, como hace JumpMenu más arriba.

#Compacto

compact muestra solo Anterior, los hijos y Siguiente: un paginador para pantallas estrechas, con la página indicada en los hijos.

<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>

En ambas formas, los hijos se sitúan después de las páginas y antes de Siguiente: un contador de páginas, un campo para ir a una página.

#Props

Prop Rust Tipo Valor por defecto Descripción
page .page(..) number obligatorio La página actual, desde 1; la app la establece en onChange.
totalPages .total_pages(..) number obligatorio Cuántas páginas hay, al menos 1.
visiblePages .visible_pages(..) number 5 El máximo de páginas mostradas a la vez, incluidas la primera y la última; al menos 5.
compact .compact(..) boolean false Solo Anterior, los hijos y Siguiente.
disabled .disabled(..) boolean false Ningún elemento admite pulsación ni foco del teclado; se aplican los estilos de disabled:, y los lectores de pantalla indican que están atenuados.
accessibilityLabel .accessibility_label(..) string ninguno Cómo llaman los lectores de pantalla a la navegación: lo que se recorre con ella.
previousAccessibilityLabel, nextAccessibilityLabel .previous_accessibility_label(..), .next_accessibility_label(..) string ninguno Lo que dicen los lectores de pantalla para Anterior y Siguiente, y su contenido sin renderItem.
itemClass, itemStyle .item_class(..), .item_style(|s| ..) string, StyleProp<TextStyle> ninguno El aspecto de cada elemento: selected: en la página actual, disabled:, focus:; los estilos de texto se transmiten a su contenido.
renderItem .render_item(|s: &PaginationItemState| ..) (item: PaginationItemState) => JSX.Element ninguno Dibuja el contenido de cada elemento a partir de su estado.
children .child(..) JSX.Element ninguno El contenido de la app después de las páginas, antes de Siguiente.
class, style métodos de estilo string, StyleProp<ViewStyle> ninguno El aspecto y la disposición de la fila.

#Eventos

Evento Rust Descripción
onChange .on_change(|page: &u32| ..) { page }: la página que el usuario eligió con una página, Anterior o Siguiente. La página actual nunca se notifica.

#Movimiento

La página actual se desplaza de un elemento a otro según indican las clases selected: de la app: transition-colors en itemClass funde el resaltado desde la página que se deja hasta la página a la que se llega. Un cambio de página mantiene cada elemento en su sitio y cambia lo que muestran los elementos, de modo que la fila nunca parpadea; las transiciones de la app se detienen con movimiento reducido (motion-reduce:transition-none).

#Accesibilidad

  • La fila es un punto de referencia de navegación cuyo nombre proviene de accessibilityLabel.
  • Cada página, Anterior y Siguiente es un botón. La página actual se anuncia como la página actual (el aria-current="page" de ARIA) y está seleccionada; Anterior en la primera página y Siguiente en la última están atenuados.
  • En macOS y la web, cada página, Anterior y Siguiente es una parada del foco del teclado, y Retorno y Espacio la pulsan. Una página pulsada desde el teclado conserva el foco una vez que la app la muestra, vaya a parar donde vaya en la fila, y lo mismo ocurre con la primera o la última página cuando Anterior o Siguiente llegan a ella.

Consulta Accesibilidad para ver etiquetas, roles y anuncios en general.

#Notas de plataforma

  • iOS, Android y Mac Catalyst: una pulsación es un toque que termina sobre un elemento.
  • Escritorio (macOS, Windows, Linux, GPUI) y la web: una pulsación es un clic; en macOS y la web, Tab llega a los elementos.
  • Todas las plataformas dibujan lo que la app estiliza.