-
Notifications
You must be signed in to change notification settings - Fork 57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/react-pagination-component #1501
Open
terrance456
wants to merge
16
commits into
main
Choose a base branch
from
feat/react-pagination-component
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+283
−0
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
bbc3efb
feat: 🎸 pagination component
terrance456 c4f2ae3
refactor: 💡 update storybook docs
terrance456 32a37ce
Merge branch 'main' into feat/react-pagination-component
terrance456 d87e0ae
refactor: 💡 improve pagination logic
terrance456 bd9f020
Merge branch 'feat/react-pagination-component' of https://github.com/…
terrance456 e547683
refactor: 💡 fix linting issue
terrance456 69ec09a
refactor: 💡 remove redundant logic
terrance456 b5fe1ce
Merge branch 'main' into feat/react-pagination-component
astrit 24b9b7d
refactor: 💡 added href to anchor tag, accessibility support tem
terrance456 9c8762b
Merge branch 'feat/react-pagination-component' of https://github.com/…
terrance456 da9916a
Merge branch 'main' into feat/react-pagination-component
terrance456 689a3b9
Merge branch 'main' into feat/react-pagination-component
astrit a2eee7f
Merge branch 'main' into feat/react-pagination-component
astrit f162bff
Merge branch 'main' into feat/react-pagination-component
vsjolander 804e59a
Merge branch 'main' into feat/react-pagination-component
terrance456 2e33a27
Merge branch 'main' into feat/react-pagination-component
astrit File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { | ||
Meta, | ||
Story, | ||
Canvas, | ||
Markdown, | ||
ArgsTable, | ||
Preview, | ||
} from '@storybook/addon-docs' | ||
import { Pagination } from './pagination' | ||
import { useState } from 'react' | ||
|
||
export const Template = (props) => { | ||
const [pageIndex, setPageIndex] = useState(1) | ||
return ( | ||
<Pagination | ||
{...props} | ||
pageIndex={pageIndex} | ||
onClickPage={(index) => setPageIndex(index)} | ||
/> | ||
) | ||
} | ||
|
||
# Pagination | ||
|
||
The `Pagination` component allows users to navigate through large sets of items by dividing them into pages. | ||
It provides controls to switch between pages and displays the current page number. | ||
|
||
<Meta title="Components/Pagination" component={Pagination} /> | ||
|
||
<Canvas> | ||
<Story | ||
name="Pagination" | ||
args={{ | ||
length: 40, | ||
pageSize: 5, | ||
}} | ||
> | ||
{Template.bind({})} | ||
</Story> | ||
</Canvas> | ||
|
||
### Small | ||
|
||
<Canvas> | ||
<Story | ||
name="Small" | ||
args={{ | ||
length: 10, | ||
pageSize: 1, | ||
small: true, | ||
}} | ||
> | ||
{Template.bind({})} | ||
</Story> | ||
</Canvas> | ||
|
||
Here are the props available for the `Pagination` component: | ||
|
||
<ArgsTable of={Pagination} /> |
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 |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { render, screen } from '@testing-library/react' | ||
import { Pagination } from './pagination' | ||
import userEvent, { UserEvent } from '@testing-library/user-event' | ||
|
||
describe('Pagination', () => { | ||
it('Should render', () => { | ||
render(<Pagination length={10} pageSize={1} offset={10} />) | ||
expect(screen.getAllByRole('button')).toHaveLength(11) | ||
}) | ||
|
||
it('Should display dots on right', () => { | ||
render(<Pagination length={10} pageSize={1} />) | ||
expect(screen.getAllByText('...')).toHaveLength(1) | ||
}) | ||
|
||
it('Should display dots on left', () => { | ||
render(<Pagination length={10} pageSize={1} pageIndex={10} />) | ||
expect(screen.getAllByText('...')).toHaveLength(1) | ||
}) | ||
|
||
it('Should display dots on both sides', () => { | ||
render(<Pagination length={10} pageSize={1} pageIndex={4} />) | ||
expect(screen.getAllByText('...')).toHaveLength(2) | ||
}) | ||
|
||
it('Should fire onClickPage fn', async () => { | ||
const user: UserEvent = userEvent.setup() | ||
const mockOnClickPage: jest.Mock = jest.fn() | ||
render( | ||
<Pagination length={10} pageSize={1} onClickPage={mockOnClickPage} />, | ||
) | ||
await user.click(screen.getByText('2')) | ||
expect(mockOnClickPage).toBeCalledWith(2) | ||
}) | ||
|
||
it('Should navigate to next page', async () => { | ||
const user: UserEvent = userEvent.setup() | ||
const mockOnClickPage: jest.Mock = jest.fn() | ||
render( | ||
<Pagination length={10} pageSize={1} onClickPage={mockOnClickPage} />, | ||
) | ||
await user.click(screen.getByLabelText('Next Page')) | ||
expect(mockOnClickPage).toBeCalledWith(2) | ||
}) | ||
|
||
it('Should navigate to previous page', async () => { | ||
const user: UserEvent = userEvent.setup() | ||
const mockOnClickPage: jest.Mock = jest.fn() | ||
render( | ||
<Pagination | ||
length={10} | ||
pageSize={1} | ||
pageIndex={2} | ||
onClickPage={mockOnClickPage} | ||
/>, | ||
) | ||
await user.click(screen.getByLabelText('Previous Page')) | ||
expect(mockOnClickPage).toBeCalledWith(1) | ||
}) | ||
}) |
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 |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import { MouseEvent, useMemo } from 'react' | ||
import classNames from 'classnames' | ||
|
||
export interface PaginationProps { | ||
/** Determines if the pagination buttons should be small */ | ||
small?: boolean | ||
/**The total number of data items to paginate */ | ||
length: number | ||
/**The number of items to display per page, default 10 */ | ||
pageSize?: number | ||
/** The current page index, start from 1 */ | ||
pageIndex?: number | ||
/** The number of sibling pages to display around the current page, default 1 */ | ||
offset?: number | ||
/**Callback function to handle page changes */ | ||
onClickPage?: (pageIndex: number) => void | ||
} | ||
|
||
const chevronArrowLeft = ( | ||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"> | ||
<path d="M206.7 464.6l-183.1-191.1C18.22 267.1 16 261.1 16 256s2.219-11.97 6.688-16.59l183.1-191.1c9.152-9.594 24.34-9.906 33.9-.7187c9.625 9.125 9.938 24.37 .7187 33.91L73.24 256l168 175.4c9.219 9.5 8.906 24.78-.7187 33.91C231 474.5 215.8 474.2 206.7 464.6z" /> | ||
</svg> | ||
) | ||
|
||
const chevronArrowRight = ( | ||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"> | ||
<path d="M113.3 47.41l183.1 191.1c4.469 4.625 6.688 10.62 6.688 16.59s-2.219 11.97-6.688 16.59l-183.1 191.1c-9.152 9.594-24.34 9.906-33.9 .7187c-9.625-9.125-9.938-24.38-.7187-33.91l168-175.4L78.71 80.6c-9.219-9.5-8.906-24.78 .7187-33.91C88.99 37.5 104.2 37.82 113.3 47.41z" /> | ||
</svg> | ||
) | ||
|
||
const ELLIPSIS = '...' | ||
|
||
const getPageRange = (start: number, end: number) => { | ||
const length = end - start + 1 | ||
return Array.from({ length }, (_, idx) => idx + start) | ||
} | ||
|
||
export const Pagination = ({ | ||
small, | ||
length, | ||
pageSize = 10, | ||
pageIndex = 1, | ||
offset = 1, | ||
onClickPage, | ||
}: PaginationProps) => { | ||
const totalPageCount: number = useMemo( | ||
() => Math.ceil(length / pageSize), | ||
[length, pageSize], | ||
) | ||
|
||
const pageList: Array<string | number> | undefined = useMemo(() => { | ||
if (offset + 5 >= totalPageCount) { | ||
return getPageRange(1, totalPageCount) | ||
} | ||
const leftEllipsisIndex = Math.max(pageIndex - offset, 1) | ||
const rightEllipsisIndex = Math.min(pageIndex + offset, totalPageCount) | ||
const showLeftEllipsis = leftEllipsisIndex > 2 | ||
const showRightEllipsis = rightEllipsisIndex < totalPageCount - 2 | ||
const eachItemCount = 3 + 2 * offset | ||
|
||
// show right elipsis | ||
if (!showLeftEllipsis && showRightEllipsis) { | ||
return [...getPageRange(1, eachItemCount), ELLIPSIS, totalPageCount] | ||
} | ||
// show left elipsis | ||
if (showLeftEllipsis && !showRightEllipsis) { | ||
return [ | ||
1, | ||
ELLIPSIS, | ||
...getPageRange(totalPageCount - eachItemCount + 1, totalPageCount), | ||
] | ||
} | ||
// show both elipsis | ||
if (showLeftEllipsis && showRightEllipsis) { | ||
return [ | ||
1, | ||
ELLIPSIS, | ||
...getPageRange(leftEllipsisIndex, rightEllipsisIndex), | ||
ELLIPSIS, | ||
totalPageCount, | ||
] | ||
} | ||
return [] | ||
}, [offset, pageIndex, totalPageCount]) | ||
|
||
const onPrev = (event: MouseEvent<HTMLAnchorElement>) => { | ||
event.preventDefault() | ||
onClickPage && onClickPage(Math.max(pageIndex - 1, 1)) | ||
} | ||
|
||
const onNext = (event: MouseEvent<HTMLAnchorElement>) => { | ||
event.preventDefault() | ||
onClickPage && onClickPage(Math.min(pageIndex + 1, totalPageCount)) | ||
} | ||
|
||
return ( | ||
<nav | ||
className={classNames('pagination', small ? 'small' : 'large')} | ||
role="navigation" | ||
aria-label="Pagination" | ||
> | ||
<ul className="gds-reset"> | ||
{pageIndex !== 1 && ( | ||
<li> | ||
{ | ||
// eslint-disable-next-line jsx-a11y/anchor-is-valid | ||
<a | ||
href="" | ||
onClick={onPrev} | ||
aria-label="Previous Page" | ||
className="gds-reset" | ||
role="button" | ||
> | ||
{chevronArrowLeft} | ||
</a> | ||
} | ||
</li> | ||
)} | ||
{pageList.map((page: string | number, index: number) => | ||
page === ELLIPSIS ? ( | ||
<li key={index} aria-hidden="true"> | ||
... | ||
</li> | ||
) : ( | ||
<li key={index}> | ||
{ | ||
// eslint-disable-next-line jsx-a11y/anchor-is-valid | ||
<a | ||
href="" | ||
onClick={(e) => { | ||
e.preventDefault() | ||
onClickPage && onClickPage(page as number) | ||
}} | ||
className="gds-reset" | ||
role="button" | ||
aria-current={page === pageIndex ? 'page' : undefined} | ||
> | ||
{page} | ||
</a> | ||
} | ||
</li> | ||
), | ||
)} | ||
{!!totalPageCount && pageIndex !== totalPageCount && ( | ||
<li> | ||
{ | ||
// eslint-disable-next-line jsx-a11y/anchor-is-valid | ||
<a | ||
href="" | ||
onClick={onNext} | ||
role="button" | ||
aria-label="Next Page" | ||
className="gds-reset" | ||
> | ||
{chevronArrowRight} | ||
</a> | ||
} | ||
</li> | ||
)} | ||
</ul> | ||
</nav> | ||
) | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Using an anchor tag w/o
href
causes problems with focus. The browser skips them all even if theyrole="button"
.Is there a reason for using anchors tags?
Suggestion: Could these be an option to render
button
ora
tags in this component? Usingbutton
will update the current page dynamically and usinga
would take you to a new page, user would also need to providehrefs
for each anchor tag.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.
Totally agree in using button but the chlorophyll styling was added only for anchor specific and angular component is also using anchor tags. So wasn't sure if we should change it. Maybe we can add in
tabIndex=0
to solve the issue hereThere 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.
Yea, looks like this is how it was done in the angular component.
Ok for now! (until replaced with a core web component)