Trang này chưa có bản tiếng Việt. Nội dung dưới đây là bản tiếng Anh.
Composites
Table Pagination
The bar that goes under a Data Table: a page-size select, a record counter, a page counter, and four navigation buttons. Like the table it pairs with, every piece of state is controlled — it slices nothing and fetches nothing.
Paired with a table
57 invoices, sliced client-side. Change the page size and the page resets to 1 — that is your job, not the component's: it clamps what it displays into the valid range but never calls onPageChange on your behalf, so a stale page number in your state would keep slicing the wrong window. The same applies when a filter shrinks total.
| Invoice | Client | Issued | Amount | Status |
|---|---|---|---|---|
| INV-2001 | Aurora Logistics | Jun 1, 2026 | $480 | Paid |
| INV-2002 | Brightline Media | Jul 2, 2026 | $1,451 | Pending |
| INV-2003 | Cormorant Foods | Aug 3, 2026 | $2,422 | Overdue |
| INV-2004 | Delta Shipworks | Jun 4, 2026 | $3,393 | Pending |
| INV-2005 | Everest Analytics | Jul 5, 2026 | $4,364 | Paid |
| INV-2006 | Foundry Labs | Aug 6, 2026 | $5,335 | Pending |
| INV-2007 | Granite Partners | Jun 7, 2026 | $6,306 | Overdue |
| INV-2008 | Harbour Freight Co | Jul 8, 2026 | $7,277 | Pending |
| INV-2009 | Ironwood Studio | Aug 9, 2026 | $8,248 | Paid |
| INV-2010 | Aurora Logistics | Jun 10, 2026 | $9,219 | Pending |
| INV-2011 | Brightline Media | Jul 11, 2026 | $10,190 | Overdue |
| INV-2012 | Cormorant Foods | Aug 12, 2026 | $11,161 | Pending |
| INV-2013 | Delta Shipworks | Jun 13, 2026 | $12,132 | Paid |
| INV-2014 | Everest Analytics | Jul 14, 2026 | $13,103 | Pending |
| INV-2015 | Foundry Labs | Aug 15, 2026 | $14,074 | Overdue |
| INV-2016 | Granite Partners | Jun 16, 2026 | $15,045 | Pending |
| INV-2017 | Harbour Freight Co | Jul 17, 2026 | $16,016 | Paid |
| INV-2018 | Ironwood Studio | Aug 18, 2026 | $16,987 | Pending |
| INV-2019 | Aurora Logistics | Jun 19, 2026 | $17,958 | Overdue |
| INV-2020 | Brightline Media | Jul 20, 2026 | $929 | Pending |
What the component computes for you
There is deliberately no pageCount prop. Two places deriving the same number drift by one on the last record with depressing regularity, and the user-visible symptom is a Next page button that is still enabled after the table has run out of rows. So the bar derives everything from total, pageSize and page:
const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)))
const current = Math.min(Math.max(1, page), pageCount) // display clamp only
const start = total === 0 ? 0 : (current - 1) * pageSize + 1
const end = Math.min(current * pageSize, total)
const isFirst = current <= 1 // disables ⏮ and ◀
const isLast = current >= pageCount // disables ▶ and ⏭pageCountis never 0 — an empty table still reads as page 1 of 1 rather than 1 of 0.startcollapses to 0 when there is nothing to count, sorange()gets(0, 0, 0, unit)and a translation can special-case it.- The clamp is presentational. If your state says page 9 of a 3-page list, the bar shows page 3 and enables nothing forward — but your slice is still computed from 9. Clamp your own state too.
Page sizes
DEFAULT_PAGE_SIZE_OPTIONS is [20, 50, 100], fixed at three steps so that every table across the Comitor products offers the same choice. Override it per table with pageSizeOptions, or remove the control with hidePageSize when the size is the system's decision. Keep pageSize inside the option list: the select matches by value, so an unlisted size leaves the trigger blank. The options are the only numbers the bar prints outside its label table, so they follow the provider’s locale directly — a 1000 option reads 1,000 in English and 1.000 under locale="vi" — and fall back to plain digits when that locale is malformed: "vi_VN" gets the Vietnamese table from the provider, but Intl rejects it.
pageSizeOptions={[5, 10, 25]} · unitLabel="invoices"
hidePageSize
total={0} — start collapses to 0 and every button is disabled
Like DEFAULT_TABLE_PAGINATION_LABELS, this constant lives in a module that carries no 'use client' directive. Anything exported from a client module arrives in a Server Component as a client-reference proxy that reads back as undefined — no throw, no warning — and validating a ?size= search param on the server is exactly what you would want this constant for.
Function-valued labels
Eight of this component's ten strings are unreachable by any dedicated prop: the four sr-only button names, the aria-label of the size select, the visible prefix in front of it, and the two counters. Only region and unitLabel have a standalone prop of their own. All ten follow the tablePagination namespace of ComitorLocaleProvider, the same provider that translates the DataTable above it, so the two cannot end up in different languages — a half-translated screen is worse than a consistently untranslated one. labels is for the wording you want different, one key at a time.
Three of those keys — range, pageStatus and pageSizeSelect — are functions, not templates with placeholders. The reason is word order. The English default reads 1–20 of 57 items, the Vietnamese table 1–20 trên tổng 57 mục: the same four values, different order, different glue. A placeholder string would force every call site to concatenate on the application side, which is a reliable way to get the order wrong in the next language. A function hands the translator the raw values and lets it decide everything else — including branching on them, for plural rules or a zero case. The flip side: a function cannot cross from a Server Component into a client one, so declare these in a client component.
The numbers arrive raw; the table formats them. Both built-in tables group the digits for their language — the English one returns 2,001–3,000 of 123,456 items, the Vietnamese one 2.001–3.000 trên tổng 123.456 mục — and the first two rows below, counting 12,345 items, show it live. A range or pageStatus of your own receives the same raw values and has to format them itself — the German table in the code does it with Intl.NumberFormat. The grouping follows the table’s language, not the full locale: under locale="en-IN" the English table still prints 123,456, not 1,23,456 — override the two keys in the provider if that matters. Before 3.1.0 every language printed 123456, so a test that matches the raw digits needs the separators now.
No provider, no labels — English
<ComitorLocaleProvider locale="vi">
The same provider and a single key: labels={{ pageStatus: (p, n) => `${p} / ${n}` }} — the other nine stay in the provider's Vietnamese
The three rules, same as every label bag
- The prop is
Partial<>. The component merges it over the provider's table, so a single key is a legal override and the other nine keep the provider's language — as the third example above shows. - Interpolating keys are functions.
range(start, end, total, unitLabel),pageStatus(page, pageCount),pageSizeSelect(unitLabel). The other seven keys are plain strings. - A standalone prop beats
labels.unitLabelandlabelare per-content strings that differ from table to table ("invoices", "contacts"), whilelabels.unitLabelandlabels.regionare per-language defaults. Set the language once at app level, with the provider, and the content prop per table.
Every key of TablePaginationLabels, with its English default and what the Vietnamese table says:
| Prop | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
region | string | 'Pagination' | aria-label of the <nav>. The label prop overrides it per instance. Vietnamese: “Phân trang” |
unitLabel | string | 'items' | Default counting unit, passed to range() and pageSizeSelect(). The unitLabel prop overrides it per instance. Vietnamese: “mục” |
pageSizePrefix | string | 'Show' | Text before the size select. Hidden below the sm breakpoint. Vietnamese: “Hiển thị” |
pageSizeSelect | (unitLabel: string) => string | unit => `${unit} per page` | FUNCTION — aria-label of the size select. Vietnamese: “Số mục mỗi trang” |
range | (start, end, total, unitLabel) => string | '2,001–3,000 of 123,456 items' | FUNCTION — the record counter, shown for range(2001, 3000, 123456). It receives the numbers raw and the built-in tables group the digits. start and end are 1-based; start is 0 when total is 0. Vietnamese: “2.001–3.000 trên tổng 123.456 mục” |
pageStatus | (page, pageCount) => string | 'Page 3 of 124' | FUNCTION — the page counter between the previous and next buttons, shown for pageStatus(3, 124). Raw numbers in, grouped by the built-in tables like range. Vietnamese: “Trang 3 / 124” |
firstPage | string | 'First page' | sr-only name of the first-page button. Vietnamese: “Trang đầu” |
previousPage | string | 'Previous page' | sr-only name of the previous-page button. Vietnamese: “Trang trước” |
nextPage | string | 'Next page' | sr-only name of the next-page button. Vietnamese: “Trang sau” |
lastPage | string | 'Last page' | sr-only name of the last-page button. Vietnamese: “Trang cuối” |
Props
| Prop | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
pagebắt buộc | number | — | Current page, counting from 1. Clamped into 1…pageCount for display only. |
pageSizebắt buộc | number | — | Rows per page. Keep it inside pageSizeOptions, or the select renders blank. |
totalbắt buộc | number | — | Total record count AFTER filtering. The page count is derived from it. |
onPageChangebắt buộc | (page: number) => void | — | Called by the four navigation buttons with the page to move to. |
onPageSizeChangebắt buộc | (pageSize: number) => void | — | Called by the size select. Reset page to 1 here — the component will not do it for you. |
pageSizeOptions | readonly number[] | DEFAULT_PAGE_SIZE_OPTIONS | Choices in the size select, printed with the digit grouping of the provider's locale (1,000 · 1.000). The default is [20, 50, 100]. |
unitLabel | string | labels.unitLabel | What this table counts — "invoices", "contacts". Passed into range() and pageSizeSelect(). Beats labels.unitLabel. |
hidePageSize | boolean | false | Hides the prefix and the size select, leaving the count and the four buttons. |
label | string | labels.region | aria-label of the <nav>. Set it whenever one screen has more than one pagination bar. |
labels | Partial<TablePaginationLabels> | from provider | Display and screen-reader strings, merged over the tablePagination table of ComitorLocaleProvider — English without one. |
className | string | — | Classes on the <nav> wrapper. |
Accessibility
- The bar is a
<nav>named bylabel(falling back tolabels.region), so it appears in the landmark list. Give each bar its own name once a screen has two paginated tables. - Both counters are
role="status"live regions. Moving between pages is announced politely without stealing focus from the button that was just pressed — which matters, because that button may become disabled at the end of the range. - The four navigation buttons are icon-only, and each carries its name in an
sr-onlyspan while the icon itself isaria-hidden.firstPage,previousPage,nextPageandlastPagefollow the provider like the visible strings do; they are the four to check, because nothing on screen shows them. - First and last are
hidden sm:inline-flex— on a narrow screen only previous and next remain, so the row never wraps into an unreadable stack. Jumping to the last page is a convenience, not the only path. - The buttons are genuinely
disabledat the ends of the range, never merely dimmed — so keyboard and screen-reader users are told, rather than shown, that there is nothing further. - The size select is a real listbox with an
aria-labelfrompageSizeSelect(unitLabel), because its visible prefix is hidden below thesmbreakpoint and cannot be relied on as its name. - Both counters use
tabular-nums, so the digits do not change width as you page and the buttons beside them stay put.