| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- import { CaretLeft } from "@phosphor-icons/react/dist/ssr/CaretLeft";
- import { CaretRight } from "@phosphor-icons/react/dist/ssr/CaretRight";
- import clsx from "clsx";
- import type { ComponentProps } from "react";
- import { useMemo, useCallback } from "react";
- import type { Link } from "react-aria-components";
- import styles from "./index.module.scss";
- import type { Props as ButtonProps } from "../Button/index.jsx";
- import { Button } from "../Button/index.jsx";
- import buttonStyles from "../Button/index.module.scss";
- import { Select } from "../Select/index.jsx";
- import { Toolbar } from "../unstyled/Toolbar/index.jsx";
- type Props = {
- numPages: number;
- currentPage: number;
- onPageChange: (newPage: number) => void;
- pageSize: number;
- pageSizeOptions: number[];
- onPageSizeChange: (newPageSize: number) => void;
- mkPageLink?: ((page: number) => string) | undefined;
- className?: string | undefined;
- };
- export const Paginator = ({
- numPages,
- currentPage,
- pageSize,
- pageSizeOptions,
- onPageChange,
- onPageSizeChange,
- mkPageLink,
- className,
- }: Props) => (
- <div className={clsx(styles.paginator, className)}>
- <PageSizeSelect
- pageSize={pageSize}
- pageSizeOptions={pageSizeOptions}
- onPageSizeChange={onPageSizeChange}
- />
- {numPages > 1 && (
- <PaginatorToolbar
- currentPage={currentPage}
- numPages={numPages}
- onPageChange={onPageChange}
- mkPageLink={mkPageLink}
- />
- )}
- </div>
- );
- type PageSizeSelectProps = {
- pageSize: number;
- pageSizeOptions: number[];
- onPageSizeChange: (newPageSize: number) => void;
- };
- const PageSizeSelect = ({
- pageSize,
- onPageSizeChange,
- pageSizeOptions,
- }: PageSizeSelectProps) => (
- <Select
- className={styles.pageSizeSelect ?? ""}
- label="Page size"
- hideLabel
- options={pageSizeOptions.map((option) => ({ id: option }))}
- selectedKey={pageSize}
- onSelectionChange={onPageSizeChange}
- show={(value) => `${value.id.toString()} per page`}
- variant="ghost"
- size="sm"
- />
- );
- type PaginatorProps = {
- numPages: number;
- currentPage: number;
- onPageChange: (newPage: number) => void;
- mkPageLink: ((page: number) => string) | undefined;
- };
- const PaginatorToolbar = ({
- numPages,
- currentPage,
- onPageChange,
- mkPageLink,
- }: PaginatorProps) => {
- const first = useMemo(
- () =>
- currentPage <= 3 || numPages <= 5
- ? 1
- : currentPage - 2 - Math.max(2 - (numPages - currentPage), 0),
- [currentPage, numPages],
- );
- const pages = useMemo(
- () =>
- Array.from({ length: Math.min(numPages - first + 1, 5) })
- .fill(undefined)
- .map((_, i) => i + first),
- [numPages, first],
- );
- return (
- <Toolbar aria-label="Page" className={styles.paginatorToolbar ?? ""}>
- <PageSelector
- hideText
- beforeIcon={CaretLeft}
- isDisabled={currentPage === 1}
- page={1}
- onPageChange={onPageChange}
- mkPageLink={mkPageLink}
- >
- First Page
- </PageSelector>
- {pages.map((page) => {
- return page === currentPage ? (
- <SelectedPage key={page}>{page.toString()}</SelectedPage>
- ) : (
- <PageSelector
- key={page}
- page={page}
- aria-label={`Page ${page.toString()}`}
- onPageChange={onPageChange}
- mkPageLink={mkPageLink}
- >
- {page.toString()}
- </PageSelector>
- );
- })}
- <PageSelector
- hideText
- beforeIcon={CaretRight}
- isDisabled={currentPage === numPages}
- page={numPages}
- onPageChange={onPageChange}
- mkPageLink={mkPageLink}
- >
- Last Page
- </PageSelector>
- </Toolbar>
- );
- };
- type PageSelectorProps = Pick<
- ComponentProps<typeof Button>,
- "hideText" | "beforeIcon" | "isDisabled" | "children"
- > & {
- page: number;
- onPageChange: (newPage: number) => void;
- mkPageLink: ((page: number) => string) | undefined;
- };
- const PageSelector = ({ mkPageLink, ...props }: PageSelectorProps) =>
- mkPageLink ? (
- <PageLink mkPageLink={mkPageLink} {...props} />
- ) : (
- <PageButton {...props} />
- );
- type PageLinkProps = Omit<
- ButtonProps<typeof Link>,
- "variant" | "size" | "href" | "onPress"
- > & {
- page: number;
- onPageChange: (newPage: number) => void;
- mkPageLink: (page: number) => string;
- };
- const PageLink = ({
- page,
- isDisabled,
- onPageChange,
- mkPageLink,
- ...props
- }: PageLinkProps) => {
- const url = useMemo(() => mkPageLink(page), [page, mkPageLink]);
- const onPress = useCallback(() => {
- onPageChange(page);
- }, [onPageChange, page]);
- return (
- <Button
- variant="ghost"
- size="sm"
- onPress={onPress}
- href={url}
- isDisabled={isDisabled === true}
- {...props}
- />
- );
- };
- type PageButtonProps = Omit<
- ButtonProps<typeof Link>,
- "variant" | "size" | "href" | "onPress"
- > & {
- page: number;
- onPageChange: (newPage: number) => void;
- };
- const PageButton = ({
- page,
- isDisabled,
- onPageChange,
- ...props
- }: PageButtonProps) => {
- const onPress = useCallback(() => {
- onPageChange(page);
- }, [onPageChange, page]);
- return (
- <Button
- variant="ghost"
- size="sm"
- onPress={onPress}
- isDisabled={isDisabled === true}
- {...props}
- />
- );
- };
- const SelectedPage = ({ children }: { children: string }) => (
- <div
- className={clsx(buttonStyles.button, styles.selectedPage)}
- data-size="sm"
- data-variant="ghost"
- data-pressed
- key={children}
- >
- <span className={buttonStyles.text}>{children}</span>
- </div>
- );
|