import React, { forwardRef } from "react"; import { ChevronLeftIcon, ChevronRightIcon } from "@navikt/aksel-icons"; import type { AkselStatusColorRole } from "@navikt/ds-tokens/types"; import { AkselColor } from "../types"; import { BodyShort, Heading } from "../typography"; import { useId } from "../utils-external"; import { cl } from "../utils/helpers"; import { useI18n } from "../utils/i18n/i18n.hooks"; import PaginationItem, { PaginationItemProps, PaginationItemType, } from "./PaginationItem"; interface RenderItemProps extends Pick< PaginationItemProps, "className" | "disabled" | "selected" | "icon" | "iconPosition" > { children: React.ReactNode; onClick: React.MouseEventHandler; page: number; size: Exclude; } export interface PaginationProps extends React.HTMLAttributes { /** * Current page. * * Pagination indexing starts at 1. */ page: number; /** * Number of always visible pages before and after the current page. * @default 1 */ siblingCount?: number; /** * Number of always visible pages at the beginning and end. * @default 1 */ boundaryCount?: number; /** * Callback when current page changes. */ onPageChange?: (page: number) => void; /** * Total number of pages. */ count: number; /** * Changes padding, height and font-size. * @default "medium" */ size?: "medium" | "small" | "xsmall"; /** * Display text alongside "previous" and "next" icons. * @default false */ prevNextTexts?: boolean; /** * Override pagination item rendering. * @default PaginationItem */ renderItem?: (item: RenderItemProps) => ReturnType; /** * Pagination heading. We recommend adding heading instead of `aria-label` to help assistive technologies with an extra navigation-stop. */ srHeading?: { tag: "h2" | "h3" | "h4" | "h5" | "h6"; text: string; }; /** * Overrides color. * * We have disallowed status-colors. * @default "neutral" * @see 🏷️ {@link AkselColor} * @see [📝 Documentation](https://aksel.nav.no/grunnleggende/styling/farger-tokens) * @private */ "data-color"?: Exclude; } interface PaginationType extends React.ForwardRefExoticComponent< PaginationProps & React.RefAttributes > { Item: PaginationItemType; } export const getSteps = ({ page, count, boundaryCount = 1, siblingCount = 1, }: Pick< PaginationProps, "page" | "count" | "boundaryCount" | "siblingCount" >) => { const range = (start: number, end: number) => Array.from({ length: end - start + 1 }, (_, i) => start + i); if (count <= (boundaryCount + siblingCount) * 2 + 3) return range(1, count); const startPages = range(1, boundaryCount); const endPages = range(count - boundaryCount + 1, count); const siblingsStart = Math.max( Math.min(page - siblingCount, count - boundaryCount - siblingCount * 2 - 1), boundaryCount + 2, ); const siblingsEnd = siblingsStart + siblingCount * 2; return [ ...startPages, siblingsStart - (startPages[startPages.length - 1] ?? 0) === 2 ? siblingsStart - 1 : "ellipsis", ...range(siblingsStart, siblingsEnd), (endPages[0] ?? count + 1) - siblingsEnd === 2 ? siblingsEnd + 1 : "ellipsis", ...endPages, ]; }; /** * A component that displays pagination controls. * * @see [📝 Documentation](https://aksel.nav.no/komponenter/core/pagination) * @see 🏷️ {@link PaginationProps} * * @example * ```jsx * * ``` */ export const Pagination = forwardRef( ( { page, onPageChange, count, boundaryCount = 1, siblingCount = 1, className, size = "medium", prevNextTexts = false, srHeading, "aria-labelledby": ariaLabelledBy, renderItem: Item = PaginationItem, "data-color": color = "neutral", ...rest }, ref, ) => { const headingId = useId(); const translate = useI18n("Pagination"); if (page < 1) { console.error("page cannot be less than 1"); return null; } if (count < 1) { console.error("count cannot be less than 1"); return null; } if (boundaryCount < 0) { console.error("boundaryCount cannot be less than 0"); return null; } if (siblingCount < 0) { console.error("siblingCount cannot be less than 0"); return null; } return ( ); }, ) as PaginationType; Pagination.Item = PaginationItem; export default Pagination;