mirror of
https://github.com/zhigang1992/react.git
synced 2026-02-11 22:32:32 +08:00
feat(pagination): add component
This commit is contained in:
@@ -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'
|
||||
|
||||
8
components/pagination/index.ts
Normal file
8
components/pagination/index.ts
Normal file
@@ -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
|
||||
18
components/pagination/pagination-context.ts
Normal file
18
components/pagination/pagination-context.ts
Normal file
@@ -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<PaginationConfig>(defaultContext)
|
||||
|
||||
export const usePaginationContext = (): PaginationConfig =>
|
||||
React.useContext<PaginationConfig>(PaginationContext)
|
||||
60
components/pagination/pagination-ellipsis.tsx
Normal file
60
components/pagination/pagination-ellipsis.tsx
Normal file
@@ -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<Props> = ({ isBefore, onClick }) => {
|
||||
const [showMore, setShowMore] = useState(false)
|
||||
|
||||
return (
|
||||
<PaginationItem
|
||||
onClick={e => onClick && onClick(e)}
|
||||
onMouseEnter={() => setShowMore(true)}
|
||||
onMouseLeave={() => setShowMore(false)}>
|
||||
{showMore ? (
|
||||
<svg
|
||||
className="more"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
shapeRendering="geometricPrecision">
|
||||
<path d="M13 17l5-5-5-5" />
|
||||
<path d="M6 17l5-5-5-5" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
shapeRendering="geometricPrecision">
|
||||
<circle cx="12" cy="12" r="1" fill="currentColor" />
|
||||
<circle cx="19" cy="12" r="1" fill="currentColor" />
|
||||
<circle cx="5" cy="12" r="1" fill="currentColor" />
|
||||
</svg>
|
||||
)}
|
||||
|
||||
<style jsx>{`
|
||||
svg {
|
||||
color: currentColor;
|
||||
stroke: currentColor;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.more {
|
||||
transform: rotate(${isBefore ? '180deg' : '0deg'});
|
||||
}
|
||||
`}</style>
|
||||
</PaginationItem>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaginationEllipsis
|
||||
101
components/pagination/pagination-item.tsx
Normal file
101
components/pagination/pagination-item.tsx
Normal file
@@ -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<React.ButtonHTMLAttributes<any>, keyof Props>
|
||||
export type PaginationItemProps = Props & NativeAttrs
|
||||
|
||||
const PaginationItem: React.FC<React.PropsWithChildren<PaginationItemProps>> = ({
|
||||
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 (
|
||||
<li>
|
||||
<button
|
||||
className={`${active ? 'active' : ''} ${disabled ? 'disabled' : ''}`}
|
||||
onClick={clickHandler}
|
||||
{...props}>
|
||||
{children}
|
||||
</button>
|
||||
<style jsx>{`
|
||||
li {
|
||||
margin-right: 6px;
|
||||
display: inline-block;
|
||||
}
|
||||
button {
|
||||
border: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
text-transform: capitalize;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
height: var(--pagination-size);
|
||||
min-width: var(--pagination-size);
|
||||
font-size: inherit;
|
||||
cursor: pointer;
|
||||
color: ${theme.palette.success};
|
||||
border-radius: ${theme.layout.radius};
|
||||
background-color: ${theme.palette.background};
|
||||
transition: all linear 200ms 0ms;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: ${hover};
|
||||
}
|
||||
|
||||
.active {
|
||||
font-weight: bold;
|
||||
background-color: ${theme.palette.success};
|
||||
color: ${theme.palette.background};
|
||||
box-shadow: ${theme.expressiveness.shadowSmall};
|
||||
}
|
||||
|
||||
.active:hover {
|
||||
background-color: ${activeHover};
|
||||
box-shadow: ${theme.expressiveness.shadowMedium};
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: ${theme.palette.accents_4};
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.disabled:hover {
|
||||
background-color: ${theme.palette.accents_2};
|
||||
}
|
||||
|
||||
button :global(svg) {
|
||||
width: 1.3em;
|
||||
height: 1.3em;
|
||||
}
|
||||
`}</style>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaginationItem
|
||||
19
components/pagination/pagination-next.tsx
Normal file
19
components/pagination/pagination-next.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
import PaginationItem from './pagination-item'
|
||||
import { usePaginationContext } from './pagination-context'
|
||||
|
||||
export type PaginationNextProps = React.ButtonHTMLAttributes<any>
|
||||
|
||||
const PaginationNext: React.FC<React.PropsWithChildren<PaginationNextProps>> = ({
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const { update, isLast } = usePaginationContext()
|
||||
return (
|
||||
<PaginationItem onClick={() => update && update('next')} disabled={isLast} {...props}>
|
||||
{children}
|
||||
</PaginationItem>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaginationNext
|
||||
104
components/pagination/pagination-pages.tsx
Normal file
104
components/pagination/pagination-pages.tsx
Normal file
@@ -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<SetStateAction<number>>
|
||||
}
|
||||
|
||||
const PaginationPages: React.FC<Props> = ({ 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) => (
|
||||
<PaginationItem
|
||||
key={`pagination-item-${value}`}
|
||||
active={value === active}
|
||||
onClick={() => setPage(value)}>
|
||||
{value}
|
||||
</PaginationItem>
|
||||
),
|
||||
[],
|
||||
)
|
||||
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 (
|
||||
<PaginationItem
|
||||
key={`pagination-middle-${index}`}
|
||||
active={index + 1 === middleNumber}
|
||||
onClick={() => setPage(value)}>
|
||||
{value}
|
||||
</PaginationItem>
|
||||
)
|
||||
})
|
||||
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 (
|
||||
<PaginationItem
|
||||
key={`pagination-item-${value}`}
|
||||
active={value === current}
|
||||
onClick={() => setPage(value)}>
|
||||
{value}
|
||||
</PaginationItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
/* eslint-enable */
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{renderItem(1, current)}
|
||||
{showBeforeEllipsis && (
|
||||
<PaginationEllipsis
|
||||
key="pagination-ellipsis-before"
|
||||
isBefore
|
||||
onClick={() => setPage(last => (last - 5 >= 1 ? last - 5 : 1))}
|
||||
/>
|
||||
)}
|
||||
{showBeforeEllipsis && showAfterEllipsis
|
||||
? middlePages
|
||||
: showBeforeEllipsis
|
||||
? endPages
|
||||
: startPages}
|
||||
{showAfterEllipsis && (
|
||||
<PaginationEllipsis
|
||||
key="pagination-ellipsis-after"
|
||||
onClick={() => setPage(last => (last + 5 <= count ? last + 5 : count))}
|
||||
/>
|
||||
)}
|
||||
{renderItem(count, current)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaginationPages
|
||||
19
components/pagination/pagination-previous.tsx
Normal file
19
components/pagination/pagination-previous.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
import PaginationItem from './pagination-item'
|
||||
import { usePaginationContext } from './pagination-context'
|
||||
|
||||
export type PaginationNextProps = React.ButtonHTMLAttributes<any>
|
||||
|
||||
const PaginationPrevious: React.FC<React.PropsWithChildren<PaginationNextProps>> = ({
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const { update, isFirst } = usePaginationContext()
|
||||
return (
|
||||
<PaginationItem onClick={() => update && update('prev')} disabled={isFirst} {...props}>
|
||||
{children}
|
||||
</PaginationItem>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaginationPrevious
|
||||
142
components/pagination/pagination.tsx
Normal file
142
components/pagination/pagination.tsx
Normal file
@@ -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<React.HTMLAttributes<any>, 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<React.PropsWithChildren<PaginationProps>> = ({
|
||||
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 = <PaginationPrevious>prev</PaginationPrevious>
|
||||
const nextDefault = <PaginationNext>next</PaginationNext>
|
||||
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<PaginationConfig>(
|
||||
() => ({
|
||||
isFirst: page <= 1,
|
||||
isLast: page >= count,
|
||||
update,
|
||||
}),
|
||||
[page],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
onChange && onChange(page)
|
||||
}, [page])
|
||||
useEffect(() => {
|
||||
if (customPage !== undefined) {
|
||||
setPage(customPage)
|
||||
}
|
||||
}, [customPage])
|
||||
|
||||
return (
|
||||
<PaginationContext.Provider value={values}>
|
||||
<nav>
|
||||
{prevItem}
|
||||
<PaginationPages count={count} current={page} limit={limit} setPage={setPage} />
|
||||
{nextItem}
|
||||
</nav>
|
||||
<style jsx>{`
|
||||
nav {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-variant: tabular-nums;
|
||||
font-feature-settings: 'tnum';
|
||||
font-size: ${font};
|
||||
--pagination-size: ${width};
|
||||
}
|
||||
|
||||
nav :global(button:last-of-type) {
|
||||
margin-right: 0;
|
||||
}
|
||||
`}</style>
|
||||
</PaginationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
type MemoPaginationComponent<P = {}> = React.NamedExoticComponent<P> & {
|
||||
Previous: typeof PaginationPrevious
|
||||
Next: typeof PaginationNext
|
||||
}
|
||||
|
||||
type ComponentProps = Partial<typeof defaultProps> &
|
||||
Omit<Props, keyof typeof defaultProps> &
|
||||
NativeAttrs
|
||||
|
||||
Pagination.defaultProps = defaultProps
|
||||
|
||||
export default React.memo(Pagination) as MemoPaginationComponent<ComponentProps>
|
||||
Reference in New Issue
Block a user