UNPKG

@carbon/react

Version:

React components for the Carbon Design System

443 lines (426 loc) 16 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. */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js'); var PropTypes = require('prop-types'); var React = require('react'); var cx = require('classnames'); var iconsReact = require('@carbon/icons-react'); var index = require('../IconButton/index.js'); var usePrefix = require('../../internal/usePrefix.js'); var layout = require('@carbon/layout'); var useMatchMedia = require('../../internal/useMatchMedia.js'); var clamp = require('../../internal/clamp.js'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes); var React__default = /*#__PURE__*/_interopDefaultLegacy(React); var cx__default = /*#__PURE__*/_interopDefaultLegacy(cx); 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 = React.useRef(null); 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 }) { const prefix = usePrefix.usePrefix(); return /*#__PURE__*/React__default["default"].createElement("li", { className: `${prefix}--pagination-nav__list-item` }, /*#__PURE__*/React__default["default"].createElement(index.IconButton, { align: "bottom", disabled: disabled, kind: "ghost", label: label, onClick: onClick }, direction === 'forward' ? _CaretRight || (_CaretRight = /*#__PURE__*/React__default["default"].createElement(iconsReact.CaretRight, null)) : _CaretLeft || (_CaretLeft = /*#__PURE__*/React__default["default"].createElement(iconsReact.CaretLeft, null)))); } function PaginationItem({ page, isActive, onClick, translateWithId: t = translateWithId }) { const prefix = usePrefix.usePrefix(); const itemLabel = t('carbon.pagination-nav.item'); return /*#__PURE__*/React__default["default"].createElement("li", { className: `${prefix}--pagination-nav__list-item` }, /*#__PURE__*/React__default["default"].createElement("button", { type: "button", className: cx__default["default"](`${prefix}--pagination-nav__page`, { [`${prefix}--pagination-nav__page--active`]: isActive }), onClick: onClick, "data-page": page, "aria-current": isActive ? 'page' : undefined }, /*#__PURE__*/React__default["default"].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.usePrefix(); //If overflow is disabled, return a select tag with no select options if (disableOverflow === true && count > 1) { return /*#__PURE__*/React__default["default"].createElement("li", { className: `${prefix}--pagination-nav__list-item` }, /*#__PURE__*/React__default["default"].createElement("div", { className: `${prefix}--pagination-nav__select` }, /*#__PURE__*/React__default["default"].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__default["default"].createElement("div", { className: `${prefix}--pagination-nav__select-icon-wrapper` }, /*#__PURE__*/React__default["default"].createElement(iconsReact.OverflowMenuHorizontal, { className: `${prefix}--pagination-nav__select-icon` })))); } if (count > 1) { return /*#__PURE__*/React__default["default"].createElement("li", { className: `${prefix}--pagination-nav__list-item` }, /*#__PURE__*/React__default["default"].createElement("div", { className: `${prefix}--pagination-nav__select` }, /*#__PURE__*/React__default["default"].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__default["default"].createElement("option", { value: "", hidden: true })), [...Array(count)].map((e, i) => /*#__PURE__*/React__default["default"].createElement("option", { value: (fromIndex + i).toString(), "data-page": fromIndex + i + 1, key: `overflow-${fromIndex + i}` }, fromIndex + i + 1))), /*#__PURE__*/React__default["default"].createElement("div", { className: `${prefix}--pagination-nav__select-icon-wrapper` }, /*#__PURE__*/React__default["default"].createElement(iconsReact.OverflowMenuHorizontal, { className: `${prefix}--pagination-nav__select-icon` })))); } if (count === 1) { return /*#__PURE__*/React__default["default"].createElement(PaginationItem, { page: fromIndex + 1, translateWithId: t, onClick: () => { onSelect?.(fromIndex); } }); } return null; } const PaginationNav = /*#__PURE__*/React__default["default"].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: ${layout.breakpoints.sm.width})`; const isSm = useMatchMedia.useMatchMedia(smMediaQuery); let numberOfPages; switch (size) { case 'md': numberOfPages = itemsShown === 4 ? itemsShown : 5; break; case 'sm': numberOfPages = clamp.clamp(itemsShown, 4, 7); break; default: numberOfPages = 4; break; } const [currentPage, setCurrentPage] = React.useState(page); const [itemsDisplayedOnPage, setItemsDisplayedOnPage] = React.useState(itemsShown >= 4 && !isSm ? itemsShown : numberOfPages); const [cuts, setCuts] = React.useState(calculateCuts(currentPage, totalItems, itemsDisplayedOnPage)); const prevPage = usePrevious(currentPage); const prefix = usePrefix.usePrefix(); const [isOverflowDisabled, setIsOverFlowDisabled] = 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 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 React.useEffect(() => { setCurrentPage(page); }, [page]); // re-calculate cuts if props.totalItems or props.itemsShown change 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]); // eslint-disable-line react-hooks/exhaustive-deps // update cuts if necessary whenever currentPage changes React.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 React.useEffect(() => { setIsOverFlowDisabled(disableOverflow); }, [disableOverflow]); const classNames = cx__default["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__*/React__default["default"].createElement("nav", _rollupPluginBabelHelpers["extends"]({ className: classNames, ref: ref }, rest, { "aria-label": "pagination" }), /*#__PURE__*/React__default["default"].createElement("ul", { className: `${prefix}--pagination-nav__list` }, /*#__PURE__*/React__default["default"].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__default["default"].createElement(PaginationItem, { page: 1, translateWithId: t, isActive: currentPage === 0, onClick: () => { jumpToItem(0); } }), /*#__PURE__*/React__default["default"].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__default["default"].createElement(PaginationItem, { key: `item-${item}`, page: item + 1, translateWithId: t, isActive: currentPage === item, onClick: () => { jumpToItem(item); } })), /*#__PURE__*/React__default["default"].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__default["default"].createElement(PaginationItem, { page: totalItems, translateWithId: t, isActive: currentPage === totalItems - 1, onClick: () => { jumpToItem(totalItems - 1); } }), /*#__PURE__*/React__default["default"].createElement(DirectionButton, { direction: "forward", "aria-label": t('carbon.pagination-nav.next'), label: t('carbon.pagination-nav.next'), disabled: forwardButtonDisabled, onClick: jumpToNext })), /*#__PURE__*/React__default["default"].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__default["default"].oneOf(['forward', 'backward']), /** * Whether or not the button should be disabled. */ disabled: PropTypes__default["default"].bool, /** * The label shown in the button's tooltip. */ label: PropTypes__default["default"].string, /** * The callback function called when the button is clicked. */ onClick: PropTypes__default["default"].func }; PaginationItem.propTypes = { /** * Whether or not this is the currently active page. */ isActive: PropTypes__default["default"].bool, /** * The callback function called when the item is clicked. */ onClick: PropTypes__default["default"].func, /** * The page number this item represents. */ page: PropTypes__default["default"].number, /** * Specify a custom translation function that takes in a message identifier * and returns the localized string for the message */ translateWithId: PropTypes__default["default"].func }; PaginationOverflow.propTypes = { /** * How many items to display in this overflow. */ count: PropTypes__default["default"].number, /** * From which index on this overflow should start displaying pages. */ fromIndex: PropTypes__default["default"].number, /** * The callback function called when the user selects a page from the overflow. */ onSelect: PropTypes__default["default"].func, /** * Specify a custom translation function that takes in a message identifier * and returns the localized string for the message */ translateWithId: PropTypes__default["default"].func }; PaginationNav.displayName = 'PaginationNav'; PaginationNav.propTypes = { /** * Additional CSS class names. */ className: PropTypes__default["default"].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__default["default"].bool, // eslint-disable-line react/prop-types /** * The number of items to be shown (minimum of 4 unless props.items < 4). */ itemsShown: PropTypes__default["default"].number, /** * Whether user should be able to loop through the items when reaching first / last. */ loop: PropTypes__default["default"].bool, /** * The callback function called when the current page changes. */ onChange: PropTypes__default["default"].func, /** * The index of current page. */ page: PropTypes__default["default"].number, /** * Specify the size of the PaginationNav. */ size: PropTypes__default["default"].oneOf(['sm', 'md', 'lg']), /** * The total number of items. */ totalItems: PropTypes__default["default"].number, /** * Specify a custom translation function that takes in a message identifier * and returns the localized string for the message */ translateWithId: PropTypes__default["default"].func }; exports["default"] = PaginationNav;