From aee6608ba027640165c31749b81fe1cfb25beaff Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Tue, 28 Jul 2026 19:46:32 +0200 Subject: [PATCH] feat: add compact pages to pagination --- pages/pagination/permutations.page.tsx | 2 + pages/pagination/simple.page.tsx | 21 +++++ .../__snapshots__/documenter.test.ts.snap | 66 ++++++++++++++- .../test-utils-selectors.test.tsx.snap | 1 + src/i18n/messages-types.ts | 5 ++ src/i18n/messages/all.ar.json | 3 +- src/i18n/messages/all.de.json | 3 +- src/i18n/messages/all.en-GB.json | 3 +- src/i18n/messages/all.en.json | 3 +- src/i18n/messages/all.es.json | 3 +- src/i18n/messages/all.fr.json | 3 +- src/i18n/messages/all.id.json | 3 +- src/i18n/messages/all.it.json | 3 +- src/i18n/messages/all.ja.json | 3 +- src/i18n/messages/all.ko.json | 3 +- src/i18n/messages/all.pt-BR.json | 3 +- src/i18n/messages/all.tr.json | 5 +- src/i18n/messages/all.zh-CN.json | 3 +- src/i18n/messages/all.zh-TW.json | 3 +- .../__tests__/pagination-compact.test.tsx | 74 +++++++++++++++++ src/pagination/index.tsx | 51 ++++++------ src/pagination/interfaces.ts | 18 +++++ src/pagination/internal.tsx | 81 ++++++++++++------- src/pagination/styles.scss | 11 ++- src/pagination/test-classes/styles.scss | 8 ++ src/test-utils/dom/pagination/index.ts | 10 ++- 26 files changed, 322 insertions(+), 70 deletions(-) create mode 100644 src/pagination/__tests__/pagination-compact.test.tsx create mode 100644 src/pagination/test-classes/styles.scss diff --git a/pages/pagination/permutations.page.tsx b/pages/pagination/permutations.page.tsx index 182a90151e..15dcaec4ef 100644 --- a/pages/pagination/permutations.page.tsx +++ b/pages/pagination/permutations.page.tsx @@ -19,6 +19,7 @@ const paginationLabels: PaginationProps.Labels = { const permutations = createPermutations([ { + pagesVariant: ['compact'], currentPageIndex: [7], pagesCount: [15], disabled: [true], @@ -26,6 +27,7 @@ const permutations = createPermutations([ jumpToPage: [undefined, { loading: false }], }, { + pagesVariant: ['compact', 'normal'], currentPageIndex: [1, 6, 15], pagesCount: [15], openEnd: [true, false], diff --git a/pages/pagination/simple.page.tsx b/pages/pagination/simple.page.tsx index 68a9a460ba..6c4b405722 100644 --- a/pages/pagination/simple.page.tsx +++ b/pages/pagination/simple.page.tsx @@ -24,6 +24,8 @@ const i18nStrings: PaginationProps.I18nStrings = { export default function PaginationSimplePage() { const [basicPageIndex, setBasicPageIndex] = useState(1); const [jumpPageIndex, setJumpPageIndex] = useState(1); + const [compactPageIndex, setCompactPageIndex] = useState(1); + const [compactOpenEndPageIndex, setCompactOpenEndPageIndex] = useState(1); return ( @@ -46,6 +48,25 @@ export default function PaginationSimplePage() { jumpToPage={{}} onChange={event => setJumpPageIndex(event.detail.currentPageIndex)} /> + +

Compact pages

+ setCompactPageIndex(event.detail.currentPageIndex)} + /> + +

Compact pages with open end

+ setCompactOpenEndPageIndex(event.detail.currentPageIndex)} + />
); diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index e872e9fbab..93ce98d99c 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -20420,6 +20420,13 @@ from changing page before items are loaded.", "type": "boolean", }, { + "description": "An object containing all the necessary localized strings required by the component: +* \`jumpToPageInputLabel\` (string) - Accessible label for the jump-to-page number input. +* \`jumpToPageError\` (string) - Error message displayed when the entered page number is invalid. +* \`jumpToPageLoadingText\` (string) - Loading text displayed while the jump-to-page action is in progress. +* \`pagesCompactText\` ((options: { currentPage: number; pagesCount: number; openEnd: boolean }) => string) - + Provides the visible text for compact pages, for example \`3 of 12\`, or \`3 of 12+\` when \`openEnd\` is \`true\`. + Receives the current page, page count, and whether pagination is open-ended.", "i18nTag": true, "inlineType": { "name": "PaginationProps.I18nStrings", @@ -20439,6 +20446,22 @@ from changing page before items are loaded.", "optional": true, "type": "string", }, + { + "inlineType": { + "name": "(options: { currentPage: number; pagesCount: number; openEnd: boolean; }) => string", + "parameters": [ + { + "name": "options", + "type": "{ currentPage: number; pagesCount: number; openEnd: boolean; }", + }, + ], + "returnType": "string", + "type": "function", + }, + "name": "pagesCompactText", + "optional": true, + "type": "((options: { currentPage: number; pagesCount: number; openEnd: boolean; }) => string)", + }, ], "type": "object", }, @@ -20474,11 +20497,29 @@ never disabled. When the user clicks on it but there are no more items to show, "type": "boolean", }, { - "description": "Sets the total number of pages. Only positive integers are allowed.", + "description": "Sets the total number of pages. When \`openEnd\` is \`true\`, this is the number of pages currently available. +Only positive integers are allowed.", "name": "pagesCount", "optional": false, "type": "number", }, + { + "defaultValue": "'normal'", + "description": "Specifies how pages are displayed: +* \`normal\` - Displays page number buttons. For larger page ranges, the displayed range is truncated with ellipses. +* \`compact\` - Displays the current page and page count between the previous and next buttons. When \`openEnd\` is \`true\`, a plus sign after the page count indicates that more pages are available.", + "inlineType": { + "name": "PaginationProps.PagesVariant", + "type": "union", + "values": [ + "normal", + "compact", + ], + }, + "name": "pagesVariant", + "optional": true, + "type": "string", + }, ], "regions": [], "releaseStatus": "stable", @@ -38369,6 +38410,20 @@ To find a specific item use the \`findBreadcrumbLink(n)\` function as chaining \ ], }, }, + { + "description": "Returns the visible text for compact pages (for example, \`3 of 12\` or \`3 of 12+\`).", + "name": "findPagesCompactText", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, { "name": "findPreviousPageButton", "parameters": [], @@ -50063,6 +50118,15 @@ To find a specific item use the \`findBreadcrumbLink(n)\` function as chaining \ ], }, }, + { + "description": "Returns the visible text for compact pages (for example, \`3 of 12\` or \`3 of 12+\`).", + "name": "findPagesCompactText", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, { "name": "findPreviousPageButton", "parameters": [], diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap index 16e98a9c41..64b8f245f2 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap @@ -499,6 +499,7 @@ exports[`test-utils selectors 1`] = ` "awsui_jump-to-page-input_fvjdu", "awsui_jump-to-page_fvjdu", "awsui_page-number_fvjdu", + "awsui_pages-compact-text_lmd86", "awsui_root_fvjdu", ], "panel-layout": [ diff --git a/src/i18n/messages-types.ts b/src/i18n/messages-types.ts index 4b02069f50..394f2a3bb0 100644 --- a/src/i18n/messages-types.ts +++ b/src/i18n/messages-types.ts @@ -335,6 +335,11 @@ export interface I18nFormatArgTypes { 'i18nStrings.jumpToPageInputLabel': never; 'i18nStrings.jumpToPageError': never; 'i18nStrings.jumpToPageLoadingText': never; + 'i18nStrings.pagesCompactText': { + openEnd: string; + currentPage: string | number; + pagesCount: string | number; + }; }; 'panel-resize-handle': { 'i18nStrings.resizeHandleAriaLabel': never; diff --git a/src/i18n/messages/all.ar.json b/src/i18n/messages/all.ar.json index 09080b3c7f..4eea153b54 100644 --- a/src/i18n/messages/all.ar.json +++ b/src/i18n/messages/all.ar.json @@ -256,7 +256,8 @@ "ariaLabels.jumpToPageButtonLabel": "الانتقال إلى الصفحة", "i18nStrings.jumpToPageInputLabel": "صفحة", "i18nStrings.jumpToPageError": "الصفحة خارج نطاق الوصول. جارٍ عرض آخر صفحة متاحة.", - "i18nStrings.jumpToPageLoadingText": "جار التحميل" + "i18nStrings.jumpToPageLoadingText": "جار التحميل", + "i18nStrings.pagesCompactText": "{openEnd, select, true {{currentPage} من {pagesCount} +} false {{currentPage} من {pagesCount}} other {}}" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "مقبض تغيير حجم اللوحة", diff --git a/src/i18n/messages/all.de.json b/src/i18n/messages/all.de.json index 726fecca6a..ff0ef98c5b 100644 --- a/src/i18n/messages/all.de.json +++ b/src/i18n/messages/all.de.json @@ -256,7 +256,8 @@ "ariaLabels.jumpToPageButtonLabel": "Zur Seite springen", "i18nStrings.jumpToPageInputLabel": "Seite", "i18nStrings.jumpToPageError": "Seite ist außerhalb des Bereichs. Zeigt die letzte verfügbare Seite an.", - "i18nStrings.jumpToPageLoadingText": "Wird geladen" + "i18nStrings.jumpToPageLoadingText": "Wird geladen", + "i18nStrings.pagesCompactText": "{openEnd, select, true {{currentPage} von {pagesCount}+} false {{currentPage} von {pagesCount}} other {}}" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Größe des Panels ändern", diff --git a/src/i18n/messages/all.en-GB.json b/src/i18n/messages/all.en-GB.json index 8c34a1e40a..0631e598b6 100644 --- a/src/i18n/messages/all.en-GB.json +++ b/src/i18n/messages/all.en-GB.json @@ -256,7 +256,8 @@ "ariaLabels.jumpToPageButtonLabel": "Jump to page", "i18nStrings.jumpToPageInputLabel": "Page", "i18nStrings.jumpToPageError": "Page out of range. Showing the last available page.", - "i18nStrings.jumpToPageLoadingText": "Loading" + "i18nStrings.jumpToPageLoadingText": "Loading", + "i18nStrings.pagesCompactText": "{openEnd, select, true {{currentPage} of {pagesCount}+} false {{currentPage} of {pagesCount}} other {}}" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Panel resize handle", diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index b784c4609b..dbac8f9e28 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -257,7 +257,8 @@ "ariaLabels.jumpToPageButtonLabel": "Jump to page", "i18nStrings.jumpToPageInputLabel": "Page", "i18nStrings.jumpToPageError": "Page out of range. Showing last available page.", - "i18nStrings.jumpToPageLoadingText": "Loading" + "i18nStrings.jumpToPageLoadingText": "Loading", + "i18nStrings.pagesCompactText": "{openEnd, select, true {{currentPage} of {pagesCount}+} false {{currentPage} of {pagesCount}} other {}}" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Panel resize handle", diff --git a/src/i18n/messages/all.es.json b/src/i18n/messages/all.es.json index db3fed3363..cdcd538ead 100644 --- a/src/i18n/messages/all.es.json +++ b/src/i18n/messages/all.es.json @@ -256,7 +256,8 @@ "ariaLabels.jumpToPageButtonLabel": "Ir a la página", "i18nStrings.jumpToPageInputLabel": "Página", "i18nStrings.jumpToPageError": "Página fuera de rango. Se muestra la última página disponible.", - "i18nStrings.jumpToPageLoadingText": "Cargando" + "i18nStrings.jumpToPageLoadingText": "Cargando", + "i18nStrings.pagesCompactText": "{openEnd, select, true {{currentPage} de {pagesCount}+} false {{currentPage} de {pagesCount}} other {}}" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Controlador de cambio del tamaño del panel", diff --git a/src/i18n/messages/all.fr.json b/src/i18n/messages/all.fr.json index 10a2455e39..8097761067 100644 --- a/src/i18n/messages/all.fr.json +++ b/src/i18n/messages/all.fr.json @@ -256,7 +256,8 @@ "ariaLabels.jumpToPageButtonLabel": "Aller à la page", "i18nStrings.jumpToPageInputLabel": "Page", "i18nStrings.jumpToPageError": "Page hors de portée. Affichage de la dernière page disponible.", - "i18nStrings.jumpToPageLoadingText": "Chargement en cours" + "i18nStrings.jumpToPageLoadingText": "Chargement en cours", + "i18nStrings.pagesCompactText": "{openEnd, select, true {{currentPage} sur plus de {pagesCount}} false {{currentPage} sur {pagesCount}} other {}}" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Poignée de redimensionnement du panneau", diff --git a/src/i18n/messages/all.id.json b/src/i18n/messages/all.id.json index 3530575312..9248b8bef3 100644 --- a/src/i18n/messages/all.id.json +++ b/src/i18n/messages/all.id.json @@ -256,7 +256,8 @@ "ariaLabels.jumpToPageButtonLabel": "Langsung ke halaman", "i18nStrings.jumpToPageInputLabel": "Halaman", "i18nStrings.jumpToPageError": "Halaman di luar rentang. Menampilkan halaman terakhir yang tersedia.", - "i18nStrings.jumpToPageLoadingText": "Memuat" + "i18nStrings.jumpToPageLoadingText": "Memuat", + "i18nStrings.pagesCompactText": "{openEnd, select, true {{currentPage} dari {pagesCount}+} false {{currentPage} dari {pagesCount}} other {}}" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Handel pengubahan ukuran panel", diff --git a/src/i18n/messages/all.it.json b/src/i18n/messages/all.it.json index cdd5462398..188705d095 100644 --- a/src/i18n/messages/all.it.json +++ b/src/i18n/messages/all.it.json @@ -256,7 +256,8 @@ "ariaLabels.jumpToPageButtonLabel": "Vai alla pagina", "i18nStrings.jumpToPageInputLabel": "Pagina", "i18nStrings.jumpToPageError": "Pagina fuori intervallo. Mostra l'ultima pagina disponibile.", - "i18nStrings.jumpToPageLoadingText": "Caricamento in corso" + "i18nStrings.jumpToPageLoadingText": "Caricamento in corso", + "i18nStrings.pagesCompactText": "{openEnd, select, true {{currentPage} di {pagesCount}+} false {{currentPage} di {pagesCount}} other {}}" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Maniglia di ridimensionamento del pannello", diff --git a/src/i18n/messages/all.ja.json b/src/i18n/messages/all.ja.json index 062bb45068..d9369edf4c 100644 --- a/src/i18n/messages/all.ja.json +++ b/src/i18n/messages/all.ja.json @@ -256,7 +256,8 @@ "ariaLabels.jumpToPageButtonLabel": "ページに移動", "i18nStrings.jumpToPageInputLabel": "ページ", "i18nStrings.jumpToPageError": "ページが範囲外です。最後に利用可能なページを表示しています。", - "i18nStrings.jumpToPageLoadingText": "ロード中" + "i18nStrings.jumpToPageLoadingText": "ロード中", + "i18nStrings.pagesCompactText": "{openEnd, select, true {{currentPage}/{pagesCount}+} false {{currentPage}/{pagesCount}} other {}}" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "パネルのサイズ変更ハンドル", diff --git a/src/i18n/messages/all.ko.json b/src/i18n/messages/all.ko.json index c0be14f1e1..a573fee611 100644 --- a/src/i18n/messages/all.ko.json +++ b/src/i18n/messages/all.ko.json @@ -256,7 +256,8 @@ "ariaLabels.jumpToPageButtonLabel": "페이지로 이동", "i18nStrings.jumpToPageInputLabel": "페이지", "i18nStrings.jumpToPageError": "페이지가 범위를 벗어났습니다. 마지막으로 사용 가능한 페이지를 표시합니다.", - "i18nStrings.jumpToPageLoadingText": "로드 중" + "i18nStrings.jumpToPageLoadingText": "로드 중", + "i18nStrings.pagesCompactText": "{openEnd, select, true {{currentPage}/{pagesCount}+} false {{currentPage}/{pagesCount}} other {}}" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "패널 크기 조정 핸들", diff --git a/src/i18n/messages/all.pt-BR.json b/src/i18n/messages/all.pt-BR.json index 7ff120b894..14b88d650b 100644 --- a/src/i18n/messages/all.pt-BR.json +++ b/src/i18n/messages/all.pt-BR.json @@ -256,7 +256,8 @@ "ariaLabels.jumpToPageButtonLabel": "Ir para a página", "i18nStrings.jumpToPageInputLabel": "Página", "i18nStrings.jumpToPageError": "Página fora do alcance. Mostrando a última página disponível.", - "i18nStrings.jumpToPageLoadingText": "Carregando" + "i18nStrings.jumpToPageLoadingText": "Carregando", + "i18nStrings.pagesCompactText": "{openEnd, select, true {mais {currentPage} de {pagesCount}} false {{currentPage} de {pagesCount}} other {}}" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Alça de redimensionamento do painel", diff --git a/src/i18n/messages/all.tr.json b/src/i18n/messages/all.tr.json index 6607f1890a..542783e8aa 100644 --- a/src/i18n/messages/all.tr.json +++ b/src/i18n/messages/all.tr.json @@ -255,8 +255,9 @@ "ariaLabels.previousPageLabel": "Önceki sayfa", "ariaLabels.jumpToPageButtonLabel": "Sayfaya atla", "i18nStrings.jumpToPageInputLabel": "Sayfa", - "i18nStrings.jumpToPageError": "Aralık dışı sayfa. Mevcut son sayfa gösteriliyor.", - "i18nStrings.jumpToPageLoadingText": "Yükleniyor" + "i18nStrings.jumpToPageError": "Sayfa aralık dışında. Mevcut son sayfa gösteriliyor.", + "i18nStrings.jumpToPageLoadingText": "Yükleniyor", + "i18nStrings.pagesCompactText": "{openEnd, select, true {{currentPage}/{pagesCount}+} false {{currentPage}/{pagesCount}} other {}}" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Panel yeniden boyutlandırma tutamacı", diff --git a/src/i18n/messages/all.zh-CN.json b/src/i18n/messages/all.zh-CN.json index 8ca1cd5d67..b7ec62e2ad 100644 --- a/src/i18n/messages/all.zh-CN.json +++ b/src/i18n/messages/all.zh-CN.json @@ -256,7 +256,8 @@ "ariaLabels.jumpToPageButtonLabel": "跳转到页面", "i18nStrings.jumpToPageInputLabel": "页面", "i18nStrings.jumpToPageError": "页面超出范围。显示最后一个可用页面。", - "i18nStrings.jumpToPageLoadingText": "正在加载" + "i18nStrings.jumpToPageLoadingText": "正在加载", + "i18nStrings.pagesCompactText": "{openEnd, select, true {{currentPage} / {pagesCount}+} false {{currentPage}/{pagesCount}} other {}}" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "面板大小调整手柄", diff --git a/src/i18n/messages/all.zh-TW.json b/src/i18n/messages/all.zh-TW.json index 974495651f..93b71192ab 100644 --- a/src/i18n/messages/all.zh-TW.json +++ b/src/i18n/messages/all.zh-TW.json @@ -256,7 +256,8 @@ "ariaLabels.jumpToPageButtonLabel": "跳到頁面", "i18nStrings.jumpToPageInputLabel": "頁面", "i18nStrings.jumpToPageError": "頁面超出範圍。顯示最後一個可用頁面。", - "i18nStrings.jumpToPageLoadingText": "正在載入" + "i18nStrings.jumpToPageLoadingText": "正在載入", + "i18nStrings.pagesCompactText": "{openEnd, select, true {{currentPage}/{pagesCount}+} false {{currentPage}/{pagesCount}} other {}}" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "面板調整大小控點", diff --git a/src/pagination/__tests__/pagination-compact.test.tsx b/src/pagination/__tests__/pagination-compact.test.tsx new file mode 100644 index 0000000000..f1891c237c --- /dev/null +++ b/src/pagination/__tests__/pagination-compact.test.tsx @@ -0,0 +1,74 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import I18nProvider from '../../../lib/components/i18n'; +import messages from '../../../lib/components/i18n/messages/all.en'; +import Pagination from '../../../lib/components/pagination'; +import createWrapper from '../../../lib/components/test-utils/dom'; + +function renderPagination(jsx: React.ReactElement) { + const { container, rerender } = render(jsx); + const wrapper = createWrapper(container).findPagination()!; + return { wrapper, rerender }; +} + +function renderWithI18n(jsx: React.ReactElement) { + const { container, rerender } = render( + + {jsx} + + ); + const wrapper = createWrapper(container).findPagination()!; + return { wrapper, rerender }; +} + +describe('compact variant', () => { + describe('visible counter text', () => { + test('uses i18n catalog "# of #" format when i18n provider is present', () => { + const { wrapper } = renderWithI18n(); + expect(wrapper.findPagesCompactText()!.getElement().textContent).toBe('3 of 12'); + }); + + test('appends "+" to the localized text when openEnd is true', () => { + const { wrapper } = renderWithI18n( + + ); + expect(wrapper.findPagesCompactText()!.getElement().textContent).toBe('3 of 12+'); + }); + + test('calls the consumer override with the page state and uses its result (no i18n provider)', () => { + const pagesCompactText = jest.fn( + ({ currentPage, pagesCount, openEnd }: { currentPage: number; pagesCount: number; openEnd: boolean }) => + `${currentPage} / ${pagesCount}${openEnd ? '+' : ''}` + ); + const { wrapper } = renderPagination( + + ); + + expect(pagesCompactText).toHaveBeenCalledWith({ currentPage: 3, pagesCount: 12, openEnd: true }); + expect(wrapper.findPagesCompactText()!.getElement().textContent).toBe('3 / 12+'); + }); + + test('visible text updates when currentPageIndex changes', () => { + const { wrapper, rerender } = renderWithI18n( + + ); + expect(wrapper.findPagesCompactText()!.getElement().textContent).toBe('1 of 10'); + + rerender( + + + + ); + expect(wrapper.findPagesCompactText()!.getElement().textContent).toBe('7 of 10'); + }); + }); +}); diff --git a/src/pagination/index.tsx b/src/pagination/index.tsx index 505f383766..e154c13a80 100644 --- a/src/pagination/index.tsx +++ b/src/pagination/index.tsx @@ -13,30 +13,33 @@ import InternalPagination from './internal'; export { PaginationProps }; -const Pagination = React.forwardRef((props, ref) => { - const baseComponentProps = useBaseComponent('Pagination', { - props: { openEnd: props.openEnd }, - metadata: { hasJumpToPage: !!props.jumpToPage }, - }); - return ( - - ); -}); +const Pagination = React.forwardRef( + ({ pagesVariant = 'normal', ...props }, ref) => { + const baseComponentProps = useBaseComponent('Pagination', { + props: { openEnd: props.openEnd, pagesVariant }, + metadata: { hasJumpToPage: !!props.jumpToPage }, + }); + return ( + + ); + } +); applyDisplayName(Pagination, 'Pagination'); diff --git a/src/pagination/interfaces.ts b/src/pagination/interfaces.ts index 90aad7e77b..7d991e5f53 100644 --- a/src/pagination/interfaces.ts +++ b/src/pagination/interfaces.ts @@ -22,6 +22,13 @@ export interface PaginationProps { */ openEnd?: boolean; + /** + * Specifies how pages are displayed: + * * `normal` - Displays page number buttons. For larger page ranges, the displayed range is truncated with ellipses. + * * `compact` - Displays the current page and page count between the previous and next buttons. When `openEnd` is `true`, a plus sign after the page count indicates that more pages are available. + */ + pagesVariant?: PaginationProps.PagesVariant; + /** * If set to `true`, the pagination links will be disabled. Use it, for example, if you want to prevent the user * from changing page before items are loaded. @@ -49,6 +56,13 @@ export interface PaginationProps { */ ariaLabels?: PaginationProps.Labels; /** + * An object containing all the necessary localized strings required by the component: + * * `jumpToPageInputLabel` (string) - Accessible label for the jump-to-page number input. + * * `jumpToPageError` (string) - Error message displayed when the entered page number is invalid. + * * `jumpToPageLoadingText` (string) - Loading text displayed while the jump-to-page action is in progress. + * * `pagesCompactText` ((options: { currentPage: number; pagesCount: number; openEnd: boolean }) => string) - + * Provides the visible text for compact pages, for example `3 of 12`, or `3 of 12+` when `openEnd` is `true`. + * Receives the current page, page count, and whether pagination is open-ended. * @i18n */ i18nStrings?: PaginationProps.I18nStrings; @@ -79,6 +93,8 @@ export interface PaginationProps { } export namespace PaginationProps { + export type PagesVariant = 'normal' | 'compact'; + export interface Labels { nextPageLabel?: string; paginationLabel?: string; @@ -94,6 +110,8 @@ export namespace PaginationProps { jumpToPageError?: string; /** @i18n */ jumpToPageLoadingText?: string; + /** @i18n */ + pagesCompactText?: (options: { currentPage: number; pagesCount: number; openEnd: boolean }) => string; } export interface ChangeDetail { diff --git a/src/pagination/internal.tsx b/src/pagination/internal.tsx index 1183df49f1..1dd555af2c 100644 --- a/src/pagination/internal.tsx +++ b/src/pagination/internal.tsx @@ -26,6 +26,7 @@ import { PaginationProps } from './interfaces'; import { getPaginationState, range } from './utils'; import styles from './styles.css.js'; +import testUtilStyles from './test-classes/styles.css.js'; interface PageButtonProps { className?: string; @@ -114,6 +115,7 @@ const InternalPagination = React.forwardRef( i18nStrings, pagesCount, disabled, + pagesVariant = 'normal', onChange, onNextPageClick, onPreviousPageClick, @@ -151,6 +153,17 @@ const InternalPagination = React.forwardRef( i18n('ariaLabels.pageLabel', ariaLabels?.pageLabel, format => pageNumber => format({ pageNumber })) ?? ((pageNumber: number) => `${pageNumber}`); + const pagesCompactText = i18n( + 'i18nStrings.pagesCompactText', + i18nStrings?.pagesCompactText, + format => options => + format({ + currentPage: options.currentPage, + pagesCount: options.pagesCount, + openEnd: `${options.openEnd}`, + }) + ); + const jumpToPageLabel = i18n('i18nStrings.jumpToPageInputLabel', i18nStrings?.jumpToPageInputLabel) ?? ''; const jumpToPageButtonLabel = i18n('ariaLabels.jumpToPageButtonLabel', ariaLabels?.jumpToPageButton) ?? ''; const jumpToPageError = i18n('i18nStrings.jumpToPageError', i18nStrings?.jumpToPageError) ?? ''; @@ -248,33 +261,47 @@ const InternalPagination = React.forwardRef( > - - {leftDots &&
  • ...
  • } - {range(leftIndex, rightIndex).map(pageIndex => ( - - ))} - {rightDots &&
  • ...
  • } - {!openEnd && pagesCount > 1 && ( - + {pagesVariant === 'compact' ? ( +
  • + + {pagesCompactText?.({ + currentPage: currentPageIndex, + pagesCount, + openEnd: !!openEnd, + })} + +
  • + ) : ( + <> + + {leftDots &&
  • ...
  • } + {range(leftIndex, rightIndex).map(pageIndex => ( + + ))} + {rightDots &&
  • ...
  • } + {!openEnd && pagesCount > 1 && ( + + )} + )} .dots { +.root-disabled > .dots, +.root-disabled > .pages-compact { color: awsui.$color-text-interactive-disabled; } diff --git a/src/pagination/test-classes/styles.scss b/src/pagination/test-classes/styles.scss new file mode 100644 index 0000000000..d2d6331984 --- /dev/null +++ b/src/pagination/test-classes/styles.scss @@ -0,0 +1,8 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +.pages-compact-text { + /* used in test-utils or tests */ +} diff --git a/src/test-utils/dom/pagination/index.ts b/src/test-utils/dom/pagination/index.ts index ad779e638b..70e5ae9502 100644 --- a/src/test-utils/dom/pagination/index.ts +++ b/src/test-utils/dom/pagination/index.ts @@ -1,12 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { ComponentWrapper, usesDom } from '@cloudscape-design/test-utils-core/dom'; +import { ComponentWrapper, ElementWrapper, usesDom } from '@cloudscape-design/test-utils-core/dom'; import ButtonWrapper from '../button'; import InputWrapper from '../input'; import PopoverWrapper from '../popover'; import styles from '../../../pagination/styles.selectors.js'; +import testStyles from '../../../pagination/test-classes/styles.selectors.js'; export class PaginationButtonWrapper extends ComponentWrapper { @usesDom @@ -47,6 +48,13 @@ export default class PaginationWrapper extends ComponentWrapper { return this.findComponent(`li:not(:first-child) .${styles.arrow}`, PaginationButtonWrapper)!; } + /** + * Returns the visible text for compact pages (for example, `3 of 12` or `3 of 12+`). + */ + findPagesCompactText(): ElementWrapper | null { + return this.findByClassName(testStyles['pages-compact-text']); + } + /** * Returns the jump to page input field. */