選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

pagination.tsx 4.6KB

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