pagination.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import React from 'react'
  2. import clsx from 'clsx'
  3. import usePagination from './hook'
  4. import type {
  5. ButtonProps,
  6. IPagination,
  7. IPaginationProps,
  8. PageButtonProps,
  9. } from './type'
  10. const defaultState: IPagination = {
  11. currentPage: 0,
  12. setCurrentPage: () => {},
  13. truncableText: '...',
  14. truncableClassName: '',
  15. pages: [],
  16. hasPreviousPage: false,
  17. hasNextPage: false,
  18. previousPages: [],
  19. isPreviousTruncable: false,
  20. middlePages: [],
  21. isNextTruncable: false,
  22. nextPages: [],
  23. }
  24. const PaginationContext: React.Context<IPagination> = React.createContext<IPagination>(defaultState)
  25. export const PrevButton = ({
  26. className,
  27. children,
  28. dataTestId,
  29. as = <button />,
  30. ...buttonProps
  31. }: ButtonProps) => {
  32. const pagination = React.useContext(PaginationContext)
  33. const previous = () => {
  34. if (pagination.currentPage + 1 > 1)
  35. pagination.setCurrentPage(pagination.currentPage - 1)
  36. }
  37. const disabled = pagination.currentPage === 0
  38. return (
  39. <as.type
  40. {...buttonProps}
  41. {...as.props}
  42. className={clsx(className, as.props.className)}
  43. onClick={() => previous()}
  44. tabIndex={disabled ? '-1' : 0}
  45. disabled={disabled}
  46. data-testid={dataTestId}
  47. onKeyPress={(event: React.KeyboardEvent) => {
  48. event.preventDefault()
  49. if (event.key === 'Enter' && !disabled)
  50. previous()
  51. }}
  52. >
  53. {as.props.children ?? children}
  54. </as.type>
  55. )
  56. }
  57. export const NextButton = ({
  58. className,
  59. children,
  60. dataTestId,
  61. as = <button />,
  62. ...buttonProps
  63. }: ButtonProps) => {
  64. const pagination = React.useContext(PaginationContext)
  65. const next = () => {
  66. if (pagination.currentPage + 1 < pagination.pages.length)
  67. pagination.setCurrentPage(pagination.currentPage + 1)
  68. }
  69. const disabled = pagination.currentPage === pagination.pages.length - 1
  70. return (
  71. <as.type
  72. {...buttonProps}
  73. {...as.props}
  74. className={clsx(className, as.props.className)}
  75. onClick={() => next()}
  76. tabIndex={disabled ? '-1' : 0}
  77. disabled={disabled}
  78. data-testid={dataTestId}
  79. onKeyPress={(event: React.KeyboardEvent) => {
  80. event.preventDefault()
  81. if (event.key === 'Enter' && !disabled)
  82. next()
  83. }}
  84. >
  85. {as.props.children ?? children}
  86. </as.type>
  87. )
  88. }
  89. type ITruncableElementProps = {
  90. prev?: boolean
  91. }
  92. const TruncableElement = ({ prev }: ITruncableElementProps) => {
  93. const pagination: IPagination = React.useContext(PaginationContext)
  94. const {
  95. isPreviousTruncable,
  96. isNextTruncable,
  97. truncableText,
  98. truncableClassName,
  99. } = pagination
  100. return ((isPreviousTruncable && prev === true) || (isNextTruncable && !prev))
  101. ? (
  102. <li className={truncableClassName || undefined}>{truncableText}</li>
  103. )
  104. : null
  105. }
  106. export const PageButton = ({
  107. as = <a />,
  108. className,
  109. dataTestIdActive,
  110. dataTestIdInactive,
  111. activeClassName,
  112. inactiveClassName,
  113. renderExtraProps,
  114. }: PageButtonProps) => {
  115. const pagination: IPagination = React.useContext(PaginationContext)
  116. const renderPageButton = (page: number) => (
  117. <li key={page}>
  118. <as.type
  119. data-testid={
  120. clsx({
  121. [`${dataTestIdActive}`]:
  122. dataTestIdActive && pagination.currentPage + 1 === page,
  123. [`${dataTestIdInactive}-${page}`]:
  124. dataTestIdActive && pagination.currentPage + 1 !== page,
  125. }) || undefined
  126. }
  127. tabIndex={0}
  128. onKeyPress={(event: React.KeyboardEvent) => {
  129. if (event.key === 'Enter')
  130. pagination.setCurrentPage(page - 1)
  131. }}
  132. onClick={() => pagination.setCurrentPage(page - 1)}
  133. className={clsx(
  134. className,
  135. pagination.currentPage + 1 === page
  136. ? activeClassName
  137. : inactiveClassName,
  138. )}
  139. {...as.props}
  140. {...(renderExtraProps ? renderExtraProps(page) : {})}
  141. >
  142. {page}
  143. </as.type>
  144. </li>
  145. )
  146. return (
  147. <>
  148. {pagination.previousPages.map(renderPageButton)}
  149. <TruncableElement prev />
  150. {pagination.middlePages.map(renderPageButton)}
  151. <TruncableElement />
  152. {pagination.nextPages.map(renderPageButton)}
  153. </>
  154. )
  155. }
  156. export const Pagination = ({
  157. dataTestId,
  158. ...paginationProps
  159. }: IPaginationProps & { dataTestId?: string }) => {
  160. const pagination = usePagination(paginationProps)
  161. return (
  162. <PaginationContext.Provider value={pagination}>
  163. <div className={paginationProps.className} data-testid={dataTestId}>
  164. {paginationProps.children}
  165. </div>
  166. </PaginationContext.Provider>
  167. )
  168. }
  169. Pagination.PrevButton = PrevButton
  170. Pagination.NextButton = NextButton
  171. Pagination.PageButton = PageButton