aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/organisms/nav/pagination/pagination.tsx
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-10-24 18:48:57 +0200
committerArmand Philippot <git@armandphilippot.com>2023-11-11 18:15:24 +0100
commit3f8ae3f558446aba3870e90c899db25ad9321499 (patch)
tree30824d02705337309d9223f8c5a6bd8fc41d482c /src/components/organisms/nav/pagination/pagination.tsx
parent98044be08600daf6bd7c7e1a4adada319dbcbbaf (diff)
refactor(components): rewrite Pagination component
Diffstat (limited to 'src/components/organisms/nav/pagination/pagination.tsx')
-rw-r--r--src/components/organisms/nav/pagination/pagination.tsx183
1 files changed, 183 insertions, 0 deletions
diff --git a/src/components/organisms/nav/pagination/pagination.tsx b/src/components/organisms/nav/pagination/pagination.tsx
new file mode 100644
index 0000000..8e95122
--- /dev/null
+++ b/src/components/organisms/nav/pagination/pagination.tsx
@@ -0,0 +1,183 @@
+import { type ForwardRefRenderFunction, forwardRef } from 'react';
+import { ButtonLink, Icon, Nav, type NavProps } from '../../../atoms';
+import { NavItem, NavList } from '../../../molecules';
+import styles from './pagination.module.scss';
+
+export type PaginationItemKind = 'backward' | 'forward' | 'number';
+
+type RenderPaginationItemAriaLabelProps = {
+ /**
+ * Does the item represent the current page?
+ */
+ isCurrentPage?: boolean;
+ /**
+ * The item kind.
+ */
+ kind: PaginationItemKind;
+ /**
+ * The linked page number.
+ */
+ pageNumber: number;
+};
+
+export type RenderPaginationItemAriaLabel = (
+ props: RenderPaginationItemAriaLabelProps
+) => string;
+
+export type RenderPaginationLink = (page: number) => string;
+
+export type PaginationProps = Omit<NavProps, 'children'> & {
+ /**
+ * The currently active page number.
+ */
+ current: number;
+ /**
+ * Function used to provide an accessible label to pagination items.
+ */
+ renderItemAriaLabel: RenderPaginationItemAriaLabel;
+ /**
+ * Function used to create the href provided for each page link.
+ */
+ renderLink: RenderPaginationLink;
+ /**
+ * The number of pages to show on each side of the current page.
+ *
+ * @default 1
+ */
+ siblings?: number;
+ /**
+ * The total number of pages.
+ */
+ total: number;
+};
+
+type GetPagesProps = Pick<PaginationProps, 'current' | 'total'> & {
+ displayRange: number;
+};
+
+const getPages = ({ current, displayRange, total }: GetPagesProps) =>
+ Array.from({ length: total }, (_, index) => {
+ const page = index + 1;
+ const isFirstPage = page === 1;
+ const isLastPage = page === total;
+ const isOutOfRangeFromStart = page < current - displayRange && !isFirstPage;
+ const isOutOfRangeFromEnd = page > current + displayRange && !isLastPage;
+ const isOutOfRange = isOutOfRangeFromStart || isOutOfRangeFromEnd;
+ const ellipsisId = isOutOfRangeFromStart
+ ? 'start-ellipsis'
+ : 'end-ellipsis';
+
+ return {
+ id: isOutOfRange ? ellipsisId : `page-${page}`,
+ number: isOutOfRangeFromStart || isOutOfRangeFromEnd ? null : page,
+ };
+ }).filter(
+ (page, index, allPages) =>
+ index === 0 || page.number !== allPages[index - 1]?.number
+ );
+
+const PaginationWithRef: ForwardRefRenderFunction<
+ HTMLElement,
+ PaginationProps
+> = (
+ {
+ className = '',
+ current,
+ renderItemAriaLabel,
+ renderLink,
+ siblings = 1,
+ total,
+ ...props
+ },
+ ref
+) => {
+ const paginationClass = `${styles.wrapper} ${className}`;
+ const displayRange =
+ current === 1 || current === total ? siblings + 1 : siblings;
+ const hasPreviousPage = current > 1;
+ const hasNextPage = current < total;
+ const pages = getPages({ current, displayRange, total });
+ const ellipsis = '\u2026' as const;
+
+ return (
+ <Nav {...props} className={paginationClass} ref={ref}>
+ <NavList
+ className={styles.list}
+ isInline
+ // eslint-disable-next-line react/jsx-no-literals
+ spacing="xs"
+ >
+ {hasPreviousPage ? (
+ <NavItem className={styles.item}>
+ <ButtonLink
+ aria-label={renderItemAriaLabel({
+ kind: 'backward',
+ pageNumber: current - 1,
+ })}
+ // eslint-disable-next-line react/jsx-no-literals
+ kind="secondary"
+ to={renderLink(current - 1)}
+ >
+ <Icon
+ aria-hidden
+ // eslint-disable-next-line react/jsx-no-literals
+ shape="arrow"
+ // eslint-disable-next-line react/jsx-no-literals
+ orientation="left"
+ />
+ </ButtonLink>
+ </NavItem>
+ ) : null}
+ {pages.map((page) => {
+ const isCurrentPage = page.number === current;
+
+ return (
+ <NavItem className={styles.item} key={page.id}>
+ <ButtonLink
+ aria-current={isCurrentPage ? 'page' : undefined}
+ aria-label={
+ page.number
+ ? renderItemAriaLabel({
+ isCurrentPage,
+ kind: 'number',
+ pageNumber: page.number,
+ })
+ : undefined
+ }
+ isDisabled={page.number === null || isCurrentPage}
+ // eslint-disable-next-line react/jsx-no-literals
+ kind="secondary"
+ to={page.number ? renderLink(page.number) : ''}
+ >
+ {page.number ?? ellipsis}
+ </ButtonLink>
+ </NavItem>
+ );
+ })}
+ {hasNextPage ? (
+ <NavItem className={styles.item}>
+ <ButtonLink
+ aria-label={renderItemAriaLabel({
+ kind: 'forward',
+ pageNumber: current + 1,
+ })}
+ // eslint-disable-next-line react/jsx-no-literals
+ kind="secondary"
+ to={renderLink(current + 1)}
+ >
+ <Icon
+ aria-hidden
+ // eslint-disable-next-line react/jsx-no-literals
+ shape="arrow"
+ // eslint-disable-next-line react/jsx-no-literals
+ orientation="right"
+ />
+ </ButtonLink>
+ </NavItem>
+ ) : null}
+ </NavList>
+ </Nav>
+ );
+};
+
+export const Pagination = forwardRef(PaginationWithRef);