@carbon/react
Version:
React components for the Carbon Design System
298 lines (296 loc) • 11 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 { useFallbackId } from "../../internal/useId.js";
import { IconButton } from "../IconButton/index.js";
import { usePreviousValue } from "../../internal/usePreviousValue.js";
import Select_default from "../Select/index.js";
import SelectItem_default from "../SelectItem/index.js";
import classNames from "classnames";
import React, { useEffect, useMemo, useRef, useState } from "react";
import PropTypes from "prop-types";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import { CaretLeft, CaretRight } from "@carbon/icons-react";
import isEqual from "react-fast-compare";
//#region src/components/Pagination/Pagination.tsx
/**
* 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 isPaginationPageSizeArray = (sizes) => typeof sizes[0] === "object" && sizes[0] !== null;
const mapPageSizesToObject = (sizes) => {
if (isPaginationPageSizeArray(sizes)) return sizes;
return sizes.map((size) => ({
text: String(size),
value: size
}));
};
function renderSelectItems(total) {
let counter = 1;
const itemArr = [];
while (counter <= total) {
itemArr.push(/* @__PURE__ */ jsx(SelectItem_default, {
value: counter,
text: String(counter)
}, counter));
counter++;
}
return itemArr;
}
const getPageSize = (pageSizes, pageSize) => {
if (typeof pageSize !== "undefined") {
if (pageSizes.find((size) => {
return pageSize === size.value;
})) return pageSize;
}
return pageSizes[0].value;
};
const Pagination = React.forwardRef(({ backwardText = "Previous page", className: customClassName = "", disabled = false, forwardText = "Next page", id, isLastPage = false, itemText = (min, max) => `${min}–${max} items`, itemRangeText = (min, max, total) => `${min}–${max} of ${total} items`, itemsPerPageText = "Items per page:", onChange, pageNumberText: _pageNumberText, pageRangeText = (_current, total) => `of ${total} ${total === 1 ? "page" : "pages"}`, pageSelectLabelText = (total) => `Page of ${total} ${total === 1 ? "page" : "pages"}`, page: controlledPage = 1, pageInputDisabled, pageSize: controlledPageSize, pageSizeInputDisabled, pageSizes: controlledPageSizes, pageText = (page) => `page ${page}`, pagesUnknown = false, size = "md", totalItems, ...rest }, ref) => {
const prefix = usePrefix();
const inputId = useFallbackId(id?.toString());
const backBtnRef = useRef(null);
const forwardBtnRef = useRef(null);
const pendingChangeRef = useRef(null);
const normalizedControlledPageSizes = useMemo(() => mapPageSizesToObject(controlledPageSizes), [controlledPageSizes]);
const prevControlledPageSize = usePreviousValue(controlledPageSize);
const prevControlledPageSizes = usePreviousValue(normalizedControlledPageSizes);
const [pageSizes, setPageSizes] = useState(normalizedControlledPageSizes);
const [page, setPage] = useState(controlledPage);
const [focusTarget, setFocusTarget] = useState(null);
const [pageSize, setPageSize] = useState(() => {
return getPageSize(normalizedControlledPageSizes, controlledPageSize);
});
const className = classNames({
[`${prefix}--pagination`]: true,
[`${prefix}--pagination--${size}`]: size,
[customClassName]: !!customClassName
});
const totalPages = totalItems ? Math.max(Math.ceil(totalItems / pageSize), 1) : 1;
const backButtonDisabled = disabled || page === 1;
const backButtonClasses = classNames({
[`${prefix}--pagination__button`]: true,
[`${prefix}--pagination__button--backward`]: true,
[`${prefix}--pagination__button--no-index`]: backButtonDisabled
});
const forwardButtonDisabled = disabled || page === totalPages && !pagesUnknown;
const forwardButtonClasses = classNames({
[`${prefix}--pagination__button`]: true,
[`${prefix}--pagination__button--forward`]: true,
[`${prefix}--pagination__button--no-index`]: forwardButtonDisabled
});
const selectItems = renderSelectItems(totalPages);
const focusMap = {
backward: backBtnRef,
forward: forwardBtnRef
};
const handleFocus = (target) => {
const targetRef = focusMap[target];
if (targetRef?.current && !targetRef.current.disabled) targetRef.current.focus();
};
useEffect(() => {
if (focusTarget) {
handleFocus(focusTarget);
setFocusTarget(null);
}
}, [focusTarget]);
useEffect(() => {
if (pendingChangeRef.current && onChange) {
onChange(pendingChangeRef.current);
pendingChangeRef.current = null;
}
}, [
onChange,
page,
pageSize
]);
useEffect(() => {
setPage(controlledPage);
}, [controlledPage]);
useEffect(() => {
if (typeof prevControlledPageSizes === "undefined" || isEqual(prevControlledPageSizes, normalizedControlledPageSizes)) return;
setPageSizes((prev) => isEqual(normalizedControlledPageSizes, prev) ? prev : normalizedControlledPageSizes);
const nextPageSize = getPageSize(normalizedControlledPageSizes, controlledPageSize ?? pageSize);
const hasPageSize = normalizedControlledPageSizes.some((size) => {
return size.value === pageSize;
});
const nextPage = hasPageSize ? page : 1;
const hasControlledPageSize = typeof controlledPageSize !== "undefined";
const hasValidControlledPageSize = hasControlledPageSize ? normalizedControlledPageSizes.some((size) => size.value === controlledPageSize) : false;
if (!hasPageSize) setPage(nextPage);
if (nextPageSize !== pageSize) setPageSize(nextPageSize);
if (onChange && (!hasControlledPageSize || !hasValidControlledPageSize) && (nextPage !== page || nextPageSize !== pageSize)) pendingChangeRef.current = {
page: nextPage,
pageSize: nextPageSize
};
}, [
controlledPageSize,
normalizedControlledPageSizes,
onChange,
page,
pageSize,
prevControlledPageSizes
]);
useEffect(() => {
if (controlledPageSize === prevControlledPageSize) return;
setPageSize(getPageSize(normalizedControlledPageSizes, controlledPageSize));
}, [
controlledPageSize,
normalizedControlledPageSizes,
prevControlledPageSize
]);
function handleSizeChange(event) {
const changes = {
pageSize: Number(event.target.value),
page: 1
};
setPage(changes.page);
setPageSize(changes.pageSize);
if (onChange) onChange(changes);
}
function handlePageInputChange(event) {
const page = Number(event.target.value);
if (page > 0 && totalItems && page <= Math.max(Math.ceil(totalItems / pageSize), 1)) {
setPage(page);
if (onChange) onChange({
page,
pageSize
});
}
}
function incrementPage() {
const nextPage = page + 1;
setPage(nextPage);
if (nextPage === totalPages) setFocusTarget("backward");
if (onChange) onChange({
page: nextPage,
pageSize,
ref: backBtnRef
});
}
function decrementPage() {
const nextPage = page - 1;
setPage(nextPage);
if (nextPage === 1) setFocusTarget("forward");
if (onChange) onChange({
page: nextPage,
pageSize,
ref: forwardBtnRef
});
}
return /* @__PURE__ */ jsxs("div", {
className,
ref,
...rest,
children: [/* @__PURE__ */ jsxs("div", {
className: `${prefix}--pagination__left`,
children: [
/* @__PURE__ */ jsx("label", {
id: `${prefix}-pagination-select-${inputId}-count-label`,
className: `${prefix}--pagination__text`,
htmlFor: `${prefix}-pagination-select-${inputId}`,
children: itemsPerPageText
}),
/* @__PURE__ */ jsx(Select_default, {
id: `${prefix}-pagination-select-${inputId}`,
className: `${prefix}--select__item-count`,
labelText: "",
hideLabel: true,
noLabel: true,
inline: true,
onChange: handleSizeChange,
disabled: pageSizeInputDisabled || disabled,
value: pageSize,
children: pageSizes.map((sizeObj) => /* @__PURE__ */ jsx(SelectItem_default, {
value: sizeObj.value,
text: String(sizeObj.text)
}, sizeObj.value))
}),
/* @__PURE__ */ jsx("span", {
className: `${prefix}--pagination__text ${prefix}--pagination__items-count`,
children: pagesUnknown || !totalItems ? totalItems === 0 ? itemRangeText(0, 0, 0) : itemText(pageSize * (page - 1) + 1, page * pageSize) : itemRangeText(Math.min(pageSize * (page - 1) + 1, totalItems), Math.min(page * pageSize, totalItems), totalItems)
})
]
}), /* @__PURE__ */ jsxs("div", {
className: `${prefix}--pagination__right`,
children: [pagesUnknown ? /* @__PURE__ */ jsx("span", {
className: `${prefix}--pagination__text ${prefix}--pagination__page-text ${prefix}--pagination__unknown-pages-text`,
children: pageText(page)
}) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Select_default, {
id: `${prefix}-pagination-select-${inputId}-right`,
className: `${prefix}--select__page-number`,
labelText: pageSelectLabelText(totalPages),
inline: true,
hideLabel: true,
onChange: handlePageInputChange,
value: page,
disabled: pageInputDisabled || disabled,
children: selectItems
}, page), /* @__PURE__ */ jsx("span", {
className: `${prefix}--pagination__text`,
children: pageRangeText(page, totalPages)
})] }), /* @__PURE__ */ jsxs("div", {
className: `${prefix}--pagination__control-buttons`,
children: [/* @__PURE__ */ jsx(IconButton, {
align: "top",
disabled: backButtonDisabled,
kind: "ghost",
className: backButtonClasses,
label: backwardText,
"aria-label": backwardText,
onClick: decrementPage,
ref: backBtnRef,
children: /* @__PURE__ */ jsx(CaretLeft, {})
}), /* @__PURE__ */ jsx(IconButton, {
align: "top",
disabled: forwardButtonDisabled || isLastPage,
kind: "ghost",
className: forwardButtonClasses,
label: forwardText,
"aria-label": forwardText,
onClick: incrementPage,
ref: forwardBtnRef,
children: /* @__PURE__ */ jsx(CaretRight, {})
})]
})]
})]
});
});
Pagination.propTypes = {
backwardText: PropTypes.string,
className: PropTypes.string,
disabled: PropTypes.bool,
forwardText: PropTypes.string,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
isLastPage: PropTypes.bool,
itemRangeText: PropTypes.func,
itemText: PropTypes.func,
itemsPerPageText: PropTypes.string,
onChange: PropTypes.func,
page: PropTypes.number,
pageInputDisabled: PropTypes.bool,
pageNumberText: PropTypes.string,
pageRangeText: PropTypes.func,
pageSelectLabelText: PropTypes.func,
pageSize: PropTypes.number,
pageSizeInputDisabled: PropTypes.bool,
pageSizes: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number.isRequired), PropTypes.arrayOf(PropTypes.shape({
text: PropTypes.string.isRequired,
value: PropTypes.number.isRequired
}).isRequired)]).isRequired,
pageText: PropTypes.func,
pagesUnknown: PropTypes.bool,
size: PropTypes.oneOf([
"sm",
"md",
"lg"
]),
totalItems: PropTypes.number
};
//#endregion
export { Pagination as default };