usePagination.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import { unstable_useControlled as useControlled } from "@mui/utils";
  2. import { ChangeEvent, ReactEventHandler } from "react";
  3. export interface UsePaginationProps {
  4. /**
  5. * Number of always visible pages at the beginning and end.
  6. * @default 1
  7. */
  8. boundaryCount?: number;
  9. /**
  10. * The name of the component where this hook is used.
  11. */
  12. componentName?: string;
  13. /**
  14. * The total number of pages.
  15. * @default 1
  16. */
  17. count?: number;
  18. /**
  19. * The page selected by default when the component is uncontrolled.
  20. * @default 1
  21. */
  22. defaultPage?: number;
  23. /**
  24. * If `true`, the component is disabled.
  25. * @default false
  26. */
  27. disabled?: boolean;
  28. /**
  29. * If `true`, hide the next-page button.
  30. * @default false
  31. */
  32. hideNextButton?: boolean;
  33. /**
  34. * If `true`, hide the previous-page button.
  35. * @default false
  36. */
  37. hidePrevButton?: boolean;
  38. /**
  39. * Callback fired when the page is changed.
  40. *
  41. * @param {React.ChangeEvent<unknown>} event The event source of the callback.
  42. * @param {number} page The page selected.
  43. */
  44. onChange?: (event: React.ChangeEvent<unknown>, page: number) => void;
  45. /**
  46. * The current page.
  47. */
  48. page?: number;
  49. /**
  50. * If `true`, show the first-page button.
  51. * @default false
  52. */
  53. showFirstButton?: boolean;
  54. /**
  55. * If `true`, show the last-page button.
  56. * @default false
  57. */
  58. showLastButton?: boolean;
  59. /**
  60. * Number of always visible pages before and after the current page.
  61. * @default 1
  62. */
  63. siblingCount?: number;
  64. }
  65. export interface UsePaginationItem {
  66. onClick: ReactEventHandler;
  67. type:
  68. | "page"
  69. | "first"
  70. | "last"
  71. | "next"
  72. | "previous"
  73. | "start-ellipsis"
  74. | "end-ellipsis";
  75. page: number | null;
  76. selected: boolean;
  77. disabled: boolean;
  78. }
  79. export interface UsePaginationResult {
  80. items: UsePaginationItem[];
  81. }
  82. export default function usePagination(
  83. props: UsePaginationProps = {}
  84. ): UsePaginationResult {
  85. // keep default values in sync with @default tags in Pagination.propTypes
  86. const {
  87. boundaryCount = 1,
  88. componentName = "usePagination",
  89. count = 1,
  90. defaultPage = 1,
  91. disabled = false,
  92. hideNextButton = false,
  93. hidePrevButton = false,
  94. onChange: handleChange,
  95. page: pageProp,
  96. showFirstButton = false,
  97. showLastButton = false,
  98. siblingCount = 1,
  99. ...other
  100. } = props;
  101. const [page, setPageState] = useControlled({
  102. controlled: pageProp,
  103. default: defaultPage,
  104. name: componentName,
  105. state: "page",
  106. });
  107. const handleClick = (event: ChangeEvent<unknown>, value: number) => {
  108. if (!pageProp) {
  109. setPageState(value);
  110. }
  111. if (handleChange) {
  112. handleChange(event, value);
  113. }
  114. };
  115. // https://dev.to/namirsab/comment/2050
  116. const range = (start: number, end: number) => {
  117. const length = end - start + 1;
  118. return Array.from({ length }, (_, i) => start + i);
  119. };
  120. const startPages = range(1, Math.min(boundaryCount, count));
  121. const endPages = range(
  122. Math.max(count - boundaryCount + 1, boundaryCount + 1),
  123. count
  124. );
  125. const siblingsStart = Math.max(
  126. Math.min(
  127. // Natural start
  128. page - siblingCount,
  129. // Lower boundary when page is high
  130. count - boundaryCount - siblingCount * 2 - 1
  131. ),
  132. // Greater than startPages
  133. boundaryCount + 2
  134. );
  135. const siblingsEnd = Math.min(
  136. Math.max(
  137. // Natural end
  138. page + siblingCount,
  139. // Upper boundary when page is low
  140. boundaryCount + siblingCount * 2 + 2
  141. ),
  142. // Less than endPages
  143. endPages.length > 0 ? endPages[0] - 2 : count - 1
  144. );
  145. // Basic list of items to render
  146. // e.g. itemList = ['first', 'previous', 1, 'ellipsis', 4, 5, 6, 'ellipsis', 10, 'next', 'last']
  147. const itemList = [
  148. ...(showFirstButton ? ["first"] : []),
  149. ...(hidePrevButton ? [] : ["previous"]),
  150. ...startPages,
  151. // Start ellipsis
  152. // eslint-disable-next-line no-nested-ternary
  153. ...(siblingsStart > boundaryCount + 2
  154. ? ["start-ellipsis"]
  155. : boundaryCount + 1 < count - boundaryCount
  156. ? [boundaryCount + 1]
  157. : []),
  158. // Sibling pages
  159. ...range(siblingsStart, siblingsEnd),
  160. // End ellipsis
  161. // eslint-disable-next-line no-nested-ternary
  162. ...(siblingsEnd < count - boundaryCount - 1
  163. ? ["end-ellipsis"]
  164. : count - boundaryCount > boundaryCount
  165. ? [count - boundaryCount]
  166. : []),
  167. ...endPages,
  168. ...(hideNextButton ? [] : ["next"]),
  169. ...(showLastButton ? ["last"] : []),
  170. ] as (UsePaginationItem["type"] | number)[];
  171. // Map the button type to its page number
  172. const buttonPage = (type: UsePaginationItem["type"]) => {
  173. switch (type) {
  174. case "first":
  175. return 1;
  176. case "previous":
  177. return page - 1;
  178. case "next":
  179. return page + 1;
  180. case "last":
  181. return count;
  182. default:
  183. return null;
  184. }
  185. };
  186. // Convert the basic item list to PaginationItem props objects
  187. const items: UsePaginationItem[] = itemList.map((item) => {
  188. return typeof item === "number"
  189. ? {
  190. onClick: (event: any) => {
  191. handleClick(event, item);
  192. },
  193. type: "page",
  194. page: item,
  195. selected: item === page,
  196. disabled,
  197. "aria-current": item === page ? "true" : undefined,
  198. }
  199. : {
  200. onClick: (event: any) => {
  201. handleClick(event, buttonPage(item) as unknown as number);
  202. },
  203. type: item,
  204. page: buttonPage(item),
  205. selected: false,
  206. disabled:
  207. disabled ||
  208. (item.indexOf("ellipsis") === -1 &&
  209. (item === "next" || item === "last" ? page >= count : page <= 1)),
  210. };
  211. });
  212. return {
  213. items,
  214. ...other,
  215. };
  216. }