UNPKG

@utahdts/utah-design-system

Version:
137 lines (129 loc) 5.36 kB
import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useImmer } from 'use-immer'; import { useAriaMessaging } from '../../../contexts/UtahDesignSystemContext/hooks/useAriaMessaging'; import { ICON_BUTTON_APPEARANCE } from '../../../enums/buttonEnums'; import { useRefAlways } from '../../../hooks/useRefAlways'; import { joinClassNames } from '../../../util/joinClassNames'; import { IconButton } from '../../buttons/IconButton'; import { PaginationLink } from './PaginationLink'; import { determinePaginationLinks } from './util/determinePaginationLinks'; /** * @param {object} props * @param {string} [props.ariaLabel] ariaLabel is used by accessibility to describe the purpose of the pagination * @param {string} [props.className] can add your own className to the pagination * @param {number} [props.defaultValue] starting page number (for uncontrolled - what good is an uncontrolled Pagination element?) * @param {string} [props.id] id to put on the pagination element * @param {import('react').RefObject<HTMLElement | null>} [props.innerRef] * @param {(newValue: number) => void} [props.onChange] controlled component: page # changed * @param {number} props.itemsPerPage how many items on each "page" * @param {number} props.totalNumberItems how many total items there are in the full data set * @param {number} [props.value] controlled component: value is the current page number (0 based-index) * @param {'div' | 'nav'} [props.wrapInElement] if wrapping in `nav`, make sure to provide the ariaLabel * @returns {import('react').JSX.Element} */ export function Pagination({ ariaLabel, className, defaultValue = 0, id, innerRef, onChange, itemsPerPage, totalNumberItems, value = 0, wrapInElement = 'div', ...rest }) { const [currentValue, setCurrentValue] = useImmer(value === undefined ? defaultValue : value); const valueUse = value === undefined ? currentValue : value; const valueRef = useRefAlways(valueUse ?? 0); const { onChange: currentOnChange, value: currentPageIndex, valueRef: currentPageIndexRef, } = useMemo( () => ({ onChange: onChange || setCurrentValue, value: valueUse ?? 0, valueRef, }), [onChange, valueUse] ); const { addPoliteMessage } = useAriaMessaging(); const numberOfPages = Math.ceil(totalNumberItems / itemsPerPage); const oldIndex = useRef(currentPageIndex); // check if current page is out of range... useEffect( () => { // if 0 records then 0 pages and 0 value is OK (0-based page index) if ((numberOfPages || currentPageIndex) && (currentPageIndex ?? NaN) >= numberOfPages) { currentOnChange(numberOfPages - 1); } if (currentPageIndex !== oldIndex.current) { oldIndex.current = currentPageIndex; addPoliteMessage(`You are now on page ${(currentPageIndex || 0) + 1}`); } }, [currentOnChange, currentPageIndex, numberOfPages] ); const paginationLinks = useMemo( () => determinePaginationLinks({ currentPageIndex, numberOfPages }), [currentPageIndex, numberOfPages] ); const WrapInElement = wrapInElement || 'div'; return ( // eslint-disable-next-line react/jsx-props-no-spreading // @ts-expect-error // eslint-disable-next-line react/jsx-props-no-spreading <WrapInElement ref={innerRef} className={joinClassNames('pagination', className)} id={id} aria-label={ariaLabel} {...rest}> <ul> <li> <IconButton appearance={ICON_BUTTON_APPEARANCE.BORDERLESS} className="pagination__prev" icon={<span className="utds-icon-before-arrow-left" aria-hidden="true" />} isDisabled={currentPageIndex === 0} onClick={useCallback(() => currentOnChange(currentPageIndexRef.current - 1), [currentOnChange, currentPageIndexRef])} title="Previous page" /> </li> { paginationLinks.map((paginationLink, paginationLinkIndex) => ( paginationLink.isEllipsis ? ( <li key={`pagination-link__ellipsis__${paginationLinkIndex}`}> <span className="pagination__ellipsis" > <span className="utds-icon-before-more-horizontal" aria-hidden="true" /> </span> </li> ) : ( <PaginationLink key={`pagination-link__${paginationLink.pageIndex}__${paginationLink.title}`} currentPageIndex={currentPageIndex} label={paginationLink.label ?? ''} onChange={currentOnChange} pageIndex={paginationLink.pageIndex} numberOfPages={numberOfPages} /> ) )) } <li> <IconButton appearance={ICON_BUTTON_APPEARANCE.BORDERLESS} className="pagination__next" icon={<span className="utds-icon-before-arrow-right" aria-hidden="true" />} isDisabled={currentPageIndex === numberOfPages - 1} onClick={useCallback(() => currentOnChange(currentPageIndexRef.current + 1), [currentOnChange, currentPageIndexRef])} title="Next page" /> </li> </ul> </WrapInElement> ); }