- );
- }
-
- if (count === 1) {
- return (
- {
- onSelect(fromIndex);
- }}
- />
- );
- }
-
- return null;
-}
-
-const PaginationNav = React.forwardRef(function PaginationNav(
- {
- className,
- onChange = () => {},
- totalItems,
- disableOverflow,
- itemsShown = 10,
- page = 0,
- loop = false,
- translateWithId: t = translateWithId,
- ...rest
- },
- ref
-) {
- const [currentPage, setCurrentPage] = useState(page);
- const [itemsThatFit, setItemsThatFit] = useState(
- itemsShown >= 4 ? itemsShown : 4
- );
- const [cuts, setCuts] = useState(
- getCuts(currentPage, totalItems, itemsThatFit)
- );
- 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 = itemsThatFit <= 4 && page > 1 ? 0 : 1;
-
- const wouldBeHiddenInFront = page >= startOffset && page <= cuts.front;
- 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(() => {
- setItemsThatFit(itemsShown >= 4 ? itemsShown : 4);
- setCuts(getCuts(currentPage, totalItems, itemsShown));
- }, [totalItems, itemsShown]); // 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 = itemsThatFit - 3;
- setCuts(getCuts(currentPage, totalItems, itemsThatFit, splitPoint));
- } else {
- const splitPoint = itemsThatFit > 4 ? 2 : 1;
- setCuts(getCuts(currentPage, totalItems, itemsThatFit, splitPoint));
- }
- }
- }, [currentPage]); // eslint-disable-line react-hooks/exhaustive-deps
-
- useEffect(() => {
- setIsOverFlowDisabled(disableOverflow);
- }, [disableOverflow]);
-
- const classNames = classnames(`${prefix}--pagination-nav`, className);
-
- const backwardButtonDisabled = !loop && currentPage === 0;
- const forwardButtonDisabled = !loop && currentPage === totalItems - 1;
-
- const startOffset = itemsThatFit <= 4 && currentPage > 1 ? 0 : 1;
-
- return (
-
- );
-});
-
-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.
- */
- 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,
-
- /**
- * 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 default PaginationNav;
diff --git a/packages/react/src/components/PaginationNav/PaginationNav.tsx b/packages/react/src/components/PaginationNav/PaginationNav.tsx
new file mode 100644
index 000000000000..8daabb8f40cd
--- /dev/null
+++ b/packages/react/src/components/PaginationNav/PaginationNav.tsx
@@ -0,0 +1,650 @@
+/**
+ * Copyright IBM Corp. 2020
+ *
+ * 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 PropTypes from 'prop-types';
+import React, { useState, useEffect, useRef } from 'react';
+import classnames from 'classnames';
+import {
+ CaretRight,
+ CaretLeft,
+ OverflowMenuHorizontal,
+} from '@carbon/icons-react';
+import { IconButton } from '../IconButton';
+import { usePrefix } from '../../internal/usePrefix';
+
+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',
+};
+
+function translateWithId(messageId: string): string {
+ return translationIds[messageId];
+}
+
+// https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
+function usePrevious(value: number) {
+ const ref = useRef(null);
+
+ useEffect(() => {
+ ref.current = value;
+ });
+
+ return ref.current;
+}
+
+function calculateCuts(
+ page: number,
+ totalItems: number,
+ itemsDisplayedOnPage: number,
+ splitPoint: number | null = 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,
+ };
+}
+
+interface DirectionButtonProps {
+ /**
+ * The direction this button represents ("forward" or "backward").
+ */
+ direction?: 'forward' | 'backward';
+
+ /**
+ * Whether or not the button should be disabled.
+ */
+ disabled?: boolean;
+
+ /**
+ * The label shown in the button's tooltip.
+ */
+ label?: string;
+
+ /**
+ * The callback function called when the button is clicked.
+ */
+ onClick?: React.MouseEventHandler;
+}
+
+function DirectionButton({
+ direction,
+ label,
+ disabled,
+ onClick,
+}: DirectionButtonProps) {
+ const prefix = usePrefix();
+
+ return (
+
+
+ {direction === 'forward' ? : }
+
+
+ );
+}
+
+interface PaginationItemProps {
+ /**
+ * Whether or not this is the currently active page.
+ */
+ isActive?: boolean;
+
+ /**
+ * The callback function called when the item is clicked.
+ */
+ onClick?: React.MouseEventHandler;
+
+ /**
+ * The page number this item represents.
+ */
+ page?: number;
+
+ /**
+ * Specify a custom translation function that takes in a message identifier
+ * and returns the localized string for the message
+ */
+ translateWithId?: (id: string) => string;
+}
+
+function PaginationItem({
+ page,
+ isActive,
+ onClick,
+ translateWithId: t = translateWithId,
+}: PaginationItemProps) {
+ const prefix = usePrefix();
+ const itemLabel = t('carbon.pagination-nav.item');
+
+ return (
+
+
+
+ );
+}
+
+interface PaginationOverflowProps {
+ /**
+ * How many items to display in this overflow.
+ */
+ count?: number;
+
+ /**
+ * From which index on this overflow should start displaying pages.
+ */
+ fromIndex?: number;
+
+ /**
+ * The callback function called when the user selects a page from the overflow.
+ */
+ onSelect?: (id: number) => void;
+
+ /**
+ * 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?: boolean;
+
+ /**
+ * Specify a custom translation function that takes in a message identifier
+ * and returns the localized string for the message
+ */
+ translateWithId?: (id: string) => string;
+}
+
+function PaginationOverflow({
+ fromIndex = NaN,
+ count = NaN,
+ onSelect,
+ // eslint-disable-next-line react/prop-types
+ disableOverflow,
+ translateWithId: t = translateWithId,
+}: PaginationOverflowProps) {
+ const prefix = usePrefix();
+
+ //If overflow is disabled, return a select tag with no select options
+ if (disableOverflow === true && count > 1) {
+ return (
+