-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Limited pagination using `maxButton` prop - Also made it fully customizable via direct props for the ButtonGroup, or via buttonProps for all the page button props - Now we don't use `0` in the URL for paginating results, despite the API requiring it I'd still move this to ui-components at some point, because we're gonna need it in ui-scaffold sooner than later. * Remove unnecessary else * Simplify ellipsis creation with a custom component Also fixed some issues related to old pagination flow (removing unnecessary adds and substracts). And passed missing arguments to both ellipsis buttons and ellipsis inputs. * Hard limit min number of maxButtons to 5 * Fix keys and pages issue * Fix pages position after latest changes
- Loading branch information
1 parent
198d5ca
commit 3019238
Showing
4 changed files
with
196 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,45 +1,196 @@ | ||
import { Button, ButtonGroup } from '@chakra-ui/react' | ||
import { ReactElement, useMemo } from 'react' | ||
import { generatePath, Link as RouterLink, useParams } from 'react-router-dom' | ||
import { Button, ButtonGroup, ButtonGroupProps, ButtonProps, Input, InputProps } from '@chakra-ui/react' | ||
import { ReactElement, useMemo, useState } from 'react' | ||
import { generatePath, Link as RouterLink, useNavigate, useParams } from 'react-router-dom' | ||
import { usePagination, useRoutedPagination } from './PaginationProvider' | ||
|
||
export const Pagination = () => { | ||
const { page, setPage, totalPages } = usePagination() | ||
export type PaginationProps = ButtonGroupProps & { | ||
maxButtons?: number | false | ||
buttonProps?: ButtonProps | ||
inputProps?: InputProps | ||
} | ||
|
||
const createButton = (page: number, currentPage: number, props: ButtonProps) => ( | ||
<Button key={page} isActive={currentPage === page} {...props}> | ||
{page + 1} | ||
</Button> | ||
) | ||
|
||
type EllipsisButtonProps = ButtonProps & { | ||
gotoPage: (page: number) => void | ||
inputProps?: InputProps | ||
} | ||
|
||
const EllipsisButton = ({ gotoPage, inputProps, ...rest }: EllipsisButtonProps) => { | ||
const [ellipsisInput, setEllipsisInput] = useState(false) | ||
|
||
const pages: ReactElement[] = useMemo(() => { | ||
const pages: ReactElement[] = [] | ||
if (ellipsisInput) { | ||
return ( | ||
<Input | ||
placeholder='Page #' | ||
width='50px' | ||
{...inputProps} | ||
onKeyDown={(e) => { | ||
if (e.target instanceof HTMLInputElement && e.key === 'Enter') { | ||
const pageNumber = Number(e.target.value) | ||
gotoPage(pageNumber) | ||
setEllipsisInput(false) | ||
} | ||
}} | ||
onBlur={() => setEllipsisInput(false)} | ||
autoFocus | ||
/> | ||
) | ||
} | ||
|
||
return ( | ||
<Button | ||
as='a' | ||
href='#goto-page' | ||
{...rest} | ||
onClick={(e) => { | ||
e.preventDefault() | ||
setEllipsisInput(true) | ||
}} | ||
> | ||
... | ||
</Button> | ||
) | ||
} | ||
|
||
const usePaginationPages = ( | ||
currentPage: number, | ||
totalPages: number | undefined, | ||
maxButtons: number | undefined | false, | ||
gotoPage: (page: number) => void, | ||
createPageButton: (i: number) => ReactElement, | ||
inputProps?: InputProps, | ||
buttonProps?: ButtonProps | ||
) => { | ||
return useMemo(() => { | ||
if (totalPages === undefined) return [] | ||
|
||
let pages: ReactElement[] = [] | ||
|
||
// Create an array of all page buttons | ||
for (let i = 0; i < totalPages; i++) { | ||
pages.push( | ||
<Button key={i} onClick={() => setPage(i)} isActive={page === i}> | ||
{i + 1} | ||
</Button> | ||
) | ||
pages.push(createPageButton(i)) | ||
} | ||
return pages | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [page, totalPages]) | ||
|
||
return <ButtonGroup isAttached>{pages.map((page) => page)}</ButtonGroup> | ||
if (!maxButtons || totalPages <= maxButtons) { | ||
return pages | ||
} | ||
|
||
const startEllipsis = ( | ||
<EllipsisButton key='start-ellipsis' gotoPage={gotoPage} inputProps={inputProps} {...buttonProps} /> | ||
) | ||
const endEllipsis = ( | ||
<EllipsisButton key='end-ellipsis' gotoPage={gotoPage} inputProps={inputProps} {...buttonProps} /> | ||
) | ||
|
||
// Add ellipsis and slice the array accordingly | ||
const sideButtons = 2 // First and last page | ||
const availableButtons = maxButtons - sideButtons // Buttons we can distribute around the current page | ||
|
||
if (currentPage <= availableButtons / 2) { | ||
// Near the start | ||
return [...pages.slice(0, availableButtons), endEllipsis, pages[totalPages - 1]] | ||
} else if (currentPage >= totalPages - 1 - availableButtons / 2) { | ||
// Near the end | ||
return [pages[0], startEllipsis, ...pages.slice(totalPages - availableButtons, totalPages)] | ||
} else { | ||
// In the middle | ||
const startPage = currentPage - Math.floor((availableButtons - 1) / 2) | ||
const endPage = currentPage + Math.floor(availableButtons / 2) | ||
return [pages[0], startEllipsis, ...pages.slice(startPage, endPage - 1), endEllipsis, pages[totalPages - 1]] | ||
} | ||
}, [currentPage, totalPages, maxButtons, gotoPage]) | ||
} | ||
|
||
export const RoutedPagination = () => { | ||
export const Pagination = ({ maxButtons = 10, buttonProps, inputProps, ...rest }: PaginationProps) => { | ||
const { page, setPage, totalPages } = usePagination() | ||
|
||
const pages = usePaginationPages( | ||
page, | ||
totalPages, | ||
maxButtons ? Math.max(5, maxButtons) : false, | ||
(page) => { | ||
if (page >= 0 && totalPages && page < totalPages) { | ||
setPage(page) | ||
} | ||
}, | ||
(i) => createButton(i, page, { onClick: () => setPage(i), ...buttonProps }) | ||
) | ||
|
||
return ( | ||
<ButtonGroup {...rest}> | ||
{totalPages === undefined ? ( | ||
<> | ||
<Button key='previous' onClick={() => setPage(page - 1)} isDisabled={page === 0} {...buttonProps}> | ||
Previous | ||
</Button> | ||
<Button key='next' onClick={() => setPage(page + 1)} {...buttonProps}> | ||
Next | ||
</Button> | ||
</> | ||
) : ( | ||
pages | ||
)} | ||
</ButtonGroup> | ||
) | ||
} | ||
|
||
export const RoutedPagination = ({ maxButtons = 10, buttonProps, ...rest }: PaginationProps) => { | ||
const { path, totalPages } = useRoutedPagination() | ||
const { page }: { page?: number } = useParams() | ||
const { page, ...extraParams }: { page?: number } = useParams() | ||
const navigate = useNavigate() | ||
|
||
const p = Number(page) || 0 | ||
const p = Number(page) || 1 | ||
|
||
const pages: ReactElement[] = useMemo(() => { | ||
const pages: ReactElement[] = [] | ||
for (let i = 0; i < totalPages; i++) { | ||
pages.push( | ||
<Button as={RouterLink} key={i} to={generatePath(path, { page: i })} isActive={p === i}> | ||
{i + 1} | ||
</Button> | ||
) | ||
} | ||
return pages | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [p, totalPages]) | ||
const pages = usePaginationPages( | ||
p, | ||
totalPages, | ||
maxButtons ? Math.max(5, maxButtons) : false, | ||
(page) => { | ||
if (page >= 0 && totalPages && page < totalPages) { | ||
navigate(generatePath(path, { page: page, ...extraParams })) | ||
} | ||
}, | ||
(i) => ( | ||
<Button | ||
as={RouterLink} | ||
key={i} | ||
to={generatePath(path, { page: i + 1, ...extraParams })} | ||
isActive={p - 1 === i} | ||
{...buttonProps} | ||
> | ||
{i + 1} | ||
</Button> | ||
) | ||
) | ||
|
||
return <ButtonGroup isAttached>{pages.map((page) => page)}</ButtonGroup> | ||
return ( | ||
<ButtonGroup {...rest}> | ||
{totalPages === undefined ? ( | ||
<> | ||
<Button | ||
key='previous' | ||
onClick={() => navigate(generatePath(path, { page: p, ...extraParams }))} | ||
isDisabled={p === 0} | ||
{...buttonProps} | ||
> | ||
Previous | ||
</Button> | ||
<Button | ||
key='next' | ||
onClick={() => navigate(generatePath(path, { page: p + 2, ...extraParams }))} | ||
{...buttonProps} | ||
> | ||
Next | ||
</Button> | ||
</> | ||
) : ( | ||
pages | ||
)} | ||
</ButtonGroup> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3019238
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π Published on https://vocdoni-explorer-dev.netlify.app as production
π Deployed on https://6662cd9a7d1f5fea97c788e6--vocdoni-explorer-dev.netlify.app
3019238
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π Published on https://vocdoni-explorer-stg.netlify.app as production
π Deployed on https://6662cd9cd83d96ea05bd2d12--vocdoni-explorer-stg.netlify.app