Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

STCOM-827 Pagination Component #1543

Draft
wants to merge 16 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -114,6 +114,7 @@ export {
export { default as FilterControlGroup } from './lib/FilterControlGroup';
export { default as FilterPaneSearch } from './lib/FilterPaneSearch';
export { default as ExportCsv } from './lib/ExportCsv';
export { default as Pagination } from './lib/Pagination';
export { default as exportToCsv } from './lib/ExportCsv/exportToCsv';

/* utilities */
Expand Down
76 changes: 76 additions & 0 deletions lib/Pagination/Pagination.css
@@ -0,0 +1,76 @@
@import '../variables.css';

/**
* Default styling
*/

.pagination {
display: flex;
list-style: none;
padding: 4px 0;
margin: 0;

&.fillWidth {
width: 100%;
justify-content: space-between;
}

& .paginationItem {
padding: 0 2px;
margin: 0 1px;
}
}

.paginationLink {
composes: button from "../Button/Button.css";
padding: 0 var(--gutter-static-two-thirds, 8px);
margin: 0;

&[aria-disabled=true] {
color: var(--color-text-p2);
transition: opacity ease-in-out 500ms;
pointer-events: none;
}

&.numberLink {
min-width: 1.72rem;
padding: 0 4px;
margin: 0;
}

&:visited {
color: inherit;
}

&::before {
border-radius: 999px;
}

/**
* Button Style: Default
*/

&.default {
background-color: transparent;
border: 1px solid var(--primary);
color: var(--primary);

& :global .stripes__icon {
fill: var(--primary);
}
}

/**
* Button style primary
*/

&.primary {
background-color: var(--primary);
border: 1px solid var(--primary);
color: #fff;

&:hover {
opacity: 0.9;
}
}
}
91 changes: 91 additions & 0 deletions lib/Pagination/Pagination.js
@@ -0,0 +1,91 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactPaginate from 'react-paginate';
import { FormattedMessage } from 'react-intl';
import Icon from '../Icon';
import css from './Pagination.css';

// < # ... # # # ... # >
// |_| |___| |_____| |___| |_|
// OUTER BREAK CENTER BREAK OUTER

const MINIMAL_DISPLAY_THRESHOLD = 6;
const MINIMAL_DISPLAY_COUNT = 3;
const EXTENDED_DISPLAY_COUNT = 6;
const MAX_EXTENDED_OUTER_PAGES = 1;

const propTypes = {
/** Class to be applied to `<nav>` containter */
className: PropTypes.string,
/** Allows you to control the pagination and define the current page. */
currentPage: PropTypes.number,
/** fills width of container, placing previous/next buttons at either end */
fillWidth: PropTypes.bool,
/** The method called to generate the href attribute value for each page. */
hrefBuilder: PropTypes.func.isRequired,
id: PropTypes.string,
/** Set accessible label for `nav` container */
label: PropTypes.string,
/** The method to call when a page is clicked. Exposes the current page object as an argument. */
onPageChange: PropTypes.func,
/** Total number of pages. */
pageCount: PropTypes.number,
/** Whether or not to show the previous and next labels */
showLabels: PropTypes.bool,
};

const Pagination = ({
id,
pageCount,
onPageChange,
hrefBuilder,
fillWidth,
currentPage,
label = 'pagination',
showLabels = true,
...props
}) => {
return (
<nav id={id} data-testid="pagination-component" aria-label={label} data-test-pagination>
<ReactPaginate
pageCount={pageCount}
pageRangeDisplayed={pageCount < MINIMAL_DISPLAY_THRESHOLD ? EXTENDED_DISPLAY_COUNT : MINIMAL_DISPLAY_COUNT}
marginPagesDisplayed={pageCount < MINIMAL_DISPLAY_THRESHOLD ? 0 : MAX_EXTENDED_OUTER_PAGES}
nextLabel={(
<div data-test-pagination-next>
<Icon size="small" icon="caret-right" iconPosition="end">
<FormattedMessage id="stripes-components.next">
{ (text) => <span className={`${showLabels ? '' : 'sr-only'}`}>{text}</span>}
</FormattedMessage>
</Icon>
</div>
)}
previousLabel={(
<div data-test-pagination-previous>
<Icon size="small" icon="caret-left">
<FormattedMessage id="stripes-components.previous">
{ (text) => <span className={`${showLabels ? '' : 'sr-only'}`}>{text}</span>}
</FormattedMessage>
</Icon>
</div>
)}
breakLabel={<Icon icon="ellipsis" aria-label="ellipsis" />}
onPageChange={onPageChange}
pageClassName={css.paginationItem}
containerClassName={`${css.pagination} ${fillWidth ? css.fillWidth : ''}`}
pageLinkClassName={`${css.paginationLink} ${css.numberLink}`}
activeLinkClassName={css.primary}
nextLinkClassName={css.paginationLink}
previousLinkClassName={css.paginationLink}
breakLinkClassName={css.paginationLink}
forcePage={currentPage}
hrefBuilder={hrefBuilder}
{...props}
/>
</nav>
);
};

Pagination.propTypes = propTypes;

export default Pagination;
1 change: 1 addition & 0 deletions lib/Pagination/index.js
@@ -0,0 +1 @@
export { default } from './Pagination';
53 changes: 53 additions & 0 deletions lib/Pagination/readme.md
@@ -0,0 +1,53 @@

# Pagination
The Pagination component is used for nagivation between pages of a list of results.

We use [react-paginate](https://github.com/AdeleD/react-paginate) under the hood, more detail can be found in their documentation. We expose their API along with a few useful props.

## Basic Usage

```

// take actions within your app corresponding to the selected page...
const handlePageClick = (page) => {
// page is an object with {selected} property - which gives the currently selected page index.
// console.log(page);
}

// generate the hrefs for links if necessary (return the full string)
const resultsHrefBuilder = (page) => {
// return '#';
}

<Pagination
pageCount={20}
onPageChange={handlePageClick}
hrefBuilder={resultsHrefBuilder}
/>
```

## Display modes based on page count...
For less than 6 pages, 3 page links will be visible between previous and next buttons (carets.)
```
< 1 2 3 >
```

For more than 6 pages, extended mode is activated - it includes collected central links between ellipsis and outer "margin" links.
```
< 1 ... 4 5 6 ... 10 >
```

## Props

Name | type | description | default | required
--- | --- | --- | --- | ---
`id` | string | Applies the 'id' attribute to the outer `<nav>` element | --- | ---
`pageCount`| number | Used to set starting/ending page numbers (1 - pageCount) | --- | required
`onPageChange` | func | --- | function called when page button is clicked | ---
`hrefBuilder` | func | function for generating hrefs for individual buttons. Provides the page object | ---
`fillWidth` | bool | if `true`, pagination will fill its parent container, placing previous and next buttons at either end, with page buttons distributed evenly between | false | ---
`currentPage` | number | manually sets the current page | --- | ---
`label` | string | label for outer `<nav>` element | --- | 'pagination' | ---
`showLabels` | bool | whether or not to display the visible "previous" and "next" labels. | true | ---


39 changes: 39 additions & 0 deletions lib/Pagination/stories/BasicUsage.js
@@ -0,0 +1,39 @@
/**
* Pagination Component: Basic Usage
*/

import React from 'react';
import { action } from '@storybook/addon-actions';
import Pagination from '..';

// eslint-disable-next-line
const handlePageClick = (page) => {
action(page);
};

export default () => (
<div>
<h3>20 pages</h3>
<Pagination
pageCount={20}
onPageChange={handlePageClick}
hrefBuilder={() => '#'}
/>
<h3>3 or fewer pages, hidden previous and next labels</h3>
<Pagination
pageCount={3}
onPageChange={handlePageClick}
hrefBuilder={() => '#'}
showLabels={false}
/>
<h3>Fill width of container</h3>
<div style={{ width: '600px', border: '1px solid #ccc' }}>
<Pagination
pageCount={3}
onPageChange={handlePageClick}
hrefBuilder={() => '#'}
fillWidth
/>
</div>
</div>
);
9 changes: 9 additions & 0 deletions lib/Pagination/stories/Pagination.stories.js
@@ -0,0 +1,9 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import withReadme from 'storybook-readme/with-readme';
import readme from '../readme.md';
import BasicUsage from './BasicUsage';

storiesOf('Pagination', module)
.addDecorator(withReadme(readme))
.add('Basic Usage', () => <BasicUsage />);
82 changes: 82 additions & 0 deletions lib/Pagination/tests/Pagination-test.js
@@ -0,0 +1,82 @@
import React from 'react';
import { expect } from 'chai';
import sinon from 'sinon';
import {
beforeEach,
describe,
it,
} from '@bigtest/mocha';

import Pagination from '../Pagination';

import { mountWithContext } from '../../../tests/helpers';
import PaginationInteractor from './interactor';

const clickHandler = sinon.spy();

describe('Pagination', () => {
const pagination = new PaginationInteractor();

describe('rendering', () => {
beforeEach(async () => {
await mountWithContext(
<Pagination
pageCount={20}
onPageChange={clickHandler}
hrefBuilder={() => '#'}
/>
);
});

it('should render pagination links', () => {
expect(pagination.isPresent).to.be.true;
});

it('last number link should display 20', () => {
expect(pagination.lastNumber.number).to.equal('20');
});

it('previous/next buttons display labels', () => {
expect(pagination.nextlink.labelHidden).to.be.false;
expect(pagination.nextlink.labelHidden).to.be.false;
});

describe('clicking the next button', () => {
beforeEach(async () => {
clickHandler.resetHistory;
await pagination.nextlink.click();
});

it('calls the pageChange handler', () => {
expect(clickHandler.calledOnce).to.be.true;
});
});
describe('clicking the previous button', () => {
beforeEach(async () => {
clickHandler.resetHistory;
await pagination.previouslink.click();
});

it('calls the pageChange handler', () => {
expect(clickHandler.calledOnce).to.be.true;
});
});
});

describe('hidden previous/next labels', () => {
beforeEach(async () => {
await mountWithContext(
<Pagination
pageCount={3}
hrefBuilder={() => '#'}
showLabels={false}
/>
);
});

it('previous/next buttons hide labels', () => {
expect(pagination.nextlink.labelHidden).to.be.true;
expect(pagination.nextlink.labelHidden).to.be.true;
});
});
});