Skip to content

Instantly share code, notes, and snippets.

@bjankord
Last active July 11, 2018 16:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bjankord/e13143007906e0193ac8f6388ec4fb64 to your computer and use it in GitHub Desktop.
Save bjankord/e13143007906e0193ac8f6388ec4fb64 to your computer and use it in GitHub Desktop.
paginator-with-button-component
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames/bind';
import ResponsiveElement from 'terra-responsive-element';
import 'terra-base/lib/baseStyles';
import Button from '../../terra-button/src/Button';
import styles from './Paginator.module.scss';
import { calculatePages, pageSet, KEYCODES } from './_paginationUtils';
const cx = classNames.bind(styles);
const propTypes = {
/**
* Function to be executed when a navigation element is selected.
*/
onPageChange: PropTypes.func.isRequired,
/**
* The active/selected page. Used to set the default selected page or maintain selection across renders.
* Required when using totalCount and itemCountPerPage.
*/
selectedPage: PropTypes.number,
/**
* Total number of all items being paginated.
* Required when using itemCountPerPage and selectedPage.
*/
totalCount: PropTypes.number,
/**
* Total number of items per page.
* Required when using selectedPage and totalCount.
*/
itemCountPerPage: PropTypes.number,
};
class Paginator extends React.Component {
constructor(props) {
super(props);
const { selectedPage, totalCount, itemCountPerPage } = this.props;
this.state = {
selectedPage: selectedPage && totalCount ? selectedPage : undefined,
pageSequence: selectedPage && totalCount ? pageSet(selectedPage, calculatePages(totalCount, itemCountPerPage)) : undefined,
};
this.handlePageChange = this.handlePageChange.bind(this);
this.handleOnKeyDown = this.handleOnKeyDown.bind(this);
this.hasNavContext = this.hasNavContext.bind(this);
this.buildPageButtons = this.buildPageButtons.bind(this);
this.reducedPaginator = this.reducedPaginator.bind(this);
}
handlePageChange(index) {
return (event) => {
event.preventDefault();
if (Number.isNaN(index)) {
this.props.onPageChange(event.currentTarget.attributes['aria-label'].value);
return false;
}
this.props.onPageChange(index);
this.setState({
selectedPage: index,
pageSequence: pageSet(index, calculatePages(this.props.totalCount, this.props.itemCountPerPage)),
});
return false;
};
}
handleOnKeyDown(index) {
return (event) => {
if (event.nativeEvent.keyCode === KEYCODES.ENTER || event.nativeEvent.keyCode === KEYCODES.SPACE) {
event.preventDefault();
if (Number.isNaN(index)) {
this.props.onPageChange(event.currentTarget.attributes['aria-label'].value);
return false;
}
this.props.onPageChange(index);
this.setState({
selectedPage: index,
pageSequence: pageSet(index, calculatePages(this.props.totalCount, this.props.itemCountPerPage)),
});
}
return false;
};
}
buildPageButtons(totalPages, onClick) {
const { pageSequence, selectedPage } = this.state;
const pageButtons = [];
pageSequence.forEach((val) => {
const paginationLinkClassNames = cx([
'nav-link',
val === selectedPage ? 'is-selected' : null,
]);
if (val > totalPages) {
return;
}
pageButtons.push((
<Button
variant="de-emphasis"
aria-label={`Page ${val}`}
aria-current={val === selectedPage && 'page'}
className={paginationLinkClassNames}
key={`pageButton_${val}`}
onClick={onClick(val)}
onKeyDown={this.handleOnKeyDown(val)}
text={val.toString()}
/>
));
});
return pageButtons;
}
hasNavContext() {
return this.props.totalCount && this.props.itemCountPerPage;
}
defaultPaginator() {
const totalPages = calculatePages(this.props.totalCount, this.props.itemCountPerPage);
const { selectedPage } = this.state;
const previousPageIndex = selectedPage === 1 ? 1 : selectedPage - 1;
const nextPageIndex = selectedPage === totalPages ? totalPages : selectedPage + 1;
const fullView = (
<div className={cx(['paginator', !this.hasNavContext() && 'pageless'])}>
{
this.hasNavContext() && (
<Button
variant="de-emphasis"
isDisabled={selectedPage === 1}
aria-label="first"
className={cx(['nav-link', 'left-controls', selectedPage === 1 && 'is-disabled'])}
onClick={this.handlePageChange(1)}
onKeyDown={this.handleOnKeyDown(1)}
text="First"
/>)
}
<Button
variant="de-emphasis"
isDisabled={selectedPage === 1}
aria-label="previous"
className={cx(['nav-link', 'left-controls', 'previous', selectedPage === 1 && 'is-disabled'])}
onClick={this.handlePageChange(previousPageIndex)}
onKeyDown={this.handleOnKeyDown(previousPageIndex)}
icon={<span className={cx('previous-icon')} />}
text="Previous"
/>
{this.hasNavContext() && this.buildPageButtons(totalPages, this.handlePageChange)}
<Button
variant="de-emphasis"
isDisabled={selectedPage === totalPages}
aria-label="next"
className={cx(['nav-link', 'right-controls', 'next', selectedPage === totalPages && 'is-disabled'])}
onClick={this.handlePageChange(nextPageIndex)}
onKeyDown={this.handleOnKeyDown(nextPageIndex)}
icon={<span className={cx('next-icon')} />}
isReversed
text="Next"
/>
{
this.hasNavContext() && (
<Button
variant="de-emphasis"
isDisabled={selectedPage === totalPages}
aria-label="last"
className={cx(['nav-link', 'right-controls', selectedPage === totalPages && 'is-disabled'])}
onClick={this.handlePageChange(totalPages)}
onKeyDown={this.handleOnKeyDown(totalPages)}
text="Last"
/>)
}
</div>
);
return fullView;
}
reducedPaginator() {
const totalPages = calculatePages(this.props.totalCount, this.props.itemCountPerPage);
const { selectedPage } = this.state;
const previousPageIndex = selectedPage === 1 ? 1 : selectedPage - 1;
const nextPageIndex = selectedPage === totalPages ? totalPages : selectedPage + 1;
const reducedView = (
<div className={cx(['paginator', !this.hasNavContext() && 'pageless'])} role="navigation" aria-label="pagination">
{
this.hasNavContext() && (
<Button
variant="de-emphasis"
isDisabled={selectedPage === 1}
aria-label="first"
className={cx(['nav-link', 'left-controls', selectedPage === 1 && 'is-disabled'])}
onClick={this.handlePageChange(1)}
onKeyDown={this.handleOnKeyDown(1)}
text="First"
/>)
}
<Button
variant="de-emphasis"
isDisabled={selectedPage === 1}
aria-label="previous"
className={cx(['nav-link', 'left-controls', 'previous', 'icon-only', selectedPage === 1 && 'is-disabled'])}
onClick={this.handlePageChange(previousPageIndex)}
onKeyDown={this.handleOnKeyDown(previousPageIndex)}
isIconOnly
text="Previous"
icon={<span className={cx('previous-icon')} />}
/>
{this.hasNavContext() && `Page ${selectedPage}`}
<Button
variant="de-emphasis"
isDisabled={selectedPage === totalPages}
aria-label="next"
className={cx(['nav-link', 'right-controls', 'next', 'icon-only', selectedPage === totalPages && 'is-disabled'])}
onClick={this.handlePageChange(nextPageIndex)}
onKeyDown={this.handleOnKeyDown(nextPageIndex)}
isIconOnly
text="Next"
isReversed
icon={<span className={cx('next-icon')} />}
/>
{
this.hasNavContext() && (
<Button
variant="de-emphasis"
isDisabled={selectedPage === totalPages}
aria-label="last"
className={cx(['nav-link', 'right-controls', selectedPage === totalPages && 'is-disabled'])}
onClick={this.handlePageChange(totalPages)}
onKeyDown={this.handleOnKeyDown(totalPages)}
text="Last"
/>)
}
</div>
);
return reducedView;
}
render() {
return <ResponsiveElement defaultElement={this.reducedPaginator()} small={this.defaultPaginator()} />;
}
}
Paginator.propTypes = propTypes;
export default Paginator;
@import '~terra-mixins';
:local {
.paginator {
align-items: center;
display: flex;
justify-content: center;
&.pageless {
justify-content: space-between;
}
&.progressive {
justify-content: space-between;
}
.nav-link {
background: var(--terra-paginator-nav-link-background);
border: var(--terra-paginator-nav-link-border);
border-radius: var(--terra-paginator-nav-link-border-radius, 3px);
color: var(--terra-paginator-nav-link-foreground-color, #0059a8);
cursor: pointer;
display: inline-block;
line-height: var(--terra-paginator-nav-link-line-height, 1.43);
margin-left: var(--terra-paginator-nav-link-margin-left, 0.14285rem);
margin-right: var(--terra-paginator-nav-link-margin-right, 0.14285rem);
min-width: var(--terra-paginator-nav-link-min-width, 2rem); // Helps ensure limited resizing as you page through large numbers.
padding: var(--terra-paginator-nav-link-padding, 0.35714rem 0);
text-align: center;
user-select: none; // Keeps user from highlighting the paging link text.
&:hover {
background: var(--terra-paginator-nav-link-background-hover);
color: var(--terra-paginator-nav-link-foreground-color-hover, #001f67);
text-decoration: var(--terra-paginator-nav-link-text-decoration-hover, underline);
}
&:focus {
background: var(--terra-paginator-nav-link-background-focus);
border: var(--terra-paginator-nav-link-border-focus);
box-shadow: var(--terra-paginator-nav-link-box-shadow-focus, 0 0 0 2px rgba(76, 178, 233, 0.25), inset 0 0 0 1px #4cb2e9);
color: var(--terra-paginator-nav-link-foreground-color-focus);
outline: none;
}
&:active {
background: var(--terra-paginator-nav-link-background-active, rgba(0, 0, 0, 0.07));
color: var(--terra-paginator-nav-link-foreground-color-active, #001f67);
text-decoration: var(--terra-paginator-nav-link-text-decoration-active, underline);
}
}
.is-selected {
color: var(--terra-paginator-nav-link-selected-color, initial);
cursor: initial;
pointer-events: none;
&:hover {
text-decoration: none;
}
&:focus {
outline: none;
}
}
.is-disabled {
color: var(--terra-paginator-nav-link-foreground-color-disabled, rgba(28, 31, 33, 0.2));
cursor: default;
pointer-events: none;
&:focus {
box-shadow: var(--terra-paginator-nav-link-disabled-box-shadow-focus, initial);
outline: none;
}
}
.left-controls {
margin-left: var(--terra-paginator-nav-link-left-controls-margin-left, 0.14286rem);
margin-right: var(--terra-paginator-nav-link-left-controls-margin-right, 0.57143rem);
}
.right-controls {
margin-left: var(--terra-paginator-nav-link-right-controls-margin-left, 0.57143rem);
margin-right: var(--terra-paginator-nav-link-right-controls-margin-right, 0.14286rem);
}
}
.previous,
.next {
> span {
margin: 0;
}
}
.previous-icon,
.next-icon {
background-repeat: no-repeat;
background-size: contain;
display: inline-block; // IE 10
height: 1rem;
position: relative;
top: var(--terra-paginator-nav-link-icon-offset-top, 0.1785rem);
width: 1rem;
}
.previous-icon {
background: var(--terra-paginator-nav-link-icon-previous, inline-svg('<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><path fill="#0059a8" d="M10.3 24L33.8 0l3.9 3.8L18 24l19.7 20.2-3.9 3.8z"></path></svg>'));
margin-right: 0.25rem;
}
.previous.is-disabled .previous-icon {
background: var(--terra-paginator-nav-link-icon-previous-disabled, inline-svg('<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><path fill="rgba(28, 31, 33, 0.2)" d="M10.3 24L33.8 0l3.9 3.8L18 24l19.7 20.2-3.9 3.8z"></path></svg>'));
}
.next-icon {
background: var(--terra-paginator-nav-link-icon-next, inline-svg('<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><path fill="#0059a8" d="M37.7 24L14.2 48l-3.9-3.8L30 24 10.3 3.8 14.2 0z"></path></svg>'));
margin-left: 0.25rem;
}
.next.is-disabled .next-icon {
background: var(--terra-paginator-nav-link-icon-next-disabled, inline-svg('<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><path fill="rgba(28, 31, 33, 0.2)" d="M37.7 24L14.2 48l-3.9-3.8L30 24 10.3 3.8 14.2 0z"></path></svg>'));
}
.previous.icon-only,
.next.icon-only {
margin: 0;
.previous-icon,
.next-icon {
margin: 0;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment