diff --git a/CHANGELOG.md b/CHANGELOG.md
index dd5410e3c4f..5d1065da43a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
## [`main`](https://github.com/elastic/eui/tree/main)
+- Added first and last page arrow buttons to `EuiPagination` when `compressed=true` ([#5362](https://github.com/elastic/eui/pull/5362))
+- Added support for indeterminate `EuiPagination` when `pageCount=0` ([#5362](https://github.com/elastic/eui/pull/5362))
+- Moved mobile behavior to a customizable `responsive` prop to `EuiPagination` that renders the `compressed` display ([#5362](https://github.com/elastic/eui/pull/5362))
+- Added `doubleArrowLeft`, `doubleArrowRight`, `arrowStart`, `arrowEnd` icons ([#5362](https://github.com/elastic/eui/pull/5362))
+
**Bug fixes**
- Fixed scrollbars in `EuiRange` tick labels in Safari ([#5427](https://github.com/elastic/eui/pull/5427))
diff --git a/src-docs/src/views/icon/icons.js b/src-docs/src/views/icon/icons.js
index ff957523265..a5fe9ca52d6 100644
--- a/src-docs/src/views/icon/icons.js
+++ b/src-docs/src/views/icon/icons.js
@@ -22,6 +22,8 @@ export const iconTypes = [
'arrowLeft',
'arrowRight',
'arrowUp',
+ 'arrowStart',
+ 'arrowEnd',
'asterisk',
'beaker',
'bell',
@@ -60,10 +62,12 @@ export const iconTypes = [
'cut',
'database',
'document',
- 'documentation',
- 'documentEdit',
'documents',
+ 'documentEdit',
+ 'documentation',
'dot',
+ 'doubleArrowLeft',
+ 'doubleArrowRight',
'download',
'email',
'empty',
diff --git a/src-docs/src/views/pagination/centered_pagination.js b/src-docs/src/views/pagination/centered_pagination.tsx
similarity index 76%
rename from src-docs/src/views/pagination/centered_pagination.js
rename to src-docs/src/views/pagination/centered_pagination.tsx
index bbf0dbff8ef..ad45ec024d4 100644
--- a/src-docs/src/views/pagination/centered_pagination.js
+++ b/src-docs/src/views/pagination/centered_pagination.tsx
@@ -8,11 +8,7 @@ import {
export default function () {
const [activePage, setActivePage] = useState(0);
- const PAGE_COUNT = 10;
-
- const goToPage = (pageNumber) => {
- setActivePage(pageNumber);
- };
+ const PAGE_COUNT = 15;
return (
@@ -21,7 +17,7 @@ export default function () {
aria-label="Centered pagination example"
pageCount={PAGE_COUNT}
activePage={activePage}
- onPageClick={(activePage) => goToPage(activePage)}
+ onPageClick={(activePage) => setActivePage(activePage)}
/>
diff --git a/src-docs/src/views/pagination/compressed.js b/src-docs/src/views/pagination/compressed.tsx
similarity index 66%
rename from src-docs/src/views/pagination/compressed.js
rename to src-docs/src/views/pagination/compressed.tsx
index 9090cd9c11b..7190af4e9eb 100644
--- a/src-docs/src/views/pagination/compressed.js
+++ b/src-docs/src/views/pagination/compressed.tsx
@@ -4,17 +4,14 @@ import { EuiPagination } from '../../../../src/components';
export default function () {
const [activePage, setActivePage] = useState(0);
- const PAGE_COUNT = 4;
-
- const goToPage = (pageNumber) => {
- setActivePage(pageNumber);
- };
+ const PAGE_COUNT = 24;
return (
goToPage(activePage)}
+ onPageClick={(activePage) => setActivePage(activePage)}
compressed
/>
);
diff --git a/src-docs/src/views/pagination/customizable_pagination.js b/src-docs/src/views/pagination/customizable_pagination.tsx
similarity index 84%
rename from src-docs/src/views/pagination/customizable_pagination.js
rename to src-docs/src/views/pagination/customizable_pagination.tsx
index 95a888d4cc8..ae06b7aaa5a 100644
--- a/src-docs/src/views/pagination/customizable_pagination.js
+++ b/src-docs/src/views/pagination/customizable_pagination.tsx
@@ -21,15 +21,15 @@ export default () => {
setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen);
const closePopover = () => setIsPopoverOpen(false);
- const goToPage = (pageNumber) => setActivePage(pageNumber);
+ const goToPage = (pageNumber: number) => setActivePage(pageNumber);
- const getIconType = (size) => {
+ const getIconType = (size: number) => {
return size === rowSize ? 'check' : 'empty';
};
const button = (
{
>
50 rows
,
- {
- closePopover();
- setRowSize(100);
- }}
- >
- 100 rows
- ,
];
return (
-
+
{
- setActivePage(pageNumber);
- };
-
return (
goToPage(activePage)}
+ onPageClick={(activePage) => setActivePage(activePage)}
/>
);
}
diff --git a/src-docs/src/views/pagination/indeterminate.tsx b/src-docs/src/views/pagination/indeterminate.tsx
new file mode 100644
index 00000000000..c8281786c24
--- /dev/null
+++ b/src-docs/src/views/pagination/indeterminate.tsx
@@ -0,0 +1,16 @@
+import React, { useState } from 'react';
+
+import { EuiPagination } from '../../../../src/components';
+
+export default function () {
+ const [activePage, setActivePage] = useState(0);
+
+ return (
+ setActivePage(activePage)}
+ />
+ );
+}
diff --git a/src-docs/src/views/pagination/many_pages.js b/src-docs/src/views/pagination/many_pages.tsx
similarity index 72%
rename from src-docs/src/views/pagination/many_pages.js
rename to src-docs/src/views/pagination/many_pages.tsx
index 5a6f050bf83..ae99644dd5f 100644
--- a/src-docs/src/views/pagination/many_pages.js
+++ b/src-docs/src/views/pagination/many_pages.tsx
@@ -6,16 +6,12 @@ export default function () {
const [activePage, setActivePage] = useState(0);
const PAGE_COUNT = 22;
- const goToPage = (pageNumber) => {
- setActivePage(pageNumber);
- };
-
return (
goToPage(activePage)}
+ onPageClick={(activePage) => setActivePage(activePage)}
/>
);
}
diff --git a/src-docs/src/views/pagination/pagination_example.js b/src-docs/src/views/pagination/pagination_example.js
index 3b55414ead8..bd8d8213dab 100644
--- a/src-docs/src/views/pagination/pagination_example.js
+++ b/src-docs/src/views/pagination/pagination_example.js
@@ -6,7 +6,6 @@ import { GuideSectionTypes } from '../../components';
import {
EuiCode,
EuiPagination,
- EuiPaginationButton,
EuiText,
EuiCallOut,
} from '../../../../src/components';
@@ -46,34 +45,33 @@ const centeredPaginationSnippet = `
import CustomizablePagination from './customizable_pagination';
const customizablePaginationSource = require('!!raw-loader!./customizable_pagination');
-const customizablePaginationSnippet = `
-
-
-
-
-
-
-
- goToPage(activePage)}
- />
-
- `;
import Compressed from './compressed';
const compressedSource = require('!!raw-loader!./compressed');
-const compressedSnippet = ` goToPage(activePage)}
compressed
+/>`,
+ ` goToPage(activePage)}
+ responsive={['xs']}
+/>`,
+];
+
+import Indeterminate from './indeterminate';
+const indeterminateSource = require('!!raw-loader!./indeterminate');
+const indeterminateSnippet = ` goToPage(activePage)}
/>`;
export const PaginationExample = {
@@ -122,7 +120,7 @@ export const PaginationExample = {
/>
>
),
- props: { EuiPagination, EuiPaginationButton },
+ props: { EuiPagination },
snippet: manyPagesSnippet,
demo: ,
playground: paginationConfig,
@@ -143,6 +141,7 @@ export const PaginationExample = {
),
snippet: fewPagesSnippet,
demo: ,
+ props: { EuiPagination },
},
{
title: 'Centered pagination',
@@ -158,14 +157,14 @@ export const PaginationExample = {
EuiFlexGroup
{' '}
- to set up this pagination layout.
+ to center the pagination in a layout.
),
snippet: centeredPaginationSnippet,
demo: ,
},
{
- title: 'Compressed display',
+ title: 'Compressed and responsive',
source: [
{
type: GuideSectionTypes.JS,
@@ -173,19 +172,55 @@ export const PaginationExample = {
},
],
text: (
-
- Use the compressed prop to minimize the horizontal
- footprint.
-
+ <>
+
+ Use the compressed prop to minimize the
+ horizontal footprint. This will replace the numbered buttons with
+ static numbers and rely on the first, last, next and previous icon
+ buttons to navigate.
+
+
+ This is also the same display that will occur when{' '}
+ responsive is not {' '}
+ false . You can adjust the responsiveness by
+ supplying an array of{' '}
+ named breakpoints to{' '}
+ responsive . The default is{' '}
+ {"['xs', 's']"} .
+
+ >
),
snippet: compressedSnippet,
demo: ,
+ props: { EuiPagination },
},
{
- title: 'Customizable pagination',
+ title: 'Indeterminate page count',
source: [
{
type: GuideSectionTypes.JS,
+ code: indeterminateSource,
+ },
+ ],
+ text: (
+
+ If the total number of pages cannot be accurately determined, you can
+ pass 0 as the pageCount . This
+ will remove the button numbers and rely solely on the arrow icon
+ buttons for navigation. Without a total page count, the last page
+ button will pass back -1 for the{' '}
+ activePage .
+
+ ),
+ snippet: indeterminateSnippet,
+ demo: ,
+ props: { EuiPagination },
+ },
+ {
+ title: 'Customizable pagination',
+ source: [
+ {
+ type: GuideSectionTypes.TSX,
code: customizablePaginationSource,
},
],
@@ -203,8 +238,8 @@ export const PaginationExample = {
tables.
),
- snippet: customizablePaginationSnippet,
demo: ,
+ props: { EuiPagination },
},
],
};
diff --git a/src-docs/src/views/tables/custom/custom.js b/src-docs/src/views/tables/custom/custom.js
index a906a22b0da..edd83dfabc2 100644
--- a/src-docs/src/views/tables/custom/custom.js
+++ b/src-docs/src/views/tables/custom/custom.js
@@ -756,7 +756,7 @@ export default class extends Component {
);
diff --git a/src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap b/src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap
index 43e8529e7a3..b04f357ca42 100644
--- a/src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap
+++ b/src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap
@@ -485,56 +485,62 @@ exports[`EuiInMemoryTable behavior pagination 1`] = `
aria-label="Pagination for table: "
className="euiPagination"
>
-
+
+ Page 2 of 2
+
+
+
-
-
-
-
-
-
-
-
-
-
+ />
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+ />
+
+
+
+
diff --git a/src/components/button/button_icon/button_icon.tsx b/src/components/button/button_icon/button_icon.tsx
index 4e9ad1ee1ca..752e260f229 100644
--- a/src/components/button/button_icon/button_icon.tsx
+++ b/src/components/button/button_icon/button_icon.tsx
@@ -73,7 +73,7 @@ export interface EuiButtonIconProps extends CommonProps {
display?: EuiButtonIconDisplay;
}
-type EuiButtonIconPropsForAnchor = {
+export type EuiButtonIconPropsForAnchor = {
type?: string;
} & PropsForAnchor<
EuiButtonIconProps,
diff --git a/src/components/button/button_icon/index.ts b/src/components/button/button_icon/index.ts
index b2e4516914f..6c54524adfb 100644
--- a/src/components/button/button_icon/index.ts
+++ b/src/components/button/button_icon/index.ts
@@ -11,4 +11,5 @@ export {
EuiButtonIconColor,
EuiButtonIconProps,
EuiButtonIconPropsForButton,
+ EuiButtonIconPropsForAnchor,
} from './button_icon';
diff --git a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap
index f3a1f720247..3b54cbb2f2a 100644
--- a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap
+++ b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap
@@ -43,13 +43,22 @@ exports[`EuiDataGrid pagination renders 1`] = `
aria-label="Pagination for preceding grid: test grid"
class="euiPagination"
>
+
+ Page 2 of 2
+
,
+ total: {pageCount} ,
+ }}
+ />
+
);
- } else if (lastPageInRange < pageCount - 1) {
- lastPageButtons.push(
-
- {(lastRangeAriaLabel: string) => (
-
- …
-
- )}
-
+ } else {
+ const pages = [];
+
+ const firstPageInRange = Math.max(
+ 0,
+ Math.min(
+ activePage - NUMBER_SURROUNDING_PAGES,
+ pageCount - MAX_VISIBLE_PAGES
+ )
+ );
+ const lastPageInRange = Math.min(
+ pageCount,
+ firstPageInRange + MAX_VISIBLE_PAGES
);
- }
- lastPageButtons.push(
-
- );
- }
+ for (
+ let i = firstPageInRange, index = 0;
+ i < lastPageInRange;
+ i++, index++
+ ) {
+ pages.push(
+
+ );
+ }
- let nextPageButtonProps = {};
- if (hasControl && activePage !== pageCount - 1) {
- nextPageButtonProps = {
- 'aria-controls': ariaControls,
- href: `#${ariaControls}`,
- };
- } else {
- nextPageButtonProps = { disabled: activePage === pageCount - 1 };
- }
+ const firstPageButtons = [];
- const nextButton = (
-
- {(nextPage: string) => (
-
- {(disabledNextPage: string) => (
- safeClick(e, activePage + 1)}
- iconType="arrowRight"
- aria-label={
- activePage === pageCount - 1 ? disabledNextPage : nextPage
- }
- color="text"
- data-test-subj="pagination-button-next"
- {...nextPageButtonProps}
+ if (firstPageInRange > 0) {
+ firstPageButtons.push(
+
+ );
+
+ if (firstPageInRange > 1 && firstPageInRange !== 2) {
+ firstPageButtons.push(
+
+ {(firstRangeAriaLabel: string) => (
+
+ …
+
+ )}
+
+ );
+ } else if (firstPageInRange === 2) {
+ firstPageButtons.push(
+
- )}
-
- )}
-
- );
+ );
+ }
+ }
+
+ const lastPageButtons = [];
- const selectablePages = pages;
-
- if (compressed) {
- const firstPageButtonCompressed = (
-
- );
- const lastPageButtonCompressed = (
-
- );
-
- return (
-
- {previousButton}
-
-
+ if (lastPageInRange < pageCount) {
+ if (lastPageInRange + 1 === pageCount - 1) {
+ lastPageButtons.push(
+
+ );
+ } else if (lastPageInRange < pageCount - 1) {
+ lastPageButtons.push(
-
-
- {nextButton}
-
- );
+ >
+ {(lastRangeAriaLabel: string) => (
+
+ …
+
+ )}
+
+ );
+ }
+
+ lastPageButtons.push(
+
+ );
+ }
+
+ const selectablePages = pages;
+
+ const accessibleName = {
+ ...(rest['aria-label'] && { 'aria-label': rest['aria-label'] }),
+ ...(rest['aria-labelledby'] && {
+ 'aria-labelledby': rest['aria-labelledby'],
+ }),
+ };
+
+ centerPageCount = (
+
+ {firstPageButtons}
+ {selectablePages}
+ {lastPageButtons}
+
+ );
+ }
}
- const accessibleName = {
- ...(rest['aria-label'] && { 'aria-label': rest['aria-label'] }),
- ...(rest['aria-labelledby'] && {
- 'aria-labelledby': rest['aria-labelledby'],
- }),
+ // All the i18n strings used to build the whole SR-only text
+ const lastLabel = useEuiI18n('euiPagination.last', 'Last');
+ const pageLabel = useEuiI18n('euiPagination.page', 'Page');
+ const ofLabel = useEuiI18n('euiPagination.of', 'of');
+ const collectionLabel = useEuiI18n('euiPagination.collection', 'collection');
+ const fromEndLabel = useEuiI18n('euiPagination.fromEndLabel', 'from end');
+
+ // Based on the `activePage` count, build the front of the SR-only text
+ // i.e. `Page 1`, `Page 2 from end`, `Last Page`
+ const accessiblePageString = (): string => {
+ if (activePage < -1)
+ return `${pageLabel} ${Math.abs(activePage)} ${fromEndLabel}`;
+ if (activePage === -1) return `${lastLabel} ${pageLabel}`;
+ return `${pageLabel} ${activePage + 1}`;
};
+ // If `pageCount` is unknown call it `collection`
+ const accessibleCollectionString =
+ pageCount === 0 ? collectionLabel : pageCount.toString();
+
+ // Create the whole string with total pageCount or `collection`
+ const accessiblePageCount = `${accessiblePageString()} ${ofLabel} ${accessibleCollectionString}`;
+
return (
+
+
+ {accessiblePageCount}
+
+
+ {firstButton}
{previousButton}
-
- {firstPageButtons}
- {selectablePages}
- {lastPageButtons}
-
+ {centerPageCount}
{nextButton}
+ {lastButton}
);
};
@@ -305,6 +330,7 @@ const PaginationButtonWrapper = ({
pageCount,
ariaControls,
safeClick,
+ disabled,
}: {
pageIndex: number;
inList?: boolean;
@@ -312,6 +338,7 @@ const PaginationButtonWrapper = ({
pageCount: number;
ariaControls?: string;
safeClick: SafeClickHandler;
+ disabled?: boolean;
}) => {
const button = (
safeClick(e, pageIndex)}
pageIndex={pageIndex}
aria-controls={ariaControls}
- hideOnMobile
+ disabled={disabled}
/>
);
diff --git a/src/components/pagination/pagination_button.tsx b/src/components/pagination/pagination_button.tsx
index 270998e9815..91c8843d38c 100644
--- a/src/components/pagination/pagination_button.tsx
+++ b/src/components/pagination/pagination_button.tsx
@@ -19,7 +19,6 @@ export type EuiPaginationButtonProps = EuiButtonEmptyProps & {
* For ellipsis or other non-clickable buttons.
*/
isPlaceholder?: boolean;
- hideOnMobile?: boolean;
pageIndex: number;
totalPages?: number;
};
@@ -41,7 +40,6 @@ export const EuiPaginationButton: FunctionComponent = ({
className,
isActive,
isPlaceholder,
- hideOnMobile,
pageIndex,
totalPages,
...rest
@@ -49,7 +47,6 @@ export const EuiPaginationButton: FunctionComponent = ({
const classes = classNames('euiPaginationButton', className, {
'euiPaginationButton-isActive': isActive,
'euiPaginationButton-isPlaceholder': isPlaceholder,
- 'euiPaginationButton--hideOnMobile': hideOnMobile,
});
const props = {
diff --git a/src/components/pagination/pagination_button_arrow.tsx b/src/components/pagination/pagination_button_arrow.tsx
new file mode 100644
index 00000000000..94c166b28f9
--- /dev/null
+++ b/src/components/pagination/pagination_button_arrow.tsx
@@ -0,0 +1,72 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { FunctionComponent } from 'react';
+import classNames from 'classnames';
+
+import {
+ EuiButtonIcon,
+ EuiButtonIconPropsForAnchor,
+} from '../button/button_icon';
+import { keysOf } from '../common';
+import { useEuiI18n } from '../i18n';
+
+const typeToIconTypeMap = {
+ first: 'arrowStart',
+ previous: 'arrowLeft',
+ next: 'arrowRight',
+ last: 'arrowEnd',
+};
+
+export const TYPES = keysOf(typeToIconTypeMap);
+export type EuiPaginationButtonArrowType = typeof TYPES[number];
+
+export type Props = Partial> & {
+ type: EuiPaginationButtonArrowType;
+ disabled?: boolean;
+ ariaControls?: string;
+};
+
+export const EuiPaginationButtonArrow: FunctionComponent = ({
+ className,
+ type,
+ disabled,
+ ariaControls,
+ onClick,
+}) => {
+ const labels = {
+ first: useEuiI18n('euiPaginationButtonArrow.firstPage', 'First page'),
+ previous: useEuiI18n(
+ 'euiPaginationButtonArrow.previousPage',
+ 'Previous page'
+ ),
+ next: useEuiI18n('euiPaginationButtonArrow.nextPage', 'Next page'),
+ last: useEuiI18n('euiPaginationButtonArrow.lastPage', 'Last page'),
+ };
+
+ const buttonProps: Partial = {};
+
+ if (ariaControls && !disabled) {
+ buttonProps.href = `#${ariaControls}`;
+ buttonProps['aria-controls'] = ariaControls;
+ }
+
+ return (
+
+ );
+};
diff --git a/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap b/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap
index 0fa07f8016b..47d357a928f 100644
--- a/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap
+++ b/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap
@@ -44,10 +44,19 @@ exports[`EuiTablePagination is rendered 1`] = `
class="euiPagination testClass1 testClass2"
data-test-subj="test subject string"
>
+
+ Page 2 of 5
+