UNPKG

@carbon/react

Version:

React components for the Carbon Design System

348 lines (346 loc) 12.1 kB
/** * 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 };