aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/molecules/nav/pagination.tsx
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-05-24 19:35:12 +0200
committerGitHub <noreply@github.com>2022-05-24 19:35:12 +0200
commitc85ab5ad43ccf52881ee224672c41ec30021cf48 (patch)
tree8058808d9bfca19383f120c46b34d99ff2f89f63 /src/components/molecules/nav/pagination.tsx
parent52404177c07a2aab7fc894362fb3060dff2431a0 (diff)
parent11b9de44a4b2f305a6a484187805e429b2767118 (diff)
refactor: use storybook and atomic design (#16)
BREAKING CHANGE: rewrite most of the Typescript types, so the content format (the meta in particular) needs to be updated.
Diffstat (limited to 'src/components/molecules/nav/pagination.tsx')
-rw-r--r--src/components/molecules/nav/pagination.tsx220
1 files changed, 220 insertions, 0 deletions
diff --git a/src/components/molecules/nav/pagination.tsx b/src/components/molecules/nav/pagination.tsx
new file mode 100644
index 0000000..934b50a
--- /dev/null
+++ b/src/components/molecules/nav/pagination.tsx
@@ -0,0 +1,220 @@
+import ButtonLink from '@components/atoms/buttons/button-link';
+import { FC, Fragment, ReactNode } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './pagination.module.scss';
+
+export type PaginationProps = {
+ /**
+ * An accessible name for the pagination.
+ */
+ 'aria-label'?: string;
+ /**
+ * The url part before page number. Default: /page/
+ */
+ baseUrl?: string;
+ /**
+ * Set additional classnames to the pagination wrapper.
+ */
+ className?: string;
+ /**
+ * The current page number.
+ */
+ current: number;
+ /**
+ * The number of items per page.
+ */
+ perPage: number;
+ /**
+ * The number of siblings on one side of the current page. Default: 1.
+ */
+ siblings?: number;
+ /**
+ * The total number of items.
+ */
+ total: number;
+};
+
+/**
+ * Pagination component
+ *
+ * Render a page-based navigation.
+ */
+const Pagination: FC<PaginationProps> = ({
+ baseUrl = '/page/',
+ className = '',
+ current,
+ perPage,
+ siblings = 2,
+ total,
+ ...props
+}) => {
+ const intl = useIntl();
+ const totalPages = Math.round(total / perPage);
+ const hasPreviousPage = current > 1;
+ const previousPageName = intl.formatMessage(
+ {
+ defaultMessage: '{icon} Previous page',
+ description: 'Pagination: previous page link',
+ id: 'aMFqPH',
+ },
+ { icon: '←' }
+ );
+ const previousPageUrl = `${baseUrl}${current - 1}`;
+ const hasNextPage = current < totalPages;
+ const nextPageName = intl.formatMessage(
+ {
+ defaultMessage: 'Next page {icon}',
+ description: 'Pagination: Next page link',
+ id: 'R4yaW6',
+ },
+ { icon: '→' }
+ );
+ const nextPageUrl = `${baseUrl}${current + 1}`;
+
+ /**
+ * Create an array with a range of values from start value to end value.
+ *
+ * @param {number} start - The first value.
+ * @param {number} end - The last value.
+ * @returns {number[]} An array from start value to end value.
+ */
+ const range = (start: number, end: number): number[] => {
+ const length = end - start + 1;
+
+ return Array.from({ length }, (_, index) => index + start);
+ };
+
+ /**
+ * Get the pagination range.
+ *
+ * @param currentPage - The current page number.
+ * @param maxPages - The total pages number.
+ * @returns {(number|string)[]} An array of page numbers with or without dots.
+ */
+ const getPaginationRange = (
+ currentPage: number,
+ maxPages: number
+ ): (number | string)[] => {
+ const dots = '\u2026';
+
+ /**
+ * Show left dots if current page less left siblings is greater than the
+ * first two pages.
+ */
+ const hasLeftDots = currentPage - siblings > 2;
+
+ /**
+ * Show right dots if current page plus right siblings is lower than the
+ * total of pages less the last page.
+ */
+ const hasRightDots = currentPage + siblings < maxPages - 1;
+
+ if (hasLeftDots && hasRightDots) {
+ const middleItems = range(currentPage - siblings, currentPage + siblings);
+ return [1, dots, ...middleItems, dots, maxPages];
+ }
+
+ if (hasLeftDots) {
+ const rightItems = range(currentPage - siblings, maxPages);
+ return [1, dots, ...rightItems];
+ }
+
+ if (hasRightDots) {
+ const leftItems = range(1, currentPage + siblings);
+ return [...leftItems, dots, maxPages];
+ }
+
+ return range(1, maxPages);
+ };
+
+ /**
+ * Get a link or a span wrapped in a list item.
+ *
+ * @param {string} id - The item id.
+ * @param {ReactNode} body - The link body.
+ * @param {string} [link] - An URL.
+ * @returns {JSX.Element} The list item.
+ */
+ const getItem = (id: string, body: ReactNode, link?: string): JSX.Element => {
+ const linkModifier = id.startsWith('page') ? 'link--number' : '';
+ const kind = id === 'previous' || id === 'next' ? 'tertiary' : 'secondary';
+
+ return (
+ <li className={styles.item}>
+ {link ? (
+ <ButtonLink
+ kind={kind}
+ target={link}
+ className={`${styles.link} ${styles[linkModifier]}`}
+ >
+ {body}
+ </ButtonLink>
+ ) : (
+ <span className={`${styles.link} ${styles['link--disabled']}`}>
+ {body}
+ </span>
+ )}
+ </li>
+ );
+ };
+
+ /**
+ * Get the list of pages.
+ *
+ * @param {number} currentPage - The current page number.
+ * @param {number} maxPages - The total of pages.
+ * @returns {JSX.Element[]} The list items.
+ */
+ const getPages = (currentPage: number, maxPages: number): JSX.Element[] => {
+ const pagesRange = getPaginationRange(currentPage, maxPages);
+
+ return pagesRange.map((page, index) => {
+ const id = typeof page === 'string' ? `dots-${index}` : `page-${page}`;
+ const currentPagePrefix = intl.formatMessage({
+ defaultMessage: 'You are here:',
+ description: 'Pagination: current page indication',
+ id: 'yE/Jdz',
+ });
+ const body =
+ typeof page === 'string'
+ ? '\u2026'
+ : intl.formatMessage(
+ {
+ defaultMessage: '<a11y>Page </a11y>{number}',
+ description: 'Pagination: page number',
+ id: 'TSXPzr',
+ },
+ {
+ number: page,
+ a11y: (chunks: ReactNode) => (
+ <span className="screen-reader-text">
+ {page === currentPage && currentPagePrefix}
+ {chunks}
+ </span>
+ ),
+ }
+ );
+ const url =
+ page === currentPage || typeof page === 'string'
+ ? undefined
+ : `${baseUrl}${page}`;
+
+ return <Fragment key={`item-${id}`}>{getItem(id, body, url)}</Fragment>;
+ });
+ };
+
+ return (
+ <nav className={`${styles.wrapper} ${className}`} {...props}>
+ <ul className={`${styles.list} ${styles['list--pages']}`}>
+ {getPages(current, totalPages)}
+ </ul>
+ <ul className={styles.list}>
+ {hasPreviousPage &&
+ getItem('previous', previousPageName, previousPageUrl)}
+ {hasNextPage && getItem('next', nextPageName, nextPageUrl)}
+ </ul>
+ </nav>
+ );
+};
+
+export default Pagination;