UNPKG

terra-paginator

Version:

Paginator to be used for data sets of known and unknown size. Provides first, last, previous, next, and paged functionality.

256 lines (225 loc) 8.62 kB
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 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.isRequired, /** * Total number of all items being paginated. * Required when using itemCountPerPage and selectedPage. */ totalCount: PropTypes.number.isRequired, /** * Total number of items per page. * Required when using selectedPage and totalCount. */ itemCountPerPage: PropTypes.number.isRequired, }; class Paginator extends React.Component { constructor(props) { super(props); 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(Number(index))) { this.props.onPageChange(event.currentTarget.attributes['aria-label'].value); return false; } this.props.onPageChange(index); return false; }; } handleOnKeyDown(index) { return (event) => { if (event.nativeEvent.keyCode === KEYCODES.ENTER || event.nativeEvent.keyCode === KEYCODES.SPACE) { event.preventDefault(); if (Number.isNaN(Number(index))) { this.props.onPageChange(event.currentTarget.attributes['aria-label'].value); return false; } this.props.onPageChange(index); } return false; }; } // TODO: Resolve lint issues - https://github.com/cerner/terra-core/issues/1689 /* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/anchor-is-valid, jsx-a11y/no-noninteractive-tabindex */ buildPageButtons(totalPages, onClick) { const { totalCount, itemCountPerPage, selectedPage } = this.props; const pageSequence = pageSet(selectedPage, calculatePages(totalCount, itemCountPerPage)); const pageButtons = []; pageSequence.forEach((val) => { const paginationLinkClassNames = cx([ 'nav-link', val === selectedPage ? 'is-selected' : null, ]); if (val > totalPages) { return; } pageButtons.push(( <a aria-label={`Page ${val}`} aria-current={val === selectedPage && 'page'} className={paginationLinkClassNames} tabIndex={val === selectedPage ? null : '0'} key={`pageButton_${val}`} onClick={onClick(val)} onKeyDown={this.handleOnKeyDown(val)} > {val} </a>)); }); return pageButtons; } hasNavContext() { return this.props.totalCount && this.props.itemCountPerPage; } defaultPaginator() { const totalPages = calculatePages(this.props.totalCount, this.props.itemCountPerPage); const { selectedPage } = this.props; 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() && ( <a aria-disabled={selectedPage === 1} aria-label="first" className={cx(['nav-link', 'left-controls', selectedPage === 1 && 'is-disabled'])} tabIndex={selectedPage === 1 ? null : '0'} onClick={this.handlePageChange(1)} onKeyDown={this.handleOnKeyDown(1)} > First </a> ) } <a aria-disabled={selectedPage === 1} aria-label="previous" className={cx(['nav-link', 'left-controls', 'previous', selectedPage === 1 && 'is-disabled'])} tabIndex={selectedPage === 1 ? null : '0'} onClick={this.handlePageChange(previousPageIndex)} onKeyDown={this.handleOnKeyDown(previousPageIndex)} > <span className={cx('icon')} /> Previous </a> {this.hasNavContext() && this.buildPageButtons(totalPages, this.handlePageChange)} <a aria-disabled={selectedPage === totalPages} aria-label="next" className={cx(['nav-link', 'right-controls', 'next', selectedPage === totalPages && 'is-disabled'])} tabIndex={selectedPage === totalPages ? null : '0'} onClick={this.handlePageChange(nextPageIndex)} onKeyDown={this.handleOnKeyDown(nextPageIndex)} > Next <span className={cx('icon')} /> </a> { this.hasNavContext() && ( <a aria-disabled={selectedPage === totalPages} aria-label="last" className={cx(['nav-link', 'right-controls', selectedPage === totalPages && 'is-disabled'])} tabIndex={selectedPage === totalPages ? null : '0'} onClick={this.handlePageChange(totalPages)} onKeyDown={this.handleOnKeyDown(totalPages)} > Last </a> ) } </div> ); return fullView; } reducedPaginator() { const totalPages = calculatePages(this.props.totalCount, this.props.itemCountPerPage); const { selectedPage } = this.props; 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() && ( <a aria-disabled={selectedPage === 1} aria-label="first" className={cx(['nav-link', 'left-controls', selectedPage === 1 && 'is-disabled'])} tabIndex={selectedPage === 1 ? null : '0'} onClick={this.handlePageChange(1)} onKeyDown={this.handleOnKeyDown(1)} > First </a> ) } <a aria-disabled={selectedPage === 1} aria-label="previous" className={cx(['nav-link', 'left-controls', 'previous', 'icon-only', selectedPage === 1 && 'is-disabled'])} tabIndex={selectedPage === 1 ? null : '0'} onClick={this.handlePageChange(previousPageIndex)} onKeyDown={this.handleOnKeyDown(previousPageIndex)} > <span className={cx('visually-hidden')}>Previous</span> <span className={cx('icon')} /> </a> {this.hasNavContext() && `Page ${selectedPage}`} <a aria-disabled={selectedPage === totalPages} aria-label="next" className={cx(['nav-link', 'right-controls', 'next', 'icon-only', selectedPage === totalPages && 'is-disabled'])} tabIndex={selectedPage === totalPages ? null : '0'} onClick={this.handlePageChange(nextPageIndex)} onKeyDown={this.handleOnKeyDown(nextPageIndex)} > <span className={cx('visually-hidden')}>Next</span> <span className={cx('icon')} /> </a> { this.hasNavContext() && ( <a aria-disabled={selectedPage === totalPages} aria-label="last" className={cx(['nav-link', 'right-controls', selectedPage === totalPages && 'is-disabled'])} tabIndex={selectedPage === totalPages ? null : '0'} onClick={this.handlePageChange(totalPages)} onKeyDown={this.handleOnKeyDown(totalPages)} > Last </a> ) } </div> ); return reducedView; } /* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/anchor-is-valid, jsx-a11y/no-noninteractive-tabindex */ render() { return <ResponsiveElement defaultElement={this.reducedPaginator()} small={this.defaultPaginator()} />; } } Paginator.propTypes = propTypes; export default Paginator;