@carbon/react
Version:
React components for the Carbon Design System
352 lines (350 loc) • 13.6 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.
*/
const require_runtime = require("../../_virtual/_rolldown/runtime.js");
const require_usePrefix = require("../../internal/usePrefix.js");
const require_index = require("../IconButton/index.js");
const require_clamp = require("../../internal/clamp.js");
const require_useMatchMedia = require("../../internal/useMatchMedia.js");
let classnames = require("classnames");
classnames = require_runtime.__toESM(classnames);
let react = require("react");
react = require_runtime.__toESM(react);
let prop_types = require("prop-types");
prop_types = require_runtime.__toESM(prop_types);
let react_jsx_runtime = require("react/jsx-runtime");
let _carbon_icons_react = require("@carbon/icons-react");
let _carbon_layout = require("@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 = (0, react.useRef)(null);
(0, react.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 = require_usePrefix.usePrefix();
const align = tooltipAlignment === "center" ? tooltipPosition : `${tooltipPosition}-${tooltipAlignment}`;
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
className: `${prefix}--pagination-nav__list-item`,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index.IconButton, {
align,
disabled,
kind: "ghost",
label,
onClick,
children: direction === "forward" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_carbon_icons_react.CaretRight, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_carbon_icons_react.CaretLeft, {})
})
});
}
function PaginationItem({ page, isActive, onClick, translateWithId: t = defaultTranslateWithId }) {
const prefix = require_usePrefix.usePrefix();
const itemLabel = t("carbon.pagination-nav.item");
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
className: `${prefix}--pagination-nav__list-item`,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
type: "button",
className: (0, classnames.default)(`${prefix}--pagination-nav__page`, { [`${prefix}--pagination-nav__page--active`]: isActive }),
onClick,
"data-page": page,
"aria-current": isActive ? "page" : void 0,
children: [/* @__PURE__ */ (0, react_jsx_runtime.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 = require_usePrefix.usePrefix();
if (disableOverflow === true && count > 1) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
className: `${prefix}--pagination-nav__list-item`,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
className: `${prefix}--pagination-nav__select`,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
className: `${prefix}--pagination-nav__page ${prefix}--pagination-nav__page--select`,
"aria-label": `Select ${t("carbon.pagination-nav.item")} number`,
disabled: true
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: `${prefix}--pagination-nav__select-icon-wrapper`,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_carbon_icons_react.OverflowMenuHorizontal, { className: `${prefix}--pagination-nav__select-icon` })
})]
})
});
if (count > 1) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
className: `${prefix}--pagination-nav__list-item`,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
className: `${prefix}--pagination-nav__select`,
children: [/* @__PURE__ */ (0, react_jsx_runtime.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__ */ (0, react_jsx_runtime.jsx)("option", {
value: "",
hidden: true
}), [...Array(count)].map((e, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
value: (fromIndex + i).toString(),
"data-page": fromIndex + i + 1,
children: fromIndex + i + 1
}, `overflow-${fromIndex + i}`))]
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: `${prefix}--pagination-nav__select-icon-wrapper`,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_carbon_icons_react.OverflowMenuHorizontal, { className: `${prefix}--pagination-nav__select-icon` })
})]
})
});
if (count === 1) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PaginationItem, {
page: fromIndex + 1,
translateWithId: t,
onClick: () => {
onSelect?.(fromIndex);
}
});
return null;
}
const PaginationNav = react.default.forwardRef(({ className, onChange = () => {}, totalItems = NaN, disableOverflow, itemsShown = 10, page = 0, loop = false, size = "lg", tooltipAlignment, tooltipPosition, translateWithId: t = defaultTranslateWithId, ...rest }, ref) => {
const isSm = require_useMatchMedia.useMatchMedia(`(max-width: ${_carbon_layout.breakpoints.sm.width})`);
let numberOfPages;
switch (size) {
case "md":
numberOfPages = itemsShown === 4 ? itemsShown : 5;
break;
case "sm":
numberOfPages = require_clamp.clamp(itemsShown, 4, 7);
break;
default:
numberOfPages = 4;
break;
}
const [currentPage, setCurrentPage] = (0, react.useState)(page);
const [itemsDisplayedOnPage, setItemsDisplayedOnPage] = (0, react.useState)(itemsShown >= 4 && !isSm ? itemsShown : numberOfPages);
const [cuts, setCuts] = (0, react.useState)(calculateCuts(currentPage, totalItems, itemsDisplayedOnPage));
const prevPage = usePrevious(currentPage);
const prefix = require_usePrefix.usePrefix();
const [isOverflowDisabled, setIsOverFlowDisabled] = (0, react.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;
}
(0, react.useEffect)(() => {
setCurrentPage(page);
}, [page]);
(0, react.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
]);
(0, react.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]);
(0, react.useEffect)(() => {
setIsOverFlowDisabled(disableOverflow);
}, [disableOverflow]);
const classNames = (0, classnames.default)(`${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__ */ (0, react_jsx_runtime.jsxs)("nav", {
className: classNames,
ref,
...rest,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
className: `${prefix}--pagination-nav__list`,
children: [
/* @__PURE__ */ (0, react_jsx_runtime.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__ */ (0, react_jsx_runtime.jsx)(PaginationItem, {
page: 1,
translateWithId: t,
isActive: currentPage === 0,
onClick: () => {
jumpToItem(0);
}
}),
/* @__PURE__ */ (0, react_jsx_runtime.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__ */ (0, react_jsx_runtime.jsx)(PaginationItem, {
page: item + 1,
translateWithId: t,
isActive: currentPage === item,
onClick: () => {
jumpToItem(item);
}
}, `item-${item}`)),
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PaginationOverflow, {
fromIndex: totalItems - cuts.back - 1,
count: cuts.back,
onSelect: jumpToItem,
disableOverflow: isOverflowDisabled
}),
totalItems > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PaginationItem, {
page: totalItems,
translateWithId: t,
isActive: currentPage === totalItems - 1,
onClick: () => {
jumpToItem(totalItems - 1);
}
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DirectionButton, {
direction: "forward",
"aria-label": t("carbon.pagination-nav.next"),
label: t("carbon.pagination-nav.next"),
disabled: forwardButtonDisabled,
onClick: jumpToNext,
tooltipAlignment,
tooltipPosition
})
]
}), /* @__PURE__ */ (0, react_jsx_runtime.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: prop_types.default.oneOf(["forward", "backward"]),
disabled: prop_types.default.bool,
label: prop_types.default.string,
onClick: prop_types.default.func,
tooltipAlignment: prop_types.default.oneOf([
"start",
"center",
"end"
]),
tooltipPosition: prop_types.default.oneOf([
"top",
"right",
"bottom",
"left"
])
};
PaginationItem.propTypes = {
isActive: prop_types.default.bool,
onClick: prop_types.default.func,
page: prop_types.default.number,
translateWithId: prop_types.default.func
};
PaginationOverflow.propTypes = {
count: prop_types.default.number,
fromIndex: prop_types.default.number,
onSelect: prop_types.default.func,
translateWithId: prop_types.default.func
};
PaginationNav.displayName = "PaginationNav";
PaginationNav.propTypes = {
className: prop_types.default.string,
disableOverflow: prop_types.default.bool,
itemsShown: prop_types.default.number,
loop: prop_types.default.bool,
onChange: prop_types.default.func,
page: prop_types.default.number,
size: prop_types.default.oneOf([
"sm",
"md",
"lg"
]),
tooltipAlignment: prop_types.default.oneOf([
"start",
"center",
"end"
]),
tooltipPosition: prop_types.default.oneOf([
"top",
"right",
"bottom",
"left"
]),
totalItems: prop_types.default.number,
translateWithId: prop_types.default.func
};
//#endregion
exports.default = PaginationNav;