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

Hierarchy list POC #1611

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
Draft
3 changes: 3 additions & 0 deletions index.js
Expand Up @@ -27,6 +27,7 @@ export { default as Editor } from './lib/Editor';
export { default as MultiSelection } from './lib/MultiSelection';
export { default as RepeatableField } from './lib/RepeatableField';
export { default as Popper } from './lib/Popper';
export { default as Tree } from './lib/Tree';

/* data containers */
export { default as Card } from './lib/Card';
Expand Down Expand Up @@ -114,6 +115,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 Expand Up @@ -144,5 +146,6 @@ export {
export { default as useCurrencyOptions } from './hooks/useCurrencyOptions';
export { default as useDateFormatter } from './hooks/useFormatDate';
export { default as useTimeFormatter } from './hooks/useFormatTime';
export { default as useClickOutside } from './hooks/useClickOutside';

export { pagingTypes as MCLPagingTypes } from './lib/MultiColumnList';
2 changes: 1 addition & 1 deletion lib/DropdownMenu/DropdownMenu.js
@@ -1,4 +1,4 @@
import React, { cloneElement, useRef } from 'react';
import React, { cloneElement, useEffect, useRef } from 'react';
import isBoolean from 'lodash/isBoolean';
import useRootClose from 'react-overlays/useRootClose';
import PropTypes from 'prop-types';
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 at their 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;
});
});
});