Skip to content

Commit

Permalink
Add <PaginationPrevNext> component (#2926)
Browse files Browse the repository at this point in the history
  • Loading branch information
balanza authored Aug 29, 2024
1 parent ecd8139 commit 10bffa1
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 54 deletions.
161 changes: 118 additions & 43 deletions assets/js/common/Pagination/Pagination.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,105 @@ import ReactPaginate from 'react-paginate';

import Select from '@common/Select';

const PREV_LABEL = '<';
const NEXT_LABEL = '>';

const boxClassNames = classNames(
'tn-page-item',
'w-full',
'px-4',
'py-2',
'text-xs',
'bg-white',
'border-t',
'border-b',
'border-l',
'hover:bg-gray-100'
);

const leftBoxClassNames = classNames(boxClassNames, 'rounded-l-lg');
const rightBoxClassNames = classNames(
boxClassNames,
'border-r',
'rounded-r-lg'
);
const activeLinkClassNames = 'text-jungle-green-500';
const disabledLinkClassNames = classNames(
'text-zinc-400',
'hover:bg-white',
'cursor-default'
);
const containerClassNames = 'flex items-center';

function ItemsPerPageSelector({
itemsPerPageOptions,
currentItemsPerPage,
onChange,
}) {
return (
itemsPerPageOptions.length > 1 && (
<div className="flex pl-3 items-center text-sm">
<span className="pr-2 text-gray-600">Results per page</span>
<Select
className="z-40"
optionsName=""
options={itemsPerPageOptions}
value={currentItemsPerPage}
onChange={onChange}
/>
</div>
)
);
}

function PaginationPrevNext({
hasPrev = true,
hasNext = true,
onSelect,
currentItemsPerPage = 10,
itemsPerPageOptions = [10],
onChangeItemsPerPage = noop,
}) {
return (
<div
className="flex justify-between p-2 bg-gray-50 width-full"
data-testid="pagination"
>
<ItemsPerPageSelector
itemsPerPageOptions={itemsPerPageOptions}
currentItemsPerPage={currentItemsPerPage}
onChange={onChangeItemsPerPage}
/>
<ul className={containerClassNames}>
<li>
<button
type="button"
className={classNames(
leftBoxClassNames,
hasPrev || disabledLinkClassNames
)}
onClick={() => hasPrev && onSelect('prev')}
>
{PREV_LABEL}
</button>
</li>
<li>
<button
type="button"
className={classNames(
rightBoxClassNames,
hasNext || disabledLinkClassNames
)}
onClick={() => hasNext && onSelect('next')}
>
{NEXT_LABEL}
</button>
</li>
</ul>
</div>
);
}

function Pagination({
pages,
currentPage,
Expand All @@ -15,65 +114,41 @@ function Pagination({
}) {
const selectedPage = Math.min(currentPage, pages);

const boxStyle = classNames(
'tn-page-item',
'w-full',
'px-4',
'py-2',
'text-xs',
'bg-white',
'border-t',
'border-b',
'border-l',
'hover:bg-gray-100'
);

return (
<div
className="flex justify-between p-2 bg-gray-50 width-full"
data-testid="pagination"
>
{itemsPerPageOptions.length > 1 ? (
<div className="flex pl-3 items-center text-sm">
<span className="pr-2 text-gray-600">Results per page</span>
<Select
className="z-40"
optionsName=""
options={itemsPerPageOptions}
value={currentItemsPerPage}
onChange={onChangeItemsPerPage}
/>
</div>
) : (
<span />
)}
<ItemsPerPageSelector
itemsPerPageOptions={itemsPerPageOptions}
currentItemsPerPage={currentItemsPerPage}
onChange={onChangeItemsPerPage}
/>

{/* ReactPaginate paged are 0-based */}
<ReactPaginate
forcePage={selectedPage - 1}
pageRangeDisplayed={3}
marginPagesDisplayed={1}
breakLabel="..."
nextLabel=">"
onClick={(e) => {
const selected =
typeof e.nextSelectedPage === 'number'
? e.nextSelectedPage
: e.selected;
onSelect(selected + 1);
}}
pageCount={pages}
previousLabel="<"
breakLabel="..."
renderOnZeroPageCount={null}
containerClassName="flex items-center"
pageClassName={boxStyle}
activeLinkClassName="text-gray-600 text-jungle-green-500"
previousClassName={classNames(boxStyle, 'rounded-l-lg')}
nextClassName={classNames(boxStyle, 'border-r', 'rounded-r-lg')}
breakClassName={boxStyle}
onPageActive={({ selected }) => onSelect(selected + 1)}
onPageChange={({ selected }) => onSelect(selected + 1)}
previousLabel={PREV_LABEL}
nextLabel={NEXT_LABEL}
containerClassName={containerClassNames}
pageLinkClassName={boxClassNames}
activeLinkClassName={activeLinkClassNames}
disabledLinkClassName={disabledLinkClassNames}
previousLinkClassName={leftBoxClassNames}
nextLinkClassName={rightBoxClassNames}
breakLinkClassName={boxClassNames}
/>
</div>
);
}

export default Pagination;

export { PaginationPrevNext };
128 changes: 118 additions & 10 deletions assets/js/common/Pagination/Pagination.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import { noop } from 'lodash';
import Pagination from '.';
import Pagination, { PaginationPrevNext } from '.';

describe('Pagination component', () => {
it('should render', () => {
Expand Down Expand Up @@ -120,12 +120,10 @@ describe('Pagination component', () => {

it.each`
currentPage | pages | previousPage
${1} | ${5} | ${1}
${2} | ${5} | ${1}
${5} | ${5} | ${4}
${5} | ${15} | ${4}
${15} | ${99} | ${14}
${1} | ${99} | ${1}
${99} | ${99} | ${98}
`(
'should select previous page ($pages, $currentPage)',
Expand All @@ -149,15 +147,39 @@ describe('Pagination component', () => {
}
);

it.each`
currentPage | pages
${1} | ${5}
${1} | ${99}
`(
'should disable prev button ($pages, $currentPage)',
async ({ pages, currentPage }) => {
const user = userEvent.setup();
const onSelect = jest.fn();

render(
<Pagination
pages={pages}
currentPage={currentPage}
currentItemsPerPage={10}
itemsPerPageOptions={[10, 20, 50]}
onSelect={onSelect}
onChangeItemsPerPage={noop}
/>
);

await act(() => user.click(screen.getByText(`<`)));
expect(onSelect).not.toHaveBeenCalled();
}
);

it.each`
currentPage | pages | nextPage
${1} | ${5} | ${2}
${2} | ${5} | ${3}
${5} | ${5} | ${5}
${5} | ${15} | ${6}
${15} | ${99} | ${16}
${1} | ${99} | ${2}
${99} | ${99} | ${99}
`(
'should select next page ($pages, $currentPage)',
async ({ pages, currentPage, nextPage }) => {
Expand All @@ -180,6 +202,32 @@ describe('Pagination component', () => {
}
);

it.each`
currentPage | pages
${5} | ${5}
${99} | ${99}
`(
'should disable next button ($pages, $currentPage)',
async ({ pages, currentPage }) => {
const user = userEvent.setup();
const onSelect = jest.fn();

render(
<Pagination
pages={pages}
currentPage={currentPage}
currentItemsPerPage={10}
itemsPerPageOptions={[10, 20, 50]}
onSelect={onSelect}
onChangeItemsPerPage={noop}
/>
);

await act(() => user.click(screen.getByText(`>`)));
expect(onSelect).not.toHaveBeenCalled();
}
);

it('should work correctly as controlled component', async () => {
const user = userEvent.setup();

Expand All @@ -206,18 +254,23 @@ describe('Pagination component', () => {
render(<ControlledComponent />);

const actions = [
{ action: `<`, expected: 1 }, // previous of the first is the first
{ action: `<`, expected: null }, // previous of the first is noop
{ action: `>`, expected: 2 },
{ action: `>`, expected: 3 },
{ action: `>`, expected: 3 }, // next of the last is the last
{ action: `>`, expected: null }, // next of the last is noop
{ action: `<`, expected: 2 },
];

for (let i = 0; i < actions.length; i += 1) {
const { action, expected } = actions[i];
// eslint-disable-next-line no-await-in-loop
await act(() => user.click(screen.getByText(actions[i].action)));
expect(onSelect).toHaveBeenCalledWith(actions[i].expected);
expect(onSelect).toHaveBeenCalledTimes(1);
await act(() => user.click(screen.getByText(action)));
if (expected === null) {
expect(onSelect).not.toHaveBeenCalled();
} else {
expect(onSelect).toHaveBeenCalledWith(expected);
expect(onSelect).toHaveBeenCalledTimes(1);
}
onSelect.mockClear();
}
});
Expand Down Expand Up @@ -252,3 +305,58 @@ describe('Pagination component', () => {
}
);
});

describe('PaginationPrevNext component', () => {
it('should render', () => {
render(<PaginationPrevNext hasNext onSelect={noop} />);

expect(screen.getByText('<')).toBeInTheDocument();
expect(screen.getByText('>')).toBeInTheDocument();
});

it('should call onSelect', async () => {
const onSelect = jest.fn();
const user = userEvent.setup();

render(<PaginationPrevNext onSelect={onSelect} />);

await act(() => user.click(screen.getByText('<')));
expect(onSelect).toHaveBeenCalledWith('prev');
expect(onSelect).toHaveBeenCalledTimes(1);
onSelect.mockClear();

await act(() => user.click(screen.getByText('>')));
expect(onSelect).toHaveBeenCalledWith('next');
expect(onSelect).toHaveBeenCalledTimes(1);
});

it('should disable prev button', async () => {
const onSelect = jest.fn();
const user = userEvent.setup();

render(<PaginationPrevNext hasPrev={false} onSelect={onSelect} />);

await act(() => user.click(screen.getByText('<')));
expect(onSelect).not.toHaveBeenCalled();

await act(() => user.click(screen.getByText('>')));
expect(onSelect).toHaveBeenCalledWith('next');
expect(onSelect).toHaveBeenCalledTimes(1);
onSelect.mockClear();
});

it('should disable next button', async () => {
const onSelect = jest.fn();
const user = userEvent.setup();

render(<PaginationPrevNext hasNext={false} onSelect={onSelect} />);

await act(() => user.click(screen.getByText('>')));
expect(onSelect).not.toHaveBeenCalled();
onSelect.mockClear();

await act(() => user.click(screen.getByText('<')));
expect(onSelect).toHaveBeenCalledWith('prev');
expect(onSelect).toHaveBeenCalledTimes(1);
});
});
Loading

0 comments on commit 10bffa1

Please sign in to comment.