diff --git a/src/components/Pagination/index.d.ts b/src/components/Pagination/index.d.ts
index dfc7328ff..ae1076c90 100644
--- a/src/components/Pagination/index.d.ts
+++ b/src/components/Pagination/index.d.ts
@@ -10,6 +10,14 @@ export interface PaginationProps {
* The max page count
*/
pageCount: number;
+ /**
+ * The number of pages to show on either side of the current page
+ */
+ siblingCount?: number;
+ /**
+ * separator for truncated pages
+ */
+ separator?: React.ReactNode;
/**
* A callback function for when clicking on previous, next and pagination items
*/
diff --git a/src/components/Pagination/index.jsx b/src/components/Pagination/index.jsx
index 58bbe5475..1637d564e 100644
--- a/src/components/Pagination/index.jsx
+++ b/src/components/Pagination/index.jsx
@@ -1,136 +1,115 @@
import React from 'react';
+import _ from 'lodash';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Button from '../Button';
import './styles.css';
-const Pagination = ({ className, activePage, pageCount, onSelect, prev, next }) => (
-
- {activePage !== 1 && prev && (
-
onSelect(activePage - 1)}
- className={classnames('aui--pagination-item', 'aui--pagination-sides')}
- >
-
-
- )}
-
-
onSelect(1)}
- className={classnames('aui--pagination-item', {
- active: activePage === 1,
+const inclusiveRange = (start, end) => _.range(start, end + 1);
+
+const Pagination = ({
+ className,
+ activePage: activePageProp,
+ pageCount,
+ siblingCount: siblingCountProp = 1,
+ separator = '...',
+ onSelect,
+ prev,
+ next,
+}) => {
+ const activePage = _.clamp(activePageProp, 1, pageCount);
+
+ const pages = React.useMemo(() => {
+ const siblingCount = _.clamp(siblingCountProp, 0, pageCount);
+ // [first page] [separator] [sibling(s)] [activePage] [sibling(s)] [separator] [last page]
+ const separatorCount = 2;
+ const endsCount = 2;
+ const centerCount = siblingCount * 2 + 1;
+ const totalPageItems = centerCount + separatorCount;
+ const totalItems = totalPageItems + endsCount;
+
+ const offset = Math.floor(centerCount / 2);
+
+ if (totalItems >= pageCount) {
+ return inclusiveRange(1, pageCount);
+ }
+
+ const showLeftSeparator = activePage > offset + 2 && activePage <= pageCount;
+ const showRightSeparator = activePage < pageCount - (offset + 2);
+
+ if (showLeftSeparator && !showRightSeparator) {
+ const rightRange = inclusiveRange(pageCount - totalPageItems + 1, pageCount);
+ return [1, separator, ...rightRange];
+ }
+
+ if (!showLeftSeparator && showRightSeparator) {
+ const leftRange = inclusiveRange(1, totalPageItems);
+
+ return [...leftRange, separator, pageCount];
+ }
+
+ if (showLeftSeparator && showRightSeparator) {
+ const centerRange = inclusiveRange(activePage - offset, activePage + offset);
+ return [1, separator, ...centerRange, separator, pageCount];
+ }
+ }, [activePage, pageCount, separator, siblingCountProp]);
+
+ return (
+
+ {prev && (
+
onSelect(activePage - 1)}
+ className={classnames('aui--pagination-item', 'aui--pagination-sides', {
+ 'aui--pagination-hidden': activePage === 1,
+ })}
+ icon={
}
+ aria-label="Previous Page"
+ />
+ )}
+
+ {pages.map((page, index) => {
+ const active = activePage === page;
+ if (page === separator)
+ return (
+
+ {separator}
+
+ );
+ return (
+ onSelect(page)}
+ className={classnames('aui--pagination-item', {
+ active,
+ })}
+ aria-label={`Go to page ${page}`}
+ icon={page}
+ />
+ );
})}
- >
- {1}
-
-
- {activePage > 3 && pageCount !== 4 && pageCount !== 5 && (
-
- ...
-
- )}
-
- {activePage === 5 && pageCount === 5 && (
- onSelect(activePage - 3)}
- >
- {activePage - 3}
-
- )}
-
- {((activePage === pageCount && pageCount > 3) || (activePage === 4 && pageCount === 5)) && (
- onSelect(activePage - 2)}
- className={classnames('aui--pagination-item')}
- >
- {activePage - 2}
-
- )}
-
- {activePage > 2 && (
- onSelect(activePage - 1)}
- >
- {activePage - 1}
-
- )}
-
- {activePage !== 1 && activePage !== pageCount && (
- onSelect(activePage)}
- className={classnames('aui--pagination-item', 'active')}
- >
- {activePage}
-
- )}
-
- {activePage < pageCount - 1 && (
- onSelect(activePage + 1)}
- >
- {activePage + 1}
-
- )}
-
- {((activePage === 1 && pageCount > 3) || (activePage === 2 && pageCount === 5)) && (
- onSelect(activePage + 2)}
- >
- {activePage + 2}
-
- )}
-
- {activePage === 1 && pageCount === 5 && (
- onSelect(activePage + 3)}
- >
- {activePage + 3}
-
- )}
-
- {activePage < pageCount - 2 && pageCount !== 4 && pageCount !== 5 && (
-
- ...
-
- )}
-
- {pageCount !== 1 && (
- onSelect(pageCount)}
- className={classnames('aui--pagination-item', {
- active: activePage === pageCount,
- })}
- >
- {pageCount}
-
- )}
-
- {activePage !== pageCount && next && (
- onSelect(activePage + 1)}
- className={classnames('aui--pagination-item', 'aui--pagination-sides')}
- >
-
-
- )}
-
-);
+
+ {next && (
+ onSelect(activePage + 1)}
+ className={classnames('aui--pagination-item', 'aui--pagination-sides', {
+ 'aui--pagination-hidden': activePage === pageCount,
+ })}
+ icon={
}
+ aria-label="Next Page"
+ />
+ )}
+
+ );
+};
Pagination.propTypes = {
className: PropTypes.string,
@@ -142,6 +121,14 @@ Pagination.propTypes = {
* The max page count
*/
pageCount: PropTypes.number.isRequired,
+ /**
+ * The number of pages to show on either side of the current page
+ */
+ siblingCount: PropTypes.number,
+ /**
+ * separator for truncated pages
+ */
+ separator: PropTypes.node,
/**
* A callback function for when clicking on previous, next and pagination items
*/
diff --git a/src/components/Pagination/index.spec.jsx b/src/components/Pagination/index.spec.jsx
index 546310b02..27c3e38c0 100644
--- a/src/components/Pagination/index.spec.jsx
+++ b/src/components/Pagination/index.spec.jsx
@@ -5,7 +5,7 @@ import Pagination from '.';
afterEach(cleanup);
-describe(' ', () => {
+describe(' ', () => {
it('should render with default props', () => {
const props = {
pageCount: 10,
@@ -15,16 +15,16 @@ describe(' ', () => {
expect(getByTestId('pagination-wrapper')).toHaveClass('aui--pagination');
- expect(queryAllByTestId('button-wrapper')).toHaveLength(5);
- expect(queryAllByTestId('button-wrapper')[0]).toHaveClass('active');
- expect(queryAllByTestId('button-wrapper')[0]).toHaveTextContent('1');
+ expect(queryAllByTestId('button-wrapper')).toHaveLength(8);
+ expect(queryAllByTestId('button-wrapper')[1]).toHaveClass('active');
+ expect(queryAllByTestId('button-wrapper')[1]).toHaveTextContent('1');
- expect(queryAllByTestId('button-wrapper')[1]).toHaveTextContent('2');
- expect(queryAllByTestId('button-wrapper')[2]).toHaveTextContent('3');
- expect(queryAllByTestId('button-wrapper')[3]).toHaveTextContent('10');
- expect(queryAllByTestId('button-wrapper')[4]).toHaveClass('aui--pagination-sides');
+ expect(queryAllByTestId('button-wrapper')[2]).toHaveTextContent('2');
+ expect(queryAllByTestId('button-wrapper')[3]).toHaveTextContent('3');
+ expect(queryAllByTestId('button-wrapper')[6]).toHaveTextContent('10');
+ expect(queryAllByTestId('button-wrapper')[7]).toHaveClass('aui--pagination-sides');
- expect(queryByTestId('pagination-right-ellipsis')).toBeInTheDocument();
+ expect(queryByTestId('pagination-ellipsis')).toBeInTheDocument();
});
it('should show active pagination button as specified', () => {
@@ -37,7 +37,7 @@ describe(' ', () => {
expect(getByTestId('pagination-wrapper')).toHaveClass('aui--pagination');
- expect(queryAllByTestId('button-wrapper')).toHaveLength(6);
+ expect(queryAllByTestId('button-wrapper')).toHaveLength(8);
expect(queryAllByTestId('button-wrapper')[0]).toHaveClass('aui--pagination-sides');
expect(queryAllByTestId('button-wrapper')[1]).toHaveTextContent('1');
expect(queryAllByTestId('button-wrapper')[1]).not.toHaveClass('active');
@@ -45,7 +45,7 @@ describe(' ', () => {
expect(queryAllByTestId('button-wrapper')[2]).toHaveTextContent('2');
expect(queryAllByTestId('button-wrapper')[2]).toHaveClass('active');
- expect(queryAllByTestId('button-wrapper')[5]).toHaveClass('aui--pagination-sides');
+ expect(queryAllByTestId('button-wrapper')[7]).toHaveClass('aui--pagination-sides');
});
it('should not show prev/next button as specified', () => {
@@ -60,7 +60,7 @@ describe(' ', () => {
expect(getByTestId('pagination-wrapper')).toHaveClass('aui--pagination');
- expect(queryAllByTestId('button-wrapper')).toHaveLength(4);
+ expect(queryAllByTestId('button-wrapper')).toHaveLength(6);
const buttons = queryAllByTestId('button-wrapper');
_.forEach(buttons, (button) => expect(button).not.toHaveClass('aui--pagination-sides'));
@@ -75,27 +75,23 @@ describe(' ', () => {
const { getByTestId, queryAllByTestId } = render( );
expect(getByTestId('pagination-wrapper')).toHaveClass('aui--pagination');
- expect(queryAllByTestId('button-wrapper')).toHaveLength(5);
+ expect(queryAllByTestId('button-wrapper')).toHaveLength(8);
- fireEvent.click(queryAllByTestId('button-wrapper')[0]);
+ fireEvent.click(queryAllByTestId('button-wrapper')[1]);
expect(props.onSelect).toHaveBeenCalledTimes(1);
expect(props.onSelect).toHaveBeenCalledWith(1);
- fireEvent.click(queryAllByTestId('button-wrapper')[1]);
+ fireEvent.click(queryAllByTestId('button-wrapper')[2]);
expect(props.onSelect).toHaveBeenCalledTimes(2);
expect(props.onSelect).toHaveBeenLastCalledWith(2);
- fireEvent.click(queryAllByTestId('button-wrapper')[2]);
+ fireEvent.click(queryAllByTestId('button-wrapper')[3]);
expect(props.onSelect).toHaveBeenCalledTimes(3);
expect(props.onSelect).toHaveBeenLastCalledWith(3);
- fireEvent.click(queryAllByTestId('button-wrapper')[3]);
+ fireEvent.click(queryAllByTestId('button-wrapper')[6]);
expect(props.onSelect).toHaveBeenCalledTimes(4);
expect(props.onSelect).toHaveBeenLastCalledWith(10);
-
- fireEvent.click(queryAllByTestId('button-wrapper')[4]);
- expect(props.onSelect).toHaveBeenCalledTimes(5);
- expect(props.onSelect).toHaveBeenLastCalledWith(2);
});
it('should trigger selecting events of buttons when active page is 4', () => {
@@ -104,42 +100,24 @@ describe(' ', () => {
pageCount: 10,
onSelect: jest.fn(),
};
- const { queryByTestId, queryAllByTestId } = render( );
+ const { queryAllByTestId } = render( );
expect(queryAllByTestId('button-wrapper')).toHaveLength(7);
expect(queryAllByTestId('button-wrapper')[0]).toHaveClass('aui--pagination-sides');
expect(queryAllByTestId('button-wrapper')[6]).toHaveClass('aui--pagination-sides');
- expect(queryByTestId('pagination-left-ellipsis')).toBeInTheDocument();
- expect(queryByTestId('pagination-right-ellipsis')).toBeInTheDocument();
+ expect(queryAllByTestId('pagination-ellipsis')).toHaveLength(2);
- fireEvent.click(queryAllByTestId('button-wrapper')[0]);
- expect(props.onSelect).toHaveBeenCalledTimes(1);
- expect(props.onSelect).toHaveBeenCalledWith(3);
-
- fireEvent.click(queryAllByTestId('button-wrapper')[1]);
- expect(props.onSelect).toHaveBeenCalledTimes(2);
- expect(props.onSelect).toHaveBeenLastCalledWith(1);
-
- fireEvent.click(queryAllByTestId('button-wrapper')[2]);
- expect(props.onSelect).toHaveBeenCalledTimes(3);
- expect(props.onSelect).toHaveBeenLastCalledWith(3);
-
- fireEvent.click(queryAllByTestId('button-wrapper')[3]);
- expect(props.onSelect).toHaveBeenCalledTimes(4);
- expect(props.onSelect).toHaveBeenLastCalledWith(4);
+ expect(queryAllByTestId('button-wrapper')[1]).toHaveTextContent('1');
+ expect(queryAllByTestId('button-wrapper')[2]).toHaveTextContent('3');
+ expect(queryAllByTestId('button-wrapper')[3]).toHaveTextContent('4');
+ expect(queryAllByTestId('button-wrapper')[4]).toHaveTextContent('5');
+ expect(queryAllByTestId('button-wrapper')[5]).toHaveTextContent('10');
fireEvent.click(queryAllByTestId('button-wrapper')[4]);
- expect(props.onSelect).toHaveBeenCalledTimes(5);
- expect(props.onSelect).toHaveBeenLastCalledWith(5);
-
- fireEvent.click(queryAllByTestId('button-wrapper')[5]);
- expect(props.onSelect).toHaveBeenCalledTimes(6);
- expect(props.onSelect).toHaveBeenLastCalledWith(10);
-
- fireEvent.click(queryAllByTestId('button-wrapper')[6]);
- expect(props.onSelect).toHaveBeenCalledTimes(7);
+ expect(props.onSelect).toHaveBeenCalledTimes(1);
expect(props.onSelect).toHaveBeenLastCalledWith(5);
+ expect(queryAllByTestId('button-wrapper')[3]).toHaveClass('active');
});
it('should trigger selecting events of buttons when active page is 4 and pageCount is 4', () => {
@@ -150,7 +128,7 @@ describe(' ', () => {
};
const { queryAllByTestId } = render( );
- expect(queryAllByTestId('button-wrapper')).toHaveLength(5);
+ expect(queryAllByTestId('button-wrapper')).toHaveLength(6);
expect(queryAllByTestId('button-wrapper')[0]).toHaveClass('aui--pagination-sides');
fireEvent.click(queryAllByTestId('button-wrapper')[0]);
@@ -181,35 +159,35 @@ describe(' ', () => {
};
const { queryAllByTestId, rerender } = render( );
- expect(queryAllByTestId('button-wrapper')).toHaveLength(6);
- expect(queryAllByTestId('button-wrapper')[5]).toHaveClass('aui--pagination-sides');
+ expect(queryAllByTestId('button-wrapper')).toHaveLength(7);
+ expect(queryAllByTestId('button-wrapper')[6]).toHaveClass('aui--pagination-sides');
- fireEvent.click(queryAllByTestId('button-wrapper')[0]);
+ fireEvent.click(queryAllByTestId('button-wrapper')[1]);
expect(props.onSelect).toHaveBeenCalledTimes(1);
expect(props.onSelect).toHaveBeenCalledWith(1);
- fireEvent.click(queryAllByTestId('button-wrapper')[1]);
+ fireEvent.click(queryAllByTestId('button-wrapper')[2]);
expect(props.onSelect).toHaveBeenCalledTimes(2);
expect(props.onSelect).toHaveBeenLastCalledWith(2);
- fireEvent.click(queryAllByTestId('button-wrapper')[2]);
+ fireEvent.click(queryAllByTestId('button-wrapper')[3]);
expect(props.onSelect).toHaveBeenCalledTimes(3);
expect(props.onSelect).toHaveBeenLastCalledWith(3);
- fireEvent.click(queryAllByTestId('button-wrapper')[3]);
+ fireEvent.click(queryAllByTestId('button-wrapper')[4]);
expect(props.onSelect).toHaveBeenCalledTimes(4);
expect(props.onSelect).toHaveBeenLastCalledWith(4);
- fireEvent.click(queryAllByTestId('button-wrapper')[4]);
+ fireEvent.click(queryAllByTestId('button-wrapper')[5]);
expect(props.onSelect).toHaveBeenCalledTimes(5);
expect(props.onSelect).toHaveBeenLastCalledWith(5);
- fireEvent.click(queryAllByTestId('button-wrapper')[5]);
+ fireEvent.click(queryAllByTestId('button-wrapper')[6]);
expect(props.onSelect).toHaveBeenCalledTimes(6);
expect(props.onSelect).toHaveBeenLastCalledWith(2);
rerender( );
- expect(queryAllByTestId('button-wrapper')).toHaveLength(6);
+ expect(queryAllByTestId('button-wrapper')).toHaveLength(7);
expect(queryAllByTestId('button-wrapper')[0]).toHaveClass('aui--pagination-sides');
fireEvent.click(queryAllByTestId('button-wrapper')[0]);
@@ -236,4 +214,21 @@ describe(' ', () => {
expect(props.onSelect).toHaveBeenCalledTimes(12);
expect(props.onSelect).toHaveBeenLastCalledWith(5);
});
+
+ it('should show right separator only when on the last page', () => {
+ const props = {
+ activePage: 10,
+ pageCount: 10,
+ onSelect: jest.fn(),
+ };
+ const { queryAllByTestId, queryByTestId } = render( );
+ expect(queryAllByTestId('button-wrapper')[1]).toHaveTextContent('1');
+ expect(queryAllByTestId('button-wrapper')[2]).toHaveTextContent('6');
+ expect(queryAllByTestId('button-wrapper')[3]).toHaveTextContent('7');
+ expect(queryAllByTestId('button-wrapper')[4]).toHaveTextContent('8');
+ expect(queryAllByTestId('button-wrapper')[5]).toHaveTextContent('9');
+ expect(queryAllByTestId('button-wrapper')[6]).toHaveTextContent('10');
+
+ expect(queryByTestId('pagination-ellipsis')).toBeInTheDocument();
+ });
});
diff --git a/src/components/Pagination/styles.css b/src/components/Pagination/styles.css
index 3b66b6f52..e6a1d8e1b 100644
--- a/src/components/Pagination/styles.css
+++ b/src/components/Pagination/styles.css
@@ -1,45 +1,59 @@
+@import '../../styles/variable.css';
+
.aui--pagination {
padding: 20px 0;
display: flex;
justify-content: center;
align-items: center;
+ gap: 2px;
}
.aui--pagination-separator {
- width: 12.5px;
- height: 24px;
- margin: 0 5px;
+ color: $color-gray;
}
-.aui--pagination-sides .previous-icon {
- width: 16px;
- height: 16px;
- background-repeat: no-repeat;
- background-size: contain;
- background-image: url('../../styles/icons/point-left.svg');
-}
+.aui--pagination-sides {
+ & .previous-icon,
+ & .next-icon {
+ width: 16px;
+ height: 16px;
+ background-repeat: no-repeat;
+ background-size: contain;
+ }
-.aui--pagination-sides .next-icon {
- width: 16px;
- height: 16px;
- background-repeat: no-repeat;
- background-size: contain;
- background-image: url('../../styles/icons/point-right.svg');
+ & .previous-icon {
+ background-image: url('../../styles/icons/point-left.svg');
+ }
+
+ & .next-icon {
+ background-image: url('../../styles/icons/point-right.svg');
+ }
}
.aui--pagination-item {
display: flex;
align-items: center;
justify-content: center;
- height: 24px;
- width: 24px;
+ user-select: none;
+ height: 30px;
+ width: 30px;
+ margin: 0;
+ font-weight: normal;
+ border-radius: 3px;
+ color: $color-text-sub;
+
+ &.active {
+ background-color: $color-gray-lightest;
+ color: $color-text;
+ font-weight: bold;
+ }
}
-.aui--pagination-item.btn-borderless:not([disabled]):hover,
-.aui--pagination-item.btn-borderless:not([disabled]):active {
- box-shadow: none;
+.aui--pagination-hidden {
+ visibility: hidden;
+ pointer-events: none;
}
-.aui--pagination-item.btn-inverse:not([disabled]):focus {
+.aui--pagination-item.aui-inverse:not([disabled]):focus {
background-color: #fff;
}
diff --git a/www/containers/props.json b/www/containers/props.json
index ebb27b179..0eeb6f18f 100644
--- a/www/containers/props.json
+++ b/www/containers/props.json
@@ -3162,6 +3162,28 @@
"required": true,
"description": "The max page count"
},
+ "siblingCount": {
+ "type": {
+ "name": "number"
+ },
+ "required": false,
+ "description": "The number of pages to show on either side of the current page",
+ "defaultValue": {
+ "value": "1",
+ "computed": false
+ }
+ },
+ "separator": {
+ "type": {
+ "name": "node"
+ },
+ "required": false,
+ "description": "separator for truncated pages",
+ "defaultValue": {
+ "value": "'...'",
+ "computed": false
+ }
+ },
"onSelect": {
"type": {
"name": "func"