// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file useTablePagination.tsx * @input React, Pagination, Table plugin types * @output Exports useTablePagination hook and config type * @position Pagination plugin; consumed by Table via plugins prop * * Returns a `TablePlugin` directly (same pattern as useTableSortable, * useTableSelection, useTableColumnSettings). * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/Table/plugins/pagination/index.ts (exports) * - /packages/core/src/Table/index.ts (exports) */ import { useMemo, useRef, type ReactNode } from 'react'; import * as stylex from "../../../style-runtime.js"; import "../../../theme/tokens.stylex.js"; import { spacingVars } from "../../../theme/tokens.stylex.js"; import { Pagination } from "../../../Pagination/index.js"; import type { PaginationProps } from "../../../Pagination/index.js"; import type { TablePlugin } from "../../types.js"; import { useTranslator } from "../../../i18n/index.js"; // ============================================================================= // Styles // ============================================================================= const styles = { wrapper: { k1xSpc: "x78zum5", $$css: true as const }, marginTop: { keoZOQ: "xtbrsbv", $$css: true as const }, marginBottom: { k1K539: "x1p37lm5", $$css: true as const }, alignStart: { kjj79g: "x1nhvcw1", $$css: true as const }, alignCenter: { kjj79g: "xl56j7k", $$css: true as const }, alignEnd: { kjj79g: "x13a6bvl", $$css: true as const } }; // ============================================================================= // Config Type // ============================================================================= /** * Configuration for useTablePagination. * * The consumer owns all state. This hook renders pagination controls * around the table via `transformTableContext`. * * @example * ``` * const [page, setPage] = useState(1); * const plugin = useTablePagination({ * page, * onPageChange: setPage, * totalItems: data.length, * pageSize: 10, * }); * * ``` */ export interface UseTablePaginationConfig { // --- Core (required) --- /** Current page number (1-based). */ page: number; /** Called when the page changes. Consumer updates their own state. */ onPageChange: (page: number) => void; // --- Data shape (provide one) --- /** * Total number of items across all pages. * Used to calculate total page count and "X\u2013Y of Z" display. * Takes precedence over `totalPages` if both are provided. */ totalItems?: number; /** * Total number of pages. Use when you know the page count but not item count. */ totalPages?: number; /** * Whether more pages exist after the current one. * Use for cursor-based pagination where the total is unknown. * Mutually exclusive with totalItems/totalPages. */ hasMore?: boolean; // --- Page size --- /** * Number of items per page. * @default 10 */ pageSize?: number; /** * Called when the user changes the page size via the page size selector. * When provided alongside `pageSizeOptions`, a page size dropdown is shown. */ onPageSizeChange?: (pageSize: number) => void; /** * Available page size options. Shows a page size selector when provided. * @example * ``` * [10, 25, 50, 100] * ``` */ pageSizeOptions?: number[]; // --- Display --- /** * Visual variant for the pagination controls. * Passed through to Pagination. * @default 'pages' */ variant?: 'pages' | 'count' | 'compact' | 'dots' | 'none'; /** * Size of the pagination controls. * @default 'md' */ size?: 'sm' | 'md'; /** * Where to render the pagination controls relative to the table. * - 'below' \u2014 renders pagination after the table (default) * - 'above' \u2014 renders pagination before the table * - 'both' \u2014 renders pagination above and below the table * - 'none' \u2014 does not auto-render; use Pagination manually * * @default 'below' */ position?: 'below' | 'above' | 'both' | 'none'; /** * Horizontal alignment of the pagination controls within their container. * - 'start' \u2014 left-aligned * - 'center' \u2014 centered (default) * - 'end' \u2014 right-aligned * * @default 'center' */ align?: 'start' | 'center' | 'end'; // --- Accessibility --- /** * Accessible label for the pagination nav landmark. * When `position` is 'both', the two navs get distinct names by suffixing * this label — "{label} (top)" above and "{label} (bottom)" below — so * same-type landmarks stay distinguishable to assistive technology. * @default 'Table pagination' */ label?: string; } // ============================================================================= // Hook // ============================================================================= /** * Pagination table plugin \u2014 renders Pagination controls around the table. * * Returns a `TablePlugin` directly (same pattern as `useTableSortable`, * `useTableSelection`, `useTableColumnSettings`). * * @example * ``` * const [page, setPage] = useState(1); * const plugin = useTablePagination({ * page, * onPageChange: setPage, * totalItems: data.length, * pageSize: 10, * }); *
* ``` */ export function useTablePagination>(config: UseTablePaginationConfig): TablePlugin { const t = useTranslator(); const { page, onPageChange, totalItems, totalPages: totalPagesProp, hasMore, pageSize: pageSizeConfig = 10, onPageSizeChange, pageSizeOptions, variant = 'pages', size = 'md', position = 'below', align = 'center', label: labelFromProps } = config; const label = labelFromProps ?? t('@astryx.table.pagination.label'); // position='both' renders two nav landmarks; suffix the base label so each // gets a unique accessible name (axe landmark-unique). const labelAbove = t('@astryx.table.pagination.labelAbove', { label }); const labelBelow = t('@astryx.table.pagination.labelBelow', { label }); // Same guard as Pagination itself: 0/NaN/negative pageSize would produce an // Infinity/NaN totalPages here, which bypasses Pagination's own coercion // because it is passed down as the explicit totalPages prop. const pageSize = Number.isFinite(pageSizeConfig) ? Math.max(1, Math.floor(pageSizeConfig)) : 10; const computedTotalPages = totalPagesProp ?? (totalItems != null ? Math.ceil(totalItems / pageSize) : undefined); // Build Pagination props const paginationProps: PaginationProps = { page, onChange: onPageChange, totalItems, totalPages: computedTotalPages, hasMore, pageSize, onPageSizeChange, pageSizeOptions, variant, size, label }; // Keep current config in a ref so the plugin can read the latest values // without needing to recreate the plugin object on every change. const configRef = useRef({ paginationProps, position, align, labelAbove, labelBelow }); configRef.current = { paginationProps, position, align, labelAbove, labelBelow }; // Stable plugin object \u2014 created once, reads config via ref. return useMemo((): TablePlugin => ({ transformTableContext(children: ReactNode): ReactNode { const { position: pos, paginationProps: props, align: a, labelAbove: above, labelBelow: below } = configRef.current; if (pos === 'none') { return children; } // Don't render pagination when there's only one page and no more data. const resolvedTotalPages = props.totalPages ?? (props.totalItems != null && props.pageSize != null ? Math.ceil(props.totalItems / props.pageSize) : undefined); const isSinglePage = resolvedTotalPages === 1 && props.hasMore !== true; if (isSinglePage) { return children; } const makeWrapper = (side: 'above' | 'below') =>
; return <> {(pos === 'above' || pos === 'both') && makeWrapper('above')} {children} {(pos === 'below' || pos === 'both') && makeWrapper('below')} ; } }), []); }