Merge pull request #4202 from overleaf/jel-gallery-pagination

Add pagination to the gallery

GitOrigin-RevId: 7107133da5e5ccf316235a6688070203c8bfa566
This commit is contained in:
Jessica Lawshe 2021-06-16 07:18:01 -05:00 committed by Copybot
parent c34d5997e9
commit f8ab7c32ea
6 changed files with 283 additions and 2 deletions

View file

@ -117,6 +117,9 @@
"github_too_many_files_error": "",
"github_validation_check": "",
"give_feedback": "",
"go_next_page": "",
"go_page": "",
"go_prev_page": "",
"go_to_error_location": "",
"have_an_extra_backup": "",
"headers": "",
@ -211,6 +214,8 @@
"other_logs_and_files": "",
"other_output_files": "",
"owner": "",
"page_current": "",
"pagination_navigation": "",
"pdf_compile_in_progress_error": "",
"pdf_compile_rate_limit_hit": "",
"pdf_compile_try_again": "",

View file

@ -0,0 +1,170 @@
import React, { useMemo } from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { useTranslation } from 'react-i18next'
function Pagination({ currentPage, totalPages, handlePageClick }) {
const { t } = useTranslation()
const maxOtherPageButtons = useMemo(() => {
let maxOtherPageButtons = 4 // does not include current page, prev/next buttons
if (totalPages < maxOtherPageButtons + 1) {
maxOtherPageButtons = totalPages - 1
}
return maxOtherPageButtons
}, [totalPages])
const pageButtons = useMemo(() => {
const result = []
let nextPage = currentPage + 1
let prevPage = currentPage - 1
function calcPages() {
if (nextPage && nextPage <= totalPages) {
result.push(nextPage)
nextPage++
} else {
nextPage = undefined
}
if (prevPage && prevPage > 0) {
result.push(prevPage)
prevPage--
} else {
prevPage = undefined
}
}
while (result.length < maxOtherPageButtons) {
calcPages()
}
result.push(currentPage) // wait until prev/next calculated to add current
result.sort((a, b) => a - b) // sort numerically
return result
}, [currentPage, totalPages, maxOtherPageButtons])
const morePrevPages = useMemo(() => {
return pageButtons[0] !== 1 && currentPage - maxOtherPageButtons / 2 > 1
}, [pageButtons, currentPage, maxOtherPageButtons])
const moreNextPages = useMemo(() => {
return pageButtons[pageButtons.length - 1] < totalPages
}, [pageButtons, totalPages])
return (
<nav role="navigation" aria-label={t('pagination_navigation')}>
<ul className="pagination">
{currentPage > 1 && (
<li>
<button
onClick={event => handlePageClick(event, currentPage - 1)}
aria-label={t('go_prev_page')}
>
«
</button>
</li>
)}
{morePrevPages && (
<li>
<span className="ellipses"></span>
</li>
)}
{pageButtons.map(page => (
<PaginationItem
key={`prev-page-${page}`}
page={page}
currentPage={currentPage}
handlePageClick={handlePageClick}
/>
))}
{moreNextPages && (
<li>
<span className="ellipses"></span>
</li>
)}
{currentPage < totalPages && (
<li>
<button
onClick={event => handlePageClick(event, currentPage + 1)}
aria-label={t('go_next_page')}
>
»
</button>
</li>
)}
</ul>
</nav>
)
}
function PaginationItem({ page, currentPage, handlePageClick }) {
const { t } = useTranslation()
const itemClassName = classNames({ active: currentPage === page })
const ariaCurrent = currentPage === page
const ariaLabel =
currentPage === page ? t('page_current', { page }) : t('go_page', { page })
return (
<li className={itemClassName}>
<button
aria-current={ariaCurrent}
onClick={event => handlePageClick(event, page)}
aria-label={ariaLabel}
>
{page}
</button>
</li>
)
}
function isPositiveNumber(value) {
return typeof value === 'number' && value > 0
}
function isCurrentPageWithinTotalPages(currentPage, totalPages) {
return currentPage <= totalPages
}
Pagination.propTypes = {
currentPage: function (props, propName, componentName) {
if (
!isPositiveNumber(props[propName]) ||
!isCurrentPageWithinTotalPages(props.currentPage, props.totalPages)
) {
return new Error(
'Invalid prop `' +
propName +
'` supplied to' +
' `' +
componentName +
'`. Validation failed.'
)
}
},
totalPages: function (props, propName, componentName) {
if (!isPositiveNumber(props[propName])) {
return new Error(
'Invalid prop `' +
propName +
'` supplied to' +
' `' +
componentName +
'`. Validation failed.'
)
}
},
handlePageClick: PropTypes.func.isRequired,
}
PaginationItem.propTypes = {
currentPage: PropTypes.number.isRequired,
page: PropTypes.number.isRequired,
handlePageClick: PropTypes.func.isRequired,
}
export default Pagination

View file

