@carbon/react
Version:
React components for the Carbon Design System
348 lines (346 loc) • 12.1 kB
JavaScript
/**
* Copyright IBM Corp. 2016, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import { usePrefix } from "../../internal/usePrefix.js";
import { IconButton } from "../IconButton/index.js";
import { clamp } from "../../internal/clamp.js";
import { useMatchMedia } from "../../internal/useMatchMedia.js";
import classNames from "classnames";
import React, { useEffect, useRef, useState } from "react";
import PropTypes from "prop-types";
import { jsx, jsxs } from "react/jsx-runtime";
import { CaretLeft, CaretRight, OverflowMenuHorizontal } from "@carbon/icons-react";
import { breakpoints } from "@carbon/layout";
//#region src/components/PaginationNav/PaginationNav.tsx
/**
* Copyright IBM Corp. 2020, 2025
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
const translationIds = {
"carbon.pagination-nav.next": "carbon.pagination-nav.next",
"carbon.pagination-nav.previous": "carbon.pagination-nav.previous",
"carbon.pagination-nav.item": "carbon.pagination-nav.item",
"carbon.pagination-nav.active": "carbon.pagination-nav.active",
"carbon.pagination-nav.of": "carbon.pagination-nav.of"
};
const defaultTranslations = {
[translationIds["carbon.pagination-nav.next"]]: "Next",
[translationIds["carbon.pagination-nav.previous"]]: "Previous",
[translationIds["carbon.pagination-nav.item"]]: "Page",
[translationIds["carbon.pagination-nav.active"]]: "Active",
[translationIds["carbon.pagination-nav.of"]]: "of"
};
const defaultTranslateWithId = (messageId) => {
return defaultTranslations[messageId];
};
function usePrevious(value) {
const ref = useRef(null);
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function calculateCuts(page, totalItems, itemsDisplayedOnPage, splitPoint = null) {
if (itemsDisplayedOnPage >= totalItems) return {
front: 0,
back: 0
};
const split = splitPoint || Math.ceil(itemsDisplayedOnPage / 2) - 1;
let frontHidden = page + 1 - split;
let backHidden = totalItems - page - (itemsDisplayedOnPage - split) + 1;
if (frontHidden <= 1) {
backHidden -= frontHidden <= 0 ? Math.abs(frontHidden) + 1 : 0;
frontHidden = 0;
}
if (backHidden <= 1) {
frontHidden -= backHidden <= 0 ? Math.abs(backHidden) + 1 : 0;
backHidden = 0;
}
return {
front: frontHidden,
back: backHidden
};
}
function DirectionButton({ direction, label, disabled, onClick, tooltipAlignment = "center", tooltipPosition = "bottom" }) {
const prefix = usePrefix();
const align = tooltipAlignment === "center" ? tooltipPosition : `${tooltipPosition}-${tooltipAlignment}`;
return /* @__PURE__ */ jsx("li", {
className: `${prefix}--pagination-nav__list-item`,
children: /* @__PURE__ */ jsx(IconButton, {
align,
disabled,
kind: "ghost",
label,
onClick,
children: direction === "forward" ? /* @__PURE__ */ jsx(CaretRight, {}) : /* @__PURE__ */ jsx(CaretLeft, {})
})
});
}
function PaginationItem({ page, isActive, onClick, translateWithId: t = defaultTranslateWithId }) {
const prefix = usePrefix();
const itemLabel = t("carbon.pagination-nav.item");
return /* @__PURE__ */ jsx("li", {
className: `${prefix}--pagination-nav__list-item`,
children: /* @__PURE__ */ jsxs("button", {
type: "button",
className: classNames(`${prefix}--pagination-nav__page`, { [`${prefix}--pagination-nav__page--active`]: isActive }),
onClick,
"data-page": page,
"aria-current": isActive ? "page" : void 0,
children: [/* @__PURE__ */ jsx("span", {
className: `${prefix}--pagination-nav__accessibility-label`,
children: isActive ? `${t("carbon.pagination-nav.active")}, ${itemLabel}` : itemLabel
}), page]
})
});
}
function PaginationOverflow({ fromIndex = NaN, count = NaN, onSelect, disableOverflow, translateWithId: t = defaultTranslateWithId }) {
const prefix = usePrefix();
if (disableOverflow === true && count > 1) return /* @__PURE__ */ jsx("li", {
className: `${prefix}--pagination-nav__list-item`,
children: /* @__PURE__ */ jsxs("div", {
className: `${prefix}--pagination-nav__select`,
children: [/* @__PURE__ */ jsx("select", {
className: `${prefix}--pagination-nav__page ${prefix}--pagination-nav__page--select`,
"aria-label": `Select ${t("carbon.pagination-nav.item")} number`,
disabled: true
}), /* @__PURE__ */ jsx("div", {
className: `${prefix}--pagination-nav__select-icon-wrapper`,
children: /* @__PURE__ */ jsx(OverflowMenuHorizontal, { className: `${prefix}--pagination-nav__select-icon` })
})]
})
});
if (count > 1) return /* @__PURE__ */ jsx("li", {
className: `${prefix}--pagination-nav__list-item`,
children: /* @__PURE__ */ jsxs("div", {
className: `${prefix}--pagination-nav__select`,
children: [/* @__PURE__ */ jsxs("select", {
className: `${prefix}--pagination-nav__page ${prefix}--pagination-nav__page--select`,
"aria-label": `Select ${t("carbon.pagination-nav.item")} number`,
onChange: (e) => {
const index = Number(e.target.value);
onSelect?.(index);
},
children: [/* @__PURE__ */ jsx("option", {
value: "",
hidden: true
}), [...Array(count)].map((e, i) => /* @__PURE__ */ jsx("option", {
value: (fromIndex + i).toString(),
"data-page": fromIndex + i + 1,
children: fromIndex + i + 1
}, `overflow-${fromIndex + i}`))]
}), /* @__PURE__ */ jsx("div", {
className: `${prefix}--pagination-nav__select-icon-wrapper`,
children: /* @__PURE__ */ jsx(OverflowMenuHorizontal, { className: `${prefix}--pagination-nav__select-icon` })
})]
})
});
if (count === 1) return /* @__PURE__ */ jsx(PaginationItem, {
page: fromIndex + 1,
translateWithId: t,
onClick: () => {
onSelect?.(fromIndex);
}
});
return null;
}
const PaginationNav = React.forwardRef(({ className, onChange = () => {}, totalItems = NaN, disableOverflow, itemsShown = 10, page = 0, loop = false, size = "lg", tooltipAlignment, tooltipPosition, translateWithId: t = defaultTranslateWithId, ...rest }, ref) => {
const isSm = useMatchMedia(`(max-width: ${breakpoints.sm.width})`);
let numberOfPages;
switch (size) {
case "md":
numberOfPages = itemsShown === 4 ? itemsShown : 5;
break;
case "sm":
numberOfPages = clamp(itemsShown, 4, 7);
break;
default:
numberOfPages = 4;
break;
}
const [currentPage, setCurrentPage] = useState(page);
const [itemsDisplayedOnPage, setItemsDisplayedOnPage] = useState(itemsShown >= 4 && !isSm ? itemsShown : numberOfPages);
const [cuts, setCuts] = useState(calculateCuts(currentPage, totalItems, itemsDisplayedOnPage));
const prevPage = usePrevious(currentPage);
const prefix = usePrefix();
const [isOverflowDisabled, setIsOverFlowDisabled] = useState(disableOverflow);
function jumpToItem(index) {
if (index >= 0 && index < totalItems) {
setCurrentPage(index);
onChange(index);
}
}
function jumpToNext() {
const nextIndex = currentPage + 1;
if (nextIndex >= totalItems) {
if (loop) jumpToItem(0);
} else jumpToItem(nextIndex);
}
function jumpToPrevious() {
const previousIndex = currentPage - 1;
if (previousIndex < 0) {
if (loop) jumpToItem(totalItems - 1);
} else jumpToItem(previousIndex);
}
function pageWouldBeHidden(page) {
const wouldBeHiddenInFront = page >= (itemsDisplayedOnPage <= 4 && page > 1 ? 0 : 1) && page <= cuts.front || page === 0;
const wouldBeHiddenInBack = page >= totalItems - cuts.back - 1 && page <= totalItems - 2;
return wouldBeHiddenInFront || wouldBeHiddenInBack;
}
useEffect(() => {
setCurrentPage(page);
}, [page]);
useEffect(() => {
const itemsToBeShown = itemsShown >= 4 && !isSm ? itemsShown : numberOfPages;
setItemsDisplayedOnPage(Math.max(itemsToBeShown, 4));
setCuts(calculateCuts(currentPage, totalItems, Math.max(itemsToBeShown, 4)));
}, [
totalItems,
itemsShown,
isSm,
size
]);
useEffect(() => {
if (pageWouldBeHidden(currentPage)) if (currentPage - (prevPage || 0) > 0) setCuts(calculateCuts(currentPage, totalItems, itemsDisplayedOnPage, itemsDisplayedOnPage - 3));
else setCuts(calculateCuts(currentPage, totalItems, itemsDisplayedOnPage, itemsDisplayedOnPage > 4 ? 2 : 1));
}, [currentPage]);
useEffect(() => {
setIsOverFlowDisabled(disableOverflow);
}, [disableOverflow]);
const classNames$1 = classNames(`${prefix}--pagination-nav`, className, { [`${prefix}--layout--size-${size}`]: size });
const backwardButtonDisabled = !loop && currentPage === 0;
const forwardButtonDisabled = !loop && currentPage === totalItems - 1;
const startOffset = itemsDisplayedOnPage <= 4 && currentPage > 1 ? 0 : 1;
return /* @__PURE__ */ jsxs("nav", {
className: classNames$1,
ref,
...rest,
children: [/* @__PURE__ */ jsxs("ul", {
className: `${prefix}--pagination-nav__list`,
children: [
/* @__PURE__ */ jsx(DirectionButton, {
direction: "backward",
"aria-label": t("carbon.pagination-nav.previous"),
label: t("carbon.pagination-nav.previous"),
disabled: backwardButtonDisabled,
onClick: jumpToPrevious,
tooltipAlignment,
tooltipPosition
}),
(itemsDisplayedOnPage >= 5 || itemsDisplayedOnPage <= 4 && currentPage <= 1) && /* @__PURE__ */ jsx(PaginationItem, {
page: 1,
translateWithId: t,
isActive: currentPage === 0,
onClick: () => {
jumpToItem(0);
}
}),
/* @__PURE__ */ jsx(PaginationOverflow, {
fromIndex: startOffset,
count: cuts.front,
onSelect: jumpToItem,
disableOverflow: isOverflowDisabled
}),
[...Array(totalItems)].map((e, i) => i).slice(startOffset + cuts.front, (1 + cuts.back) * -1).map((item) => /* @__PURE__ */ jsx(PaginationItem, {
page: item + 1,
translateWithId: t,
isActive: currentPage === item,
onClick: () => {
jumpToItem(item);
}
}, `item-${item}`)),
/* @__PURE__ */ jsx(PaginationOverflow, {
fromIndex: totalItems - cuts.back - 1,
count: cuts.back,
onSelect: jumpToItem,
disableOverflow: isOverflowDisabled
}),
totalItems > 1 && /* @__PURE__ */ jsx(PaginationItem, {
page: totalItems,
translateWithId: t,
isActive: currentPage === totalItems - 1,
onClick: () => {
jumpToItem(totalItems - 1);
}
}),
/* @__PURE__ */ jsx(DirectionButton, {
direction: "forward",
"aria-label": t("carbon.pagination-nav.next"),
label: t("carbon.pagination-nav.next"),
disabled: forwardButtonDisabled,
onClick: jumpToNext,
tooltipAlignment,
tooltipPosition
})
]
}), /* @__PURE__ */ jsx("div", {
"aria-live": "polite",
"aria-atomic": "true",
className: `${prefix}--pagination-nav__accessibility-label`,
children: `${t("carbon.pagination-nav.item")} ${currentPage + 1} ${t("carbon.pagination-nav.of")} ${totalItems}`
})]
});
});
DirectionButton.propTypes = {
direction: PropTypes.oneOf(["forward", "backward"]),
disabled: PropTypes.bool,
label: PropTypes.string,
onClick: PropTypes.func,
tooltipAlignment: PropTypes.oneOf([
"start",
"center",
"end"
]),
tooltipPosition: PropTypes.oneOf([
"top",
"right",
"bottom",
"left"
])
};
PaginationItem.propTypes = {
isActive: PropTypes.bool,
onClick: PropTypes.func,
page: PropTypes.number,
translateWithId: PropTypes.func
};
PaginationOverflow.propTypes = {
count: PropTypes.number,
fromIndex: PropTypes.number,
onSelect: PropTypes.func,
translateWithId: PropTypes.func
};
PaginationNav.displayName = "PaginationNav";
PaginationNav.propTypes = {
className: PropTypes.string,
disableOverflow: PropTypes.bool,
itemsShown: PropTypes.number,
loop: PropTypes.bool,
onChange: PropTypes.func,
page: PropTypes.number,
size: PropTypes.oneOf([
"sm",
"md",
"lg"
]),
tooltipAlignment: PropTypes.oneOf([
"start",
"center",
"end"
]),
tooltipPosition: PropTypes.oneOf([
"top",
"right",
"bottom",
"left"
]),
totalItems: PropTypes.number,
translateWithId: PropTypes.func
};
//#endregion
export { PaginationNav as default };