UNPKG

@carbon/react

Version:

React components for the Carbon Design System

433 lines (420 loc) 14.4 kB
/** * Copyright IBM Corp. 2016, 2023 * * 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 { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js'; import PropTypes from 'prop-types'; import React, { useState, useEffect, useRef } from 'react'; import cx from 'classnames'; import { CaretRight, CaretLeft, OverflowMenuHorizontal } from '@carbon/icons-react'; import { IconButton } from '../IconButton/index.js'; import { usePrefix } from '../../internal/usePrefix.js'; import { breakpoints } from '@carbon/layout'; import { useMatchMedia } from '../../internal/useMatchMedia.js'; import { clamp } from '../../internal/clamp.js'; var _CaretRight, _CaretLeft, _option; const translationIds = { 'carbon.pagination-nav.next': 'Next', 'carbon.pagination-nav.previous': 'Previous', 'carbon.pagination-nav.item': 'Page', 'carbon.pagination-nav.active': 'Active', 'carbon.pagination-nav.of': 'of' }; /** * Message ids that will be passed to translateWithId(). */ function translateWithId(messageId) { return translationIds[messageId]; } // https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state 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 }) { const prefix = usePrefix(); return /*#__PURE__*/React.createElement("li", { className: `${prefix}--pagination-nav__list-item` }, /*#__PURE__*/React.createElement(IconButton, { align: "bottom", disabled: disabled, kind: "ghost", label: label, onClick: onClick }, direction === 'forward' ? _CaretRight || (_CaretRight = /*#__PURE__*/React.createElement(CaretRight, null)) : _CaretLeft || (_CaretLeft = /*#__PURE__*/React.createElement(CaretLeft, null)))); } function PaginationItem({ page, isActive, onClick, translateWithId: t = translateWithId }) { const prefix = usePrefix(); const itemLabel = t('carbon.pagination-nav.item'); return /*#__PURE__*/React.createElement("li", { className: `${prefix}--pagination-nav__list-item` }, /*#__PURE__*/React.createElement("button", { type: "button", className: cx(`${prefix}--pagination-nav__page`, { [`${prefix}--pagination-nav__page--active`]: isActive }), onClick: onClick, "data-page": page, "aria-current": isActive ? 'page' : undefined }, /*#__PURE__*/React.createElement("span", { className: `${prefix}--pagination-nav__accessibility-label` }, isActive ? `${t('carbon.pagination-nav.active')}, ${itemLabel}` : itemLabel), page)); } function PaginationOverflow({ fromIndex = NaN, count = NaN, onSelect, // eslint-disable-next-line react/prop-types disableOverflow, translateWithId: t = translateWithId }) { const prefix = usePrefix(); //If overflow is disabled, return a select tag with no select options if (disableOverflow === true && count > 1) { return /*#__PURE__*/React.createElement("li", { className: `${prefix}--pagination-nav__list-item` }, /*#__PURE__*/React.createElement("div", { className: `${prefix}--pagination-nav__select` }, /*#__PURE__*/React.createElement("select", { className: `${prefix}--pagination-nav__page ${prefix}--pagination-nav__page--select`, "aria-label": `Select ${t('carbon.pagination-nav.item')} number`, disabled: true }), /*#__PURE__*/React.createElement("div", { className: `${prefix}--pagination-nav__select-icon-wrapper` }, /*#__PURE__*/React.createElement(OverflowMenuHorizontal, { className: `${prefix}--pagination-nav__select-icon` })))); } if (count > 1) { return /*#__PURE__*/React.createElement("li", { className: `${prefix}--pagination-nav__list-item` }, /*#__PURE__*/React.createElement("div", { className: `${prefix}--pagination-nav__select` }, /*#__PURE__*/React.createElement("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); } }, _option || (_option = /*#__PURE__*/React.createElement("option", { value: "", hidden: true })), [...Array(count)].map((e, i) => /*#__PURE__*/React.createElement("option", { value: (fromIndex + i).toString(), "data-page": fromIndex + i + 1, key: `overflow-${fromIndex + i}` }, fromIndex + i + 1))), /*#__PURE__*/React.createElement("div", { className: `${prefix}--pagination-nav__select-icon-wrapper` }, /*#__PURE__*/React.createElement(OverflowMenuHorizontal, { className: `${prefix}--pagination-nav__select-icon` })))); } if (count === 1) { return /*#__PURE__*/React.createElement(PaginationItem, { page: fromIndex + 1, translateWithId: t, onClick: () => { onSelect?.(fromIndex); } }); } return null; } const PaginationNav = /*#__PURE__*/React.forwardRef(function PaginationNav({ className, onChange = () => {}, totalItems = NaN, disableOverflow, itemsShown = 10, page = 0, loop = false, size = 'lg', translateWithId: t = translateWithId, ...rest }, ref) { const smMediaQuery = `(max-width: ${breakpoints.sm.width})`; const isSm = useMatchMedia(smMediaQuery); 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 startOffset = itemsDisplayedOnPage <= 4 && page > 1 ? 0 : 1; const wouldBeHiddenInFront = page >= startOffset && page <= cuts.front || page === 0; const wouldBeHiddenInBack = page >= totalItems - cuts.back - 1 && page <= totalItems - 2; return wouldBeHiddenInFront || wouldBeHiddenInBack; } // jump to new page if props.page is updated useEffect(() => { setCurrentPage(page); }, [page]); // re-calculate cuts if props.totalItems or props.itemsShown change 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]); // eslint-disable-line react-hooks/exhaustive-deps // update cuts if necessary whenever currentPage changes useEffect(() => { if (pageWouldBeHidden(currentPage)) { const delta = currentPage - (prevPage || 0); if (delta > 0) { const splitPoint = itemsDisplayedOnPage - 3; setCuts(calculateCuts(currentPage, totalItems, itemsDisplayedOnPage, splitPoint)); } else { const splitPoint = itemsDisplayedOnPage > 4 ? 2 : 1; setCuts(calculateCuts(currentPage, totalItems, itemsDisplayedOnPage, splitPoint)); } } }, [currentPage]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { setIsOverFlowDisabled(disableOverflow); }, [disableOverflow]); const classNames = cx(`${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__*/React.createElement("nav", _extends({ className: classNames, ref: ref }, rest, { "aria-label": "pagination" }), /*#__PURE__*/React.createElement("ul", { className: `${prefix}--pagination-nav__list` }, /*#__PURE__*/React.createElement(DirectionButton, { direction: "backward", "aria-label": t('carbon.pagination-nav.previous'), label: t('carbon.pagination-nav.previous'), disabled: backwardButtonDisabled, onClick: jumpToPrevious }), // render first item if at least 5 items can be displayed or // 4 items can be displayed and the current page is either 0 or 1 (itemsDisplayedOnPage >= 5 || itemsDisplayedOnPage <= 4 && currentPage <= 1) && /*#__PURE__*/React.createElement(PaginationItem, { page: 1, translateWithId: t, isActive: currentPage === 0, onClick: () => { jumpToItem(0); } }), /*#__PURE__*/React.createElement(PaginationOverflow, { fromIndex: startOffset, count: cuts.front, onSelect: jumpToItem, disableOverflow: isOverflowDisabled }), // render items between overflows [...Array(totalItems)].map((e, i) => i).slice(startOffset + cuts.front, (1 + cuts.back) * -1).map(item => /*#__PURE__*/React.createElement(PaginationItem, { key: `item-${item}`, page: item + 1, translateWithId: t, isActive: currentPage === item, onClick: () => { jumpToItem(item); } })), /*#__PURE__*/React.createElement(PaginationOverflow, { fromIndex: totalItems - cuts.back - 1, count: cuts.back, onSelect: jumpToItem, disableOverflow: isOverflowDisabled }), // render last item unless there is only one in total totalItems > 1 && /*#__PURE__*/React.createElement(PaginationItem, { page: totalItems, translateWithId: t, isActive: currentPage === totalItems - 1, onClick: () => { jumpToItem(totalItems - 1); } }), /*#__PURE__*/React.createElement(DirectionButton, { direction: "forward", "aria-label": t('carbon.pagination-nav.next'), label: t('carbon.pagination-nav.next'), disabled: forwardButtonDisabled, onClick: jumpToNext })), /*#__PURE__*/React.createElement("div", { "aria-live": "polite", "aria-atomic": "true", className: `${prefix}--pagination-nav__accessibility-label` }, `${t('carbon.pagination-nav.item')} ${currentPage + 1} ${t('carbon.pagination-nav.of')} ${totalItems}`)); }); DirectionButton.propTypes = { /** * The direction this button represents ("forward" or "backward"). */ direction: PropTypes.oneOf(['forward', 'backward']), /** * Whether or not the button should be disabled. */ disabled: PropTypes.bool, /** * The label shown in the button's tooltip. */ label: PropTypes.string, /** * The callback function called when the button is clicked. */ onClick: PropTypes.func }; PaginationItem.propTypes = { /** * Whether or not this is the currently active page. */ isActive: PropTypes.bool, /** * The callback function called when the item is clicked. */ onClick: PropTypes.func, /** * The page number this item represents. */ page: PropTypes.number, /** * Specify a custom translation function that takes in a message identifier * and returns the localized string for the message */ translateWithId: PropTypes.func }; PaginationOverflow.propTypes = { /** * How many items to display in this overflow. */ count: PropTypes.number, /** * From which index on this overflow should start displaying pages. */ fromIndex: PropTypes.number, /** * The callback function called when the user selects a page from the overflow. */ onSelect: PropTypes.func, /** * Specify a custom translation function that takes in a message identifier * and returns the localized string for the message */ translateWithId: PropTypes.func }; PaginationNav.displayName = 'PaginationNav'; PaginationNav.propTypes = { /** * Additional CSS class names. */ className: PropTypes.string, /** * If true, the '...' pagination overflow will not render page links between the first and last rendered buttons. * Set this to true if you are having performance problems with large data sets. */ disableOverflow: PropTypes.bool, // eslint-disable-line react/prop-types /** * The number of items to be shown (minimum of 4 unless props.items < 4). */ itemsShown: PropTypes.number, /** * Whether user should be able to loop through the items when reaching first / last. */ loop: PropTypes.bool, /** * The callback function called when the current page changes. */ onChange: PropTypes.func, /** * The index of current page. */ page: PropTypes.number, /** * Specify the size of the PaginationNav. */ size: PropTypes.oneOf(['sm', 'md', 'lg']), /** * The total number of items. */ totalItems: PropTypes.number, /** * Specify a custom translation function that takes in a message identifier * and returns the localized string for the message */ translateWithId: PropTypes.func }; export { PaginationNav as default };