From 81b192dfecf189d5ebc08ab63322bd8fc769feac Mon Sep 17 00:00:00 2001 From: unix Date: Tue, 9 Jun 2020 08:37:56 +0800 Subject: [PATCH] feat(pagination): add component --- components/index.ts | 1 + components/pagination/index.ts | 8 + components/pagination/pagination-context.ts | 18 +++ components/pagination/pagination-ellipsis.tsx | 60 ++++++++ components/pagination/pagination-item.tsx | 101 +++++++++++++ components/pagination/pagination-next.tsx | 19 +++ components/pagination/pagination-pages.tsx | 104 +++++++++++++ components/pagination/pagination-previous.tsx | 19 +++ components/pagination/pagination.tsx | 142 ++++++++++++++++++ 9 files changed, 472 insertions(+) create mode 100644 components/pagination/index.ts create mode 100644 components/pagination/pagination-context.ts create mode 100644 components/pagination/pagination-ellipsis.tsx create mode 100644 components/pagination/pagination-item.tsx create mode 100644 components/pagination/pagination-next.tsx create mode 100644 components/pagination/pagination-pages.tsx create mode 100644 components/pagination/pagination-previous.tsx create mode 100644 components/pagination/pagination.tsx diff --git a/components/index.ts b/components/index.ts index 647a4b8..517e943 100644 --- a/components/index.ts +++ b/components/index.ts @@ -59,3 +59,4 @@ export { default as Page } from './page' export { default as Grid } from './grid' export { default as ButtonGroup } from './button-group' export { default as Breadcrumbs } from './breadcrumbs' +export { default as Pagination } from './pagination' diff --git a/components/pagination/index.ts b/components/pagination/index.ts new file mode 100644 index 0000000..9a60112 --- /dev/null +++ b/components/pagination/index.ts @@ -0,0 +1,8 @@ +import Pagination from './pagination' +import PaginationPrevious from './pagination-previous' +import PaginationNext from './pagination-next' + +Pagination.Previous = PaginationPrevious +Pagination.Next = PaginationNext + +export default Pagination diff --git a/components/pagination/pagination-context.ts b/components/pagination/pagination-context.ts new file mode 100644 index 0000000..8342ec7 --- /dev/null +++ b/components/pagination/pagination-context.ts @@ -0,0 +1,18 @@ +import React from 'react' +import { tuple } from '../utils/prop-types' +const paginationUpdateTypes = tuple('prev', 'next', 'click') + +export type PaginationUpdateType = typeof paginationUpdateTypes[number] + +export interface PaginationConfig { + isFirst?: boolean + isLast?: boolean + update?: (type: PaginationUpdateType) => void +} + +const defaultContext = {} + +export const PaginationContext = React.createContext(defaultContext) + +export const usePaginationContext = (): PaginationConfig => + React.useContext(PaginationContext) diff --git a/components/pagination/pagination-ellipsis.tsx b/components/pagination/pagination-ellipsis.tsx new file mode 100644 index 0000000..e4d759d --- /dev/null +++ b/components/pagination/pagination-ellipsis.tsx @@ -0,0 +1,60 @@ +import React, { useState } from 'react' +import PaginationItem from './pagination-item' + +interface Props { + isBefore?: boolean + onClick?: (e: React.MouseEvent) => void +} + +const PaginationEllipsis: React.FC = ({ isBefore, onClick }) => { + const [showMore, setShowMore] = useState(false) + + return ( + onClick && onClick(e)} + onMouseEnter={() => setShowMore(true)} + onMouseLeave={() => setShowMore(false)}> + {showMore ? ( + + + + + ) : ( + + + + + + )} + + + + ) +} + +export default PaginationEllipsis diff --git a/components/pagination/pagination-item.tsx b/components/pagination/pagination-item.tsx new file mode 100644 index 0000000..e4b7283 --- /dev/null +++ b/components/pagination/pagination-item.tsx @@ -0,0 +1,101 @@ +import React, { useMemo } from 'react' +import useTheme from '../styles/use-theme' +import { addColorAlpha } from 'components/utils/color' + +interface Props { + active?: boolean + disabled?: boolean + onClick?: (e: React.MouseEvent) => void +} + +type NativeAttrs = Omit, keyof Props> +export type PaginationItemProps = Props & NativeAttrs + +const PaginationItem: React.FC> = ({ + active, + children, + disabled, + onClick, + ...props +}) => { + const theme = useTheme() + const [hover, activeHover] = useMemo( + () => [addColorAlpha(theme.palette.success, 0.1), addColorAlpha(theme.palette.success, 0.8)], + [theme.palette.success], + ) + const clickHandler = (event: React.MouseEvent) => { + if (disabled) return + onClick && onClick(event) + } + + return ( +
  • + + +
  • + ) +} + +export default PaginationItem diff --git a/components/pagination/pagination-next.tsx b/components/pagination/pagination-next.tsx new file mode 100644 index 0000000..d01f893 --- /dev/null +++ b/components/pagination/pagination-next.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import PaginationItem from './pagination-item' +import { usePaginationContext } from './pagination-context' + +export type PaginationNextProps = React.ButtonHTMLAttributes + +const PaginationNext: React.FC> = ({ + children, + ...props +}) => { + const { update, isLast } = usePaginationContext() + return ( + update && update('next')} disabled={isLast} {...props}> + {children} + + ) +} + +export default PaginationNext diff --git a/components/pagination/pagination-pages.tsx b/components/pagination/pagination-pages.tsx new file mode 100644 index 0000000..50a24a1 --- /dev/null +++ b/components/pagination/pagination-pages.tsx @@ -0,0 +1,104 @@ +import React, { Dispatch, SetStateAction, useCallback, useMemo } from 'react' +import PaginationItem from './pagination-item' +import PaginationEllipsis from 'components/pagination/pagination-ellipsis' + +interface Props { + limit: number + count: number + current: number + setPage: Dispatch> +} + +const PaginationPages: React.FC = ({ limit, count, current, setPage }) => { + const showPages = useMemo(() => { + const oddLimit = limit % 2 === 0 ? limit - 1 : limit + return oddLimit - 2 + }, [limit]) + const middleNumber = (showPages + 1) / 2 + + const [showBeforeEllipsis, showAfterEllipsis] = useMemo(() => { + const showEllipsis = count > limit + return [ + showEllipsis && current > middleNumber + 1, + showEllipsis && current < count - middleNumber, + ] + }, [current, showPages, middleNumber, count, limit]) + const pagesArray = useMemo(() => [...new Array(showPages)], [showPages]) + + const renderItem = useCallback( + (value: number, active: number) => ( + setPage(value)}> + {value} + + ), + [], + ) + const startPages = pagesArray.map((_, index) => { + const value = index + 2 + return renderItem(value, current) + }) + const middlePages = pagesArray.map((_, index) => { + const middleIndexNumber = middleNumber - (index + 1) + const value = current - middleIndexNumber + return ( + setPage(value)}> + {value} + + ) + }) + const endPages = pagesArray.map((_, index) => { + const value = count - (showPages - index) + return renderItem(value, current) + }) + if (count <= limit) { + /* eslint-disable react/jsx-no-useless-fragment */ + return ( + <> + {[...new Array(count)].map((_, index) => { + const value = index + 1 + return ( + setPage(value)}> + {value} + + ) + })} + + ) + /* eslint-enable */ + } + return ( + <> + {renderItem(1, current)} + {showBeforeEllipsis && ( + setPage(last => (last - 5 >= 1 ? last - 5 : 1))} + /> + )} + {showBeforeEllipsis && showAfterEllipsis + ? middlePages + : showBeforeEllipsis + ? endPages + : startPages} + {showAfterEllipsis && ( + setPage(last => (last + 5 <= count ? last + 5 : count))} + /> + )} + {renderItem(count, current)} + + ) +} + +export default PaginationPages diff --git a/components/pagination/pagination-previous.tsx b/components/pagination/pagination-previous.tsx new file mode 100644 index 0000000..79b2bf9 --- /dev/null +++ b/components/pagination/pagination-previous.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import PaginationItem from './pagination-item' +import { usePaginationContext } from './pagination-context' + +export type PaginationNextProps = React.ButtonHTMLAttributes + +const PaginationPrevious: React.FC> = ({ + children, + ...props +}) => { + const { update, isFirst } = usePaginationContext() + return ( + update && update('prev')} disabled={isFirst} {...props}> + {children} + + ) +} + +export default PaginationPrevious diff --git a/components/pagination/pagination.tsx b/components/pagination/pagination.tsx new file mode 100644 index 0000000..8072785 --- /dev/null +++ b/components/pagination/pagination.tsx @@ -0,0 +1,142 @@ +import React, { useEffect, useMemo } from 'react' +import PaginationPrevious from './pagination-previous' +import PaginationNext from './pagination-next' +import PaginationPages from './pagination-pages' +import { PaginationContext, PaginationConfig, PaginationUpdateType } from './pagination-context' +import useCurrentState from '../utils/use-current-state' +import { pickChild } from '../utils/collections' +import { NormalSizes } from '../utils/prop-types' + +interface Props { + size?: NormalSizes + page?: number + initialPage?: number + count?: number + limit?: number + onChange?: (val: number) => void +} + +const defaultProps = { + size: 'medium' as NormalSizes, + initialPage: 1, + count: 1, + limit: 7, +} + +type NativeAttrs = Omit, keyof Props> +export type PaginationProps = Props & typeof defaultProps & NativeAttrs + +type PaginationSize = { + font: string + width: string +} + +const getPaginationSizes = (size: NormalSizes) => { + const sizes: { [key in NormalSizes]: PaginationSize } = { + mini: { + font: '.75rem', + width: '1.25rem', + }, + small: { + font: '.75rem', + width: '1.65rem', + }, + medium: { + font: '.875rem', + width: '2rem', + }, + large: { + font: '1rem', + width: '2.4rem', + }, + } + return sizes[size] +} + +const Pagination: React.FC> = ({ + page: customPage, + initialPage, + count, + limit, + size, + children, + onChange, +}) => { + const [page, setPage, pageRef] = useCurrentState(initialPage) + const [, prevChildren] = pickChild(children, PaginationPrevious) + const [, nextChildren] = pickChild(children, PaginationNext) + + const [prevItem, nextItem] = useMemo(() => { + const hasChildren = (c: any) => React.Children.count(c) > 0 + const prevDefault = prev + const nextDefault = next + return [ + hasChildren(prevChildren) ? prevChildren : prevDefault, + hasChildren(nextChildren) ? nextChildren : nextDefault, + ] + }, [prevChildren, nextChildren]) + const { font, width } = useMemo(() => getPaginationSizes(size), [size]) + + const update = (type: PaginationUpdateType) => { + if (type === 'prev' && pageRef.current > 1) { + setPage(last => last - 1) + } + if (type === 'next' && pageRef.current < count) { + setPage(last => last + 1) + } + } + const values = useMemo( + () => ({ + isFirst: page <= 1, + isLast: page >= count, + update, + }), + [page], + ) + + useEffect(() => { + onChange && onChange(page) + }, [page]) + useEffect(() => { + if (customPage !== undefined) { + setPage(customPage) + } + }, [customPage]) + + return ( + + + + + ) +} + +type MemoPaginationComponent

    = React.NamedExoticComponent

    & { + Previous: typeof PaginationPrevious + Next: typeof PaginationNext +} + +type ComponentProps = Partial & + Omit & + NativeAttrs + +Pagination.defaultProps = defaultProps + +export default React.memo(Pagination) as MemoPaginationComponent