Commit 14c13589 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Add PaginationNavigation component

- Add a component to render pagination controls
- Component is unused
parent e7790d76
import propTypes from 'prop-types';
import { pageNumberOptions } from '../util/pagination';
import Button from './Button';
/**
* @typedef PaginationNavigationProps
* @prop {number} currentPage - The currently-visible page of results. Pages
* start at 1 (not 0).
* @prop {(page: number) => void} onChangePage - Callback for changing page
* @prop {number} totalPages
*/
/**
* Render pagination navigation controls, with buttons to go next, previous
* and nearby pages.
*
* @param {PaginationNavigationProps} props
*/
function PaginationNavigation({ currentPage, onChangePage, totalPages }) {
// Pages are 1-indexed
const hasNextPage = currentPage < totalPages;
const hasPreviousPage = currentPage > 1;
const pageNumbers = pageNumberOptions(currentPage, totalPages);
const changePageTo = (pageNumber, eventTarget) => {
onChangePage(pageNumber);
// Because changing pagination page doesn't reload the page (as it would
// in a "traditional" HTML context), the clicked-upon navigation button
// will awkwardly retain focus unless it is actively removed.
// TODO: Evaluate this for a11y issues
/** @type HTMLElement */ (eventTarget)?.blur();
};
return (
<div className="PaginationNavigation">
<div className="PaginationNavigation__relative PaginationNavigation__prev">
{hasPreviousPage && (
<Button
className="PaginationNavigation__button"
icon="arrow-left"
buttonText="prev"
title="Go to previous page"
onClick={e => changePageTo(currentPage - 1, e.target)}
/>
)}
</div>
<ul className="PaginationNavigation__pages">
{pageNumbers.map((page, idx) => (
<li key={idx}>
{page === null ? (
<div className="PaginationNavigation__gap">...</div>
) : (
<Button
key={`page-${idx}`}
buttonText={page.toString()}
title={`Go to page ${page}`}
className="PaginationNavigation__page-button"
isPressed={page === currentPage}
onClick={e => changePageTo(page, e.target)}
/>
)}
</li>
))}
</ul>
<div className="PaginationNavigation__relative PaginationNavigation__next">
{hasNextPage && (
<Button
className="PaginationNavigation__button PaginationNavigation__button-right"
icon="arrow-right"
buttonText="next"
iconPosition="right"
title="Go to next page"
onClick={e => changePageTo(currentPage + 1, e.target)}
/>
)}
</div>
</div>
);
}
PaginationNavigation.propTypes = {
currentPage: propTypes.number,
onChangePage: propTypes.func,
totalPages: propTypes.number,
};
export default PaginationNavigation;
import { mount } from 'enzyme';
import { act } from 'preact/test-utils';
import PaginationNavigation, { $imports } from '../PaginationNavigation';
describe('PaginationNavigation', () => {
let fakeOnChangePage;
let fakePageNumberOptions;
const findButton = (wrapper, title) =>
wrapper.find('button').filterWhere(n => n.props().title === title);
const createComponent = (props = {}) => {
return mount(
<PaginationNavigation
currentPage={1}
onChangePage={fakeOnChangePage}
totalPages={10}
{...props}
/>
);
};
beforeEach(() => {
fakeOnChangePage = sinon.stub();
fakePageNumberOptions = sinon.stub().returns([1, 2, 3, 4, null, 10]);
$imports.$mock({
'../util/pagination': { pageNumberOptions: fakePageNumberOptions },
});
});
afterEach(() => {
$imports.$restore();
});
describe('prev button', () => {
it('should render a prev button when there are previous pages to show', () => {
const wrapper = createComponent({ currentPage: 2 });
const button = findButton(wrapper, 'Go to previous page');
assert.isTrue(button.exists());
});
it('should not render a prev button if there are no previous pages to show', () => {
const wrapper = createComponent({ currentPage: 1 });
const button = findButton(wrapper, 'Go to previous page');
assert.isFalse(button.exists());
});
it('should invoke the onChangePage callback when clicked', () => {
const wrapper = createComponent({ currentPage: 2 });
const button = findButton(wrapper, 'Go to previous page');
button.simulate('click');
assert.calledWith(fakeOnChangePage, 1);
});
it('should remove focus from button after clicked', () => {
const wrapper = createComponent({ currentPage: 2 });
const button = findButton(wrapper, 'Go to previous page');
const buttonEl = button.getDOMNode();
const blurSpy = sinon.spy(buttonEl, 'blur');
act(() => {
button.simulate('click');
});
assert.equal(blurSpy.callCount, 1);
});
});
describe('next button', () => {
it('should render a next button when there are further pages to show', () => {
const wrapper = createComponent({ currentPage: 1 });
const button = findButton(wrapper, 'Go to next page');
assert.isTrue(button.exists());
});
it('should not render a next button if there are no further pages to show', () => {
const wrapper = createComponent({ currentPage: 10 });
const button = findButton(wrapper, 'Go to next page');
assert.isFalse(button.exists());
});
it('should invoke the `onChangePage` callback when clicked', () => {
const wrapper = createComponent({ currentPage: 1 });
const button = findButton(wrapper, 'Go to next page');
button.simulate('click');
assert.calledWith(fakeOnChangePage, 2);
});
it('should remove focus from button after clicked', () => {
const wrapper = createComponent({ currentPage: 1 });
const button = findButton(wrapper, 'Go to next page');
const buttonEl = button.getDOMNode();
const blurSpy = sinon.spy(buttonEl, 'blur');
act(() => {
button.simulate('click');
});
assert.equal(blurSpy.callCount, 1);
});
});
describe('page number buttons', () => {
it('should render buttons for each page number available', () => {
fakePageNumberOptions.returns([1, 2, 3, 4, null, 10]);
const wrapper = createComponent();
[1, 2, 3, 4, 10].forEach(pageNumber => {
const button = findButton(wrapper, `Go to page ${pageNumber}`);
assert.isTrue(button.exists());
});
// There is one "gap":
assert.equal(wrapper.find('.PaginationNavigation__gap').length, 1);
});
it('should invoke the onChangePage callback when page number button clicked', () => {
fakePageNumberOptions.returns([1, 2, 3, 4, null, 10]);
const wrapper = createComponent();
[1, 2, 3, 4, 10].forEach(pageNumber => {
const button = findButton(wrapper, `Go to page ${pageNumber}`);
button.simulate('click');
assert.calledWith(fakeOnChangePage, pageNumber);
});
});
});
});
@use '../../mixins/buttons';
@use "../../mixins/layout";
@use "../../mixins/utils";
@use "../../variables" as var;
.PaginationNavigation {
@include utils.font--large;
@include layout.row;
&__relative {
// Set a minimum width for the previous and next button containers so that
// there is no jumping around as they appear and disappear.
min-width: 5em;
}
&__next {
// Make sure "next" button is flush right
@include layout.row($justify: flex-end);
}
&__pages {
@include layout.row($justify: center, $align: center);
flex-grow: 1;
}
&__button,
&__page-button {
@include buttons.button--labeled(
$background-color: transparent,
$active-background-color: var.$grey-3
);
}
&__page-button {
margin: var.$layout-space--xxsmall;
padding: var.$layout-space--small var.$layout-space;
}
&__button-right {
// TODO TEMPORARY adjustment until we can adjust the actual icon
svg {
margin-left: var.$layout-space--xxsmall;
}
padding-right: var.$layout-space--xxsmall;
}
&__page-button[aria-pressed='true'] {
background-color: var.$grey-3;
}
}
...@@ -41,6 +41,7 @@ ...@@ -41,6 +41,7 @@
@use './components/ModerationBanner'; @use './components/ModerationBanner';
@use './components/NotebookView'; @use './components/NotebookView';
@use './components/NotebookResultCount'; @use './components/NotebookResultCount';
@use './components/PaginationNavigation';
@use './components/SelectionTabs'; @use './components/SelectionTabs';
@use './components/ShareAnnotationsPanel'; @use './components/ShareAnnotationsPanel';
@use './components/SearchInput'; @use './components/SearchInput';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment