pagination.jsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import { useMemo } from 'react'
  2. import PropTypes from 'prop-types'
  3. import classNames from 'classnames'
  4. import { useTranslation } from 'react-i18next'
  5. function Pagination({ currentPage, totalPages, handlePageClick }) {
  6. const { t } = useTranslation()
  7. const maxOtherPageButtons = useMemo(() => {
  8. let maxOtherPageButtons = 4 // does not include current page, prev/next buttons
  9. if (totalPages < maxOtherPageButtons + 1) {
  10. maxOtherPageButtons = totalPages - 1
  11. }
  12. return maxOtherPageButtons
  13. }, [totalPages])
  14. const pageButtons = useMemo(() => {
  15. const result = []
  16. let nextPage = currentPage + 1
  17. let prevPage = currentPage - 1
  18. function calcPages() {
  19. if (nextPage && nextPage <= totalPages) {
  20. result.push(nextPage)
  21. nextPage++
  22. } else {
  23. nextPage = undefined
  24. }
  25. if (prevPage && prevPage > 0) {
  26. result.push(prevPage)
  27. prevPage--
  28. } else {
  29. prevPage = undefined
  30. }
  31. }
  32. while (result.length < maxOtherPageButtons) {
  33. calcPages()
  34. }
  35. result.push(currentPage) // wait until prev/next calculated to add current
  36. result.sort((a, b) => a - b) // sort numerically
  37. return result
  38. }, [currentPage, totalPages, maxOtherPageButtons])
  39. const morePrevPages = useMemo(() => {
  40. return pageButtons[0] !== 1 && currentPage - maxOtherPageButtons / 2 > 1
  41. }, [pageButtons, currentPage, maxOtherPageButtons])
  42. const moreNextPages = useMemo(() => {
  43. return pageButtons[pageButtons.length - 1] < totalPages
  44. }, [pageButtons, totalPages])
  45. return (
  46. <nav role="navigation" aria-label={t('pagination_navigation')}>
  47. <ul className="pagination">
  48. {currentPage > 1 && (
  49. <li>
  50. <button
  51. onClick={event => handlePageClick(event, currentPage - 1)}
  52. aria-label={t('go_prev_page')}
  53. >
  54. «
  55. </button>
  56. </li>
  57. )}
  58. {morePrevPages && (
  59. <li>
  60. <span className="ellipses">…</span>
  61. </li>
  62. )}
  63. {pageButtons.map(page => (
  64. <PaginationItem
  65. key={`prev-page-${page}`}
  66. page={page}
  67. currentPage={currentPage}
  68. handlePageClick={handlePageClick}
  69. />
  70. ))}
  71. {moreNextPages && (
  72. <li>
  73. <span className="ellipses">…</span>
  74. </li>
  75. )}
  76. {currentPage < totalPages && (
  77. <li>
  78. <button
  79. onClick={event => handlePageClick(event, currentPage + 1)}
  80. aria-label={t('go_next_page')}
  81. >
  82. »
  83. </button>
  84. </li>
  85. )}
  86. </ul>
  87. </nav>
  88. )
  89. }
  90. function PaginationItem({ page, currentPage, handlePageClick }) {
  91. const { t } = useTranslation()
  92. const itemClassName = classNames({ active: currentPage === page })
  93. const ariaCurrent = currentPage === page
  94. const ariaLabel =
  95. currentPage === page ? t('page_current', { page }) : t('go_page', { page })
  96. return (
  97. <li className={itemClassName}>
  98. <button
  99. aria-current={ariaCurrent}
  100. onClick={event => handlePageClick(event, page)}
  101. aria-label={ariaLabel}
  102. >
  103. {page}
  104. </button>
  105. </li>
  106. )
  107. }
  108. function isPositiveNumber(value) {
  109. return typeof value === 'number' && value > 0
  110. }
  111. function isCurrentPageWithinTotalPages(currentPage, totalPages) {
  112. return currentPage <= totalPages
  113. }
  114. Pagination.propTypes = {
  115. currentPage: function (props, propName, componentName) {
  116. if (
  117. !isPositiveNumber(props[propName]) ||
  118. !isCurrentPageWithinTotalPages(props.currentPage, props.totalPages)
  119. ) {
  120. return new Error(
  121. 'Invalid prop `' +
  122. propName +
  123. '` supplied to' +
  124. ' `' +
  125. componentName +
  126. '`. Validation failed.'
  127. )
  128. }
  129. },
  130. totalPages: function (props, propName, componentName) {
  131. if (!isPositiveNumber(props[propName])) {
  132. return new Error(
  133. 'Invalid prop `' +
  134. propName +
  135. '` supplied to' +
  136. ' `' +
  137. componentName +
  138. '`. Validation failed.'
  139. )
  140. }
  141. },
  142. handlePageClick: PropTypes.func.isRequired,
  143. }
  144. PaginationItem.propTypes = {
  145. currentPage: PropTypes.number.isRequired,
  146. page: PropTypes.number.isRequired,
  147. handlePageClick: PropTypes.func.isRequired,
  148. }
  149. export default Pagination