index.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. import { CaretLeft } from "@phosphor-icons/react/dist/ssr/CaretLeft";
  2. import { CaretRight } from "@phosphor-icons/react/dist/ssr/CaretRight";
  3. import clsx from "clsx";
  4. import type { ComponentProps } from "react";
  5. import { useMemo, useCallback } from "react";
  6. import type { Link } from "react-aria-components";
  7. import styles from "./index.module.scss";
  8. import type { Props as ButtonProps } from "../Button/index.jsx";
  9. import { Button } from "../Button/index.jsx";
  10. import buttonStyles from "../Button/index.module.scss";
  11. import { Select } from "../Select/index.jsx";
  12. import { Toolbar } from "../unstyled/Toolbar/index.jsx";
  13. type Props = {
  14. numPages: number;
  15. currentPage: number;
  16. onPageChange: (newPage: number) => void;
  17. pageSize: number;
  18. pageSizeOptions: number[];
  19. onPageSizeChange: (newPageSize: number) => void;
  20. mkPageLink?: ((page: number) => string) | undefined;
  21. className?: string | undefined;
  22. };
  23. export const Paginator = ({
  24. numPages,
  25. currentPage,
  26. pageSize,
  27. pageSizeOptions,
  28. onPageChange,
  29. onPageSizeChange,
  30. mkPageLink,
  31. className,
  32. }: Props) => (
  33. <div className={clsx(styles.paginator, className)}>
  34. <PageSizeSelect
  35. pageSize={pageSize}
  36. pageSizeOptions={pageSizeOptions}
  37. onPageSizeChange={onPageSizeChange}
  38. />
  39. {numPages > 1 && (
  40. <PaginatorToolbar
  41. currentPage={currentPage}
  42. numPages={numPages}
  43. onPageChange={onPageChange}
  44. mkPageLink={mkPageLink}
  45. />
  46. )}
  47. </div>
  48. );
  49. type PageSizeSelectProps = {
  50. pageSize: number;
  51. pageSizeOptions: number[];
  52. onPageSizeChange: (newPageSize: number) => void;
  53. };
  54. const PageSizeSelect = ({
  55. pageSize,
  56. onPageSizeChange,
  57. pageSizeOptions,
  58. }: PageSizeSelectProps) => (
  59. <Select
  60. className={styles.pageSizeSelect ?? ""}
  61. label="Page size"
  62. hideLabel
  63. options={pageSizeOptions.map((option) => ({ id: option }))}
  64. selectedKey={pageSize}
  65. onSelectionChange={onPageSizeChange}
  66. show={(value) => `${value.id.toString()} per page`}
  67. variant="ghost"
  68. size="sm"
  69. />
  70. );
  71. type PaginatorProps = {
  72. numPages: number;
  73. currentPage: number;
  74. onPageChange: (newPage: number) => void;
  75. mkPageLink: ((page: number) => string) | undefined;
  76. };
  77. const PaginatorToolbar = ({
  78. numPages,
  79. currentPage,
  80. onPageChange,
  81. mkPageLink,
  82. }: PaginatorProps) => {
  83. const first = useMemo(
  84. () =>
  85. currentPage <= 3 || numPages <= 5
  86. ? 1
  87. : currentPage - 2 - Math.max(2 - (numPages - currentPage), 0),
  88. [currentPage, numPages],
  89. );
  90. const pages = useMemo(
  91. () =>
  92. Array.from({ length: Math.min(numPages - first + 1, 5) })
  93. .fill(undefined)
  94. .map((_, i) => i + first),
  95. [numPages, first],
  96. );
  97. return (
  98. <Toolbar aria-label="Page" className={styles.paginatorToolbar ?? ""}>
  99. <PageSelector
  100. hideText
  101. beforeIcon={CaretLeft}
  102. isDisabled={currentPage === 1}
  103. page={1}
  104. onPageChange={onPageChange}
  105. mkPageLink={mkPageLink}
  106. >
  107. First Page
  108. </PageSelector>
  109. {pages.map((page) => {
  110. return page === currentPage ? (
  111. <SelectedPage key={page}>{page.toString()}</SelectedPage>
  112. ) : (
  113. <PageSelector
  114. key={page}
  115. page={page}
  116. aria-label={`Page ${page.toString()}`}
  117. onPageChange={onPageChange}
  118. mkPageLink={mkPageLink}
  119. >
  120. {page.toString()}
  121. </PageSelector>
  122. );
  123. })}
  124. <PageSelector
  125. hideText
  126. beforeIcon={CaretRight}
  127. isDisabled={currentPage === numPages}
  128. page={numPages}
  129. onPageChange={onPageChange}
  130. mkPageLink={mkPageLink}
  131. >
  132. Last Page
  133. </PageSelector>
  134. </Toolbar>
  135. );
  136. };
  137. type PageSelectorProps = Pick<
  138. ComponentProps<typeof Button>,
  139. "hideText" | "beforeIcon" | "isDisabled" | "children"
  140. > & {
  141. page: number;
  142. onPageChange: (newPage: number) => void;
  143. mkPageLink: ((page: number) => string) | undefined;
  144. };
  145. const PageSelector = ({ mkPageLink, ...props }: PageSelectorProps) =>
  146. mkPageLink ? (
  147. <PageLink mkPageLink={mkPageLink} {...props} />
  148. ) : (
  149. <PageButton {...props} />
  150. );
  151. type PageLinkProps = Omit<
  152. ButtonProps<typeof Link>,
  153. "variant" | "size" | "href" | "onPress"
  154. > & {
  155. page: number;
  156. onPageChange: (newPage: number) => void;
  157. mkPageLink: (page: number) => string;
  158. };
  159. const PageLink = ({
  160. page,
  161. isDisabled,
  162. onPageChange,
  163. mkPageLink,
  164. ...props
  165. }: PageLinkProps) => {
  166. const url = useMemo(() => mkPageLink(page), [page, mkPageLink]);
  167. const onPress = useCallback(() => {
  168. onPageChange(page);
  169. }, [onPageChange, page]);
  170. return (
  171. <Button
  172. variant="ghost"
  173. size="sm"
  174. onPress={onPress}
  175. href={url}
  176. isDisabled={isDisabled === true}
  177. {...props}
  178. />
  179. );
  180. };
  181. type PageButtonProps = Omit<
  182. ButtonProps<typeof Link>,
  183. "variant" | "size" | "href" | "onPress"
  184. > & {
  185. page: number;
  186. onPageChange: (newPage: number) => void;
  187. };
  188. const PageButton = ({
  189. page,
  190. isDisabled,
  191. onPageChange,
  192. ...props
  193. }: PageButtonProps) => {
  194. const onPress = useCallback(() => {
  195. onPageChange(page);
  196. }, [onPageChange, page]);
  197. return (
  198. <Button
  199. variant="ghost"
  200. size="sm"
  201. onPress={onPress}
  202. isDisabled={isDisabled === true}
  203. {...props}
  204. />
  205. );
  206. };
  207. const SelectedPage = ({ children }: { children: string }) => (
  208. <div
  209. className={clsx(buttonStyles.button, styles.selectedPage)}
  210. data-size="sm"
  211. data-variant="ghost"
  212. data-pressed
  213. key={children}
  214. >
  215. <span className={buttonStyles.text}>{children}</span>
  216. </div>
  217. );