fix(pagination): window totalItems

This commit is contained in:
Eric Liu 2025-04-25 08:54:52 -07:00
commit 0575737913
7 changed files with 102 additions and 20 deletions

View file

@ -2681,24 +2681,25 @@ None.
### Props
| Prop name | Required | Kind | Reactive | Type | Default value | Description |
| :-------------------- | :------- | :--------------- | :------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| pageSize | No | <code>let</code> | Yes | <code>number</code> | <code>10</code> | Specify the number of items to display in a page |
| page | No | <code>let</code> | Yes | <code>number</code> | <code>1</code> | Specify the current page index |
| totalItems | No | <code>let</code> | No | <code>number</code> | <code>0</code> | Specify the total number of items |
| disabled | No | <code>let</code> | No | <code>boolean</code> | <code>false</code> | Set to `true` to disable the pagination |
| forwardText | No | <code>let</code> | No | <code>string</code> | <code>"Next page"</code> | Specify the forward button text |
| backwardText | No | <code>let</code> | No | <code>string</code> | <code>"Previous page"</code> | Specify the backward button text |
| itemsPerPageText | No | <code>let</code> | No | <code>string</code> | <code>"Items per page:"</code> | Specify the items per page text |
| itemText | No | <code>let</code> | No | <code>(min: number, max: number) => string</code> | <code>(min, max) => \`${min}${max} item${max === 1 ? "" : "s"}\`</code> | Override the item text |
| itemRangeText | No | <code>let</code> | No | <code>(min: number, max: number, total: number) => string</code> | <code>(min, max, total) => \`${min}${max} of ${total} item${max === 1 ? "" : "s"}\`</code> | Override the item range text |
| pageInputDisabled | No | <code>let</code> | No | <code>boolean</code> | <code>false</code> | Set to `true` to disable the page input |
| pageSizeInputDisabled | No | <code>let</code> | No | <code>boolean</code> | <code>false</code> | Set to `true` to disable the page size input |
| pageSizes | No | <code>let</code> | No | <code>ReadonlyArray<number></code> | <code>[10]</code> | Specify the available page sizes |
| pagesUnknown | No | <code>let</code> | No | <code>boolean</code> | <code>false</code> | Set to `true` if the number of pages is unknown |
| pageText | No | <code>let</code> | No | <code>(page: number) => string</code> | <code>(page) => \`page ${page}\`</code> | Override the page text |
| pageRangeText | No | <code>let</code> | No | <code>(current: number, total: number) => string</code> | <code>(current, total) => \`of ${total} page${total === 1 ? "" : "s"}\`</code> | Override the page range text |
| id | No | <code>let</code> | No | <code>string</code> | <code>"ccs-" + Math.random().toString(36)</code> | Set an id for the top-level element |
| Prop name | Required | Kind | Reactive | Type | Default value | Description |
| :-------------------- | :------- | :--------------- | :------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| pageSize | No | <code>let</code> | Yes | <code>number</code> | <code>10</code> | Specify the number of items to display in a page |
| page | No | <code>let</code> | Yes | <code>number</code> | <code>1</code> | Specify the current page index |
| totalItems | No | <code>let</code> | No | <code>number</code> | <code>0</code> | Specify the total number of items |
| pageWindow | No | <code>let</code> | No | <code>number</code> | <code>1000</code> | If `totalItems` is a large number, it can affect the<br />rendering performance of this component since its value<br />is used to calculate the number of pages in the native<br />select dropdown. This value creates a small window of<br />pages rendered around the current page. By default,<br />a maximum of 1000 select items are rendered. |
| disabled | No | <code>let</code> | No | <code>boolean</code> | <code>false</code> | Set to `true` to disable the pagination |
| forwardText | No | <code>let</code> | No | <code>string</code> | <code>"Next page"</code> | Specify the forward button text |
| backwardText | No | <code>let</code> | No | <code>string</code> | <code>"Previous page"</code> | Specify the backward button text |
| itemsPerPageText | No | <code>let</code> | No | <code>string</code> | <code>"Items per page:"</code> | Specify the items per page text |
| itemText | No | <code>let</code> | No | <code>(min: number, max: number) => string</code> | <code>(min, max) => \`${min}${max} item${max === 1 ? "" : "s"}\`</code> | Override the item text |
| itemRangeText | No | <code>let</code> | No | <code>(min: number, max: number, total: number) => string</code> | <code>(min, max, total) => \`${min}${max} of ${total} item${max === 1 ? "" : "s"}\`</code> | Override the item range text |
| pageInputDisabled | No | <code>let</code> | No | <code>boolean</code> | <code>false</code> | Set to `true` to disable the page input |
| pageSizeInputDisabled | No | <code>let</code> | No | <code>boolean</code> | <code>false</code> | Set to `true` to disable the page size input |
| pageSizes | No | <code>let</code> | No | <code>ReadonlyArray<number></code> | <code>[10]</code> | Specify the available page sizes |
| pagesUnknown | No | <code>let</code> | No | <code>boolean</code> | <code>false</code> | Set to `true` if the number of pages is unknown |
| pageText | No | <code>let</code> | No | <code>(page: number) => string</code> | <code>(page) => \`page ${page}\`</code> | Override the page text |
| pageRangeText | No | <code>let</code> | No | <code>(current: number, total: number) => string</code> | <code>(current, total) => \`of ${total} page${total === 1 ? "" : "s"}\`</code> | Override the page range text |
| id | No | <code>let</code> | No | <code>string</code> | <code>"ccs-" + Math.random().toString(36)</code> | Set an id for the top-level element |
### Slots

View file

@ -10048,6 +10048,18 @@
"constant": false,
"reactive": false
},
{
"name": "pageWindow",
"kind": "let",
"description": "If `totalItems` is a large number, it can affect the\nrendering performance of this component since its value\nis used to calculate the number of pages in the native\nselect dropdown. This value creates a small window of\npages rendered around the current page. By default,\na maximum of 1000 select items are rendered.",
"type": "number",
"value": "1000",
"isFunction": false,
"isFunctionDeclaration": false,
"isRequired": false,
"constant": false,
"reactive": false
},
{
"name": "disabled",
"kind": "let",

View file

@ -19,6 +19,19 @@ components: ["Pagination", "PaginationSkeleton"]
<Pagination totalItems={102} pageSizes="{[16, 36, 99]}" pageSize="{36}" />
## Page window
The number of native select items rendered is derived from the value of `totalItems`.
If `totalItems` is a very large number, this can impact rendering performance since
thousands of elements may be rendered. By default, the window of rendered items is
capped at 1,000. For example, if `totalItems=100_000` and the `pageSize=10`,
1,000 select options are rendered.
Use the `pageWindow` prop to increase this value.
<Pagination totalItems={100_000} pageSizes={[10, 15, 20]} />
## Hidden page input
<Pagination totalItems={102} pageInputDisabled />

View file

@ -12,6 +12,16 @@
/** Specify the total number of items */
export let totalItems = 0;
/**
* If `totalItems` is a large number, it can affect the
* rendering performance of this component since its value
* is used to calculate the number of pages in the native
* select dropdown. This value creates a small window of
* pages rendered around the current page. By default,
* a maximum of 1000 select items are rendered.
*/
export let pageWindow = 1000;
/** Set to `true` to disable the pagination */
export let disabled = false;
@ -81,6 +91,21 @@
const dispatch = createEventDispatcher();
/**
* Returns a subset of page numbers centered around the current page to prevent
* performance issues with large datasets. Creates a capped window of pages
* instead of potentially thousands, improving render speed and memory usage.
* @param {number} currentPage - The current page number
* @param {number} totalPages - Total number of pages
* @param {number} window - How many pages to show before/after current page
* @returns {number[]} Array of page numbers to display
*/
function getWindowedPages(currentPage, totalPages, window) {
const start = Math.max(1, currentPage - window);
const end = Math.min(totalPages, currentPage + window);
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
}
afterUpdate(() => {
if (page > totalPages) {
page = totalPages;
@ -89,7 +114,7 @@
$: dispatch("update", { pageSize, page });
$: totalPages = Math.max(Math.ceil(totalItems / pageSize), 1);
$: selectItems = Array.from({ length: totalPages }, (_, i) => i);
$: selectItems = getWindowedPages(page, totalPages, pageWindow);
$: backButtonDisabled = disabled || page === 1;
$: forwardButtonDisabled = disabled || page === totalPages;
</script>
@ -146,7 +171,7 @@
bind:selected={page}
>
{#each selectItems as size, i (size)}
<SelectItem value={size + 1} text={(size + 1).toString()} />
<SelectItem value={size} text={size.toString()} />
{/each}
</Select>
<span class:bx--pagination__text={true}>

View file

@ -11,6 +11,7 @@
export let pageSizeInputDisabled = false;
export let pageSize = 10;
export let pageSizes: ReadonlyArray<number> = [10];
export let pageWindow: undefined | number = undefined;
export let pagesUnknown = false;
</script>
@ -23,6 +24,7 @@
{itemsPerPageText}
{pageInputDisabled}
{pageSizeInputDisabled}
{pageWindow}
bind:pageSize
{pageSizes}
{pagesUnknown}

View file

@ -196,4 +196,22 @@ describe("Pagination", () => {
expect(screen.getByText("00 of 0 items")).toBeInTheDocument();
});
it("renders a cap of 1000 page numbers by default", () => {
render(Pagination, {
props: { totalItems: 100_000 },
});
const pageNumbers = screen.getByLabelText(/Page number, of 10000 pages/);
expect(pageNumbers).toHaveLength(1_000 + 1);
});
it("renders a custom page window", () => {
render(Pagination, {
props: { totalItems: 100_000, pageWindow: 100 },
});
const pageNumbers = screen.getByLabelText(/Page number, of 10000 pages/);
expect(pageNumbers).toHaveLength(100 + 1);
});
});

View file

@ -16,6 +16,17 @@ type $Props = {
*/
totalItems?: number;
/**
* If `totalItems` is a large number, it can affect the
* rendering performance of this component since its value
* is used to calculate the number of pages in the native
* select dropdown. This value creates a small window of
* pages rendered around the current page. By default,
* a maximum of 1000 select items are rendered.
* @default 1000
*/
pageWindow?: number;
/**
* Set to `true` to disable the pagination
* @default false