@ -0,0 +1,21 @@
import React from 'react'
import Pagination from '../js/shared/components/pagination'
export const Interactive = args => {
return <Pagination {...args} />
}
export default {
title: 'Pagination',
component: Pagination,
args: {
currentPage: 1,
totalPages: 10,
handlePageClick: () => {},
},
argTypes: {
currentPage: { control: { type: 'number', min: 1, max: 10, step: 1 } },
totalPages: { control: { disable: true } },
},
}

View file

@ -10,6 +10,7 @@
> li {
display: inline; // Remove list-style and block-level defaults
> a,
> button,
> span {
position: relative;
float: left; // Collapse white-space
@ -23,6 +24,7 @@
}
&:first-child {
> a,
> button,
> span {
margin-left: 0;
.border-left-radius(@border-radius-base);
@ -30,6 +32,7 @@
}
&:last-child {
> a,
> button,
> span {
.border-right-radius(@border-radius-base);
}
@ -37,6 +40,7 @@
}
> li > a,
> li > button,
> li > span {
&:hover,
&:focus {
@ -47,6 +51,7 @@
}
> .active > a,
> .active > button,
> .active > span {
&,
&:hover,
@ -65,13 +70,20 @@
> span:focus,
> a,
> a:hover,
> a:focus {
> a:focus,
> button,
> button:hover,
> button:focus {
color: @pagination-disabled-color;
background-color: @pagination-disabled-bg;
border-color: @pagination-disabled-border;
cursor: not-allowed;
}
}
.ellipses {
pointer-events: none;
}
}
// Sizing

View file

@ -1461,5 +1461,10 @@
"cancel_anytime": "We're confident that you'll love __appName__, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days.",
"for_visa_mastercard_and_discover": "For <0>Visa, MasterCard and Discover</0>, the <1>3 digits</1> on the <2>back</2> of your card.",
"for_american_express": "For <0>American Express</0>, the <1>4 digits</1> on the <2>front</2> of your card.",
"request_password_reset_to_reconfirm": "Request password reset email to reconfirm"
"request_password_reset_to_reconfirm": "Request password reset email to reconfirm",
"go_next_page": "Go to Next Page",
"go_prev_page": "Go to Previous Page",
"page_current": "Page __page__, Current Page",
"go_page": "Go to page __page__",
"pagination_navigation": "Pagination Navigation"
}

View file

@ -0,0 +1,68 @@
import { expect } from 'chai'
import React from 'react'
import { render, screen } from '@testing-library/react'
import Pagination from '../../../../frontend/js/shared/components/pagination'
describe('<Pagination />', function () {
it('renders with current page handled', async function () {
render(
<Pagination currentPage={6} totalPages={10} handlePageClick={() => {}} />
)
await screen.findByLabelText('Page 6, Current Page')
})
it('renders with nearby page buttons and prev/next button', async function () {
render(
<Pagination currentPage={2} totalPages={4} handlePageClick={() => {}} />
)
await screen.findByLabelText('Page 2, Current Page')
await screen.findByLabelText('Go to page 1')
await screen.findByLabelText('Go to page 3')
await screen.findByLabelText('Go to page 4')
await screen.findByLabelText('Go to Previous Page')
await screen.findByLabelText('Go to Next Page')
})
it('does not render the prev button when expected', async function () {
render(
<Pagination currentPage={1} totalPages={2} handlePageClick={() => {}} />
)
await screen.findByLabelText('Page 1, Current Page')
await screen.findByLabelText('Go to Next Page')
expect(screen.queryByLabelText('Go to Prev Page')).to.be.null
})
it('does not render the next button when expected', async function () {
render(
<Pagination currentPage={2} totalPages={2} handlePageClick={() => {}} />
)
await screen.findByLabelText('Page 2, Current Page')
await screen.findByLabelText('Go to Previous Page')
expect(screen.queryByLabelText('Go to Next Page')).to.be.null
})
it('renders 1 ellipses when there are more pages than buttons and on first page', async function () {
render(
<Pagination currentPage={1} totalPages={10} handlePageClick={() => {}} />
)
const ellipses = await screen.findAllByText('…')
expect(ellipses.length).to.equal(1)
})
it('renders 1 ellipses when on last page and there are more previous pages than buttons', async function () {
render(
<Pagination currentPage={10} totalPages={10} handlePageClick={() => {}} />
)
const ellipses = await screen.findAllByText('…')
expect(ellipses.length).to.equal(1)
})
it('renders 2 ellipses when there are more pages than buttons', async function () {
render(
<Pagination currentPage={5} totalPages={10} handlePageClick={() => {}} />
)
const ellipses = await screen.findAllByText('…')
expect(ellipses.length).to.equal(2)
})
it('only renders the number of page buttons set by maxOtherPageButtons', async function () {
render(
<Pagination currentPage={1} totalPages={100} handlePageClick={() => {}} />
)
const items = document.querySelectorAll('button')
expect(items.length).to.equal(6) // 5 page buttons + next button
})
})