-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
## 📝 Changes - Pagination spec Co-authored-by: Kevin Liu <kliu@easypost.com>
- Loading branch information
1 parent
1fd8015
commit 8da6b85
Showing
1 changed file
with
188 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
# `Pagination` Component Specification | ||
|
||
## Overview | ||
|
||
A `Pagination` component enabled the user to divide large amounts of content into multiple pages, and navigate bwteen pages. | ||
|
||
### Prior Art | ||
|
||
- [Paste `<Pagination />`](https://paste.twilio.design/components/pagination) | ||
- [Polaris `<Pagination />`](https://polaris.shopify.com/components/navigation/pagination) | ||
|
||
--- | ||
|
||
## Design | ||
|
||
The design of the `Pagination` component consists of a next and a previous button for navigation, and a dropdown for the user to select specific page. This component will rely on React Aria's `useFocusRing` and `useKeyboard` hooks to handle keyboard interactions. | ||
|
||
### API | ||
|
||
```ts | ||
export type PaginationProps = { | ||
/** | ||
* Whether there is a previous page to show. | ||
* @default false | ||
*/ | ||
hasPreviouse?: boolean; | ||
/** | ||
* Whether there is a next page to show. | ||
* @default false | ||
*/ | ||
hasNext?: boolean; | ||
/** | ||
* Callback when previous button is clicked. | ||
*/ | ||
onPrevious?: () => void; | ||
/** | ||
* Callback when next button is clicked. | ||
*/ | ||
onNext?: () => void; | ||
/** | ||
* Callback when select page from dropdown. | ||
*/ | ||
onSelect?: () => void; | ||
/** | ||
* Accessible label for Pagination, used as the | ||
* aria-label. | ||
*/ | ||
label: ReactNode; | ||
/** | ||
* Whether the Pagination component should be disabled. | ||
*/ | ||
isDisabled?: boolean; | ||
/** | ||
* The current page. | ||
*/ | ||
page?: number; | ||
/** | ||
* The total number of pages. | ||
*/ | ||
count?: number; | ||
}; | ||
``` | ||
|
||
### Example Usage | ||
|
||
_Basic useage:_ | ||
|
||
```tsx | ||
import { Pagination } from "@easypost/easy-ui/Pagination"; | ||
|
||
export function Component() { | ||
const pageRef = React.useRef(1); | ||
const handleNext = () => (pageRef.current += 1); | ||
const handlePrevious = () => (pageRef.current -= 1); | ||
const hasPrevious = pageRef.current > 1; | ||
const hadNext = count > pageRef.current; | ||
return ( | ||
<Pagination | ||
label="Example" | ||
page={pageRef.current} | ||
onPrevious={handlePrevious} | ||
onNext={handleNext} | ||
hasPrevious={hasPrevious} | ||
hadNext={hadNext} | ||
/> | ||
); | ||
} | ||
``` | ||
|
||
_Pagination with known total pages:_ | ||
|
||
```tsx | ||
import { Pagination } from "@easypost/easy-ui/Pagination"; | ||
|
||
export function Component() { | ||
const pageRef = React.useRef(1); | ||
const totalPages = 5; | ||
const handleNext = () => (pageRef.current += 1); | ||
const handlePrevious = () => (pageRef.current -= 1); | ||
const hasPrevious = pageRef.current > 1; | ||
const hadNext = count > pageRef.current; | ||
const handleSelect = (page) => (pageRef.current = page); | ||
return ( | ||
<Pagination | ||
label="Example" | ||
page={pageRef.current} | ||
onPrevious={handlePrevious} | ||
onNext={handleNext} | ||
hasPrevious={hasPrevious} | ||
hadNext={hadNext} | ||
count={totalPages} | ||
onSelect={handleSelect} | ||
/> | ||
); | ||
} | ||
``` | ||
|
||
_Disabled:_ | ||
|
||
```tsx | ||
import { Pagination } from "@easypost/easy-ui/Pagination"; | ||
|
||
export function Component() { | ||
return <Pagination label="Example" isDisabled />; | ||
} | ||
``` | ||
|
||
### Anatomy | ||
|
||
```tsx | ||
export function Pagination(props: PaginationProps) { | ||
const { | ||
label, | ||
page, | ||
onPrevious, | ||
onNext, | ||
hasPrevious, | ||
hasNext, | ||
count, | ||
isDisabled, | ||
onSelect, | ||
} = props; | ||
const className = classNames( | ||
styles.pagination, | ||
isDisabled && styles.disabled, | ||
); | ||
function numberToArray(num: number) { | ||
return Array.from({ length: num }, (_, i) => i + 1); | ||
} | ||
|
||
return ( | ||
<nav aria-label={label} className={className}> | ||
<button | ||
aria-label="Previous" | ||
onClick={onPrevious} | ||
className={classNames(!hasPrevious && styles.buttonDisabled)} | ||
/> | ||
{count && ( | ||
<Select selectedKey={page} onSelectionChange={onSelect}> | ||
{numberToArray(count).map((number) => ( | ||
<Select.Option key={number} arial-label={`page ${number}`}> | ||
<Text>{`${number} of ${count}`}</Text> | ||
</Select.Option> | ||
))} | ||
</Select> | ||
)} | ||
<button | ||
aria-label="Next" | ||
onClick={onNext} | ||
className={classNames(!hasNext && styles.buttonDisabled)} | ||
/> | ||
</nav> | ||
); | ||
} | ||
``` | ||
|
||
--- | ||
|
||
## Behavior | ||
|
||
### Accessibility | ||
|
||
- Use a wrapping `<nav>` element to identify Pagination as a navigation section. | ||
- An appropriate `aria-label` should be provided through `label` props. | ||
|
||
### Dependencies | ||
|
||
- React Aria's `useFocusRing` and `useKeyboard` for keyboard control. |