Skip to content

Commit

Permalink
feat: add Pagination component (#1474)
Browse files Browse the repository at this point in the history
## πŸ“ Changes

Create Pagination component

## βœ… Checklist

Easy UI has certain UX standards that must be met. In general,
non-trivial changes should meet the following criteria:

- [x] Visuals match Design Specs in Figma
- [x] Stories accompany any component changes
- [x] Code is in accordance with our style guide
- [x] Design tokens are utilized
- [x] Unit tests accompany any component changes
- [x] TSDoc is written for any API surface area
- [x] Specs are up-to-date
- [x] Console is free from warnings
- [x] No accessibility violations are reported
- [x] Cross-browser check is performed (Chrome, Safari, Firefox)
- [x] Changeset is added

~Strikethrough~ any items that are not applicable to this pull request.

---------

Co-authored-by: Kevin Liu <kliu@easypost.com>
  • Loading branch information
kevinalexliu and Kevin Liu authored Nov 19, 2024
1 parent c3bb727 commit eb43221
Show file tree
Hide file tree
Showing 10 changed files with 600 additions and 71 deletions.
5 changes: 5 additions & 0 deletions .changeset/friendly-planets-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@easypost/easy-ui": minor
---

feat: add Pagination component
184 changes: 113 additions & 71 deletions documentation/specs/Pagination.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Overview

A `Pagination` component enabled the user to divide large amounts of content into multiple pages, and navigate bwteen pages.
A `Pagination` component enabled the user to divide large amounts of content into multiple pages, and navigate between pages.

### Prior Art

Expand All @@ -13,17 +13,40 @@ A `Pagination` component enabled the user to divide large amounts of content int

## 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.
The design of the `Pagination` component consists of a next and a previous button for navigation, and a dropdown menu for users to select specific page.

### API

```ts
export type PaginationButtonProps = UnstyledButtonProps & {
symbol: IconSymbol;
};

export type PaginationDropdownProps = {
/**
* The current page.
*/
page: number;
/**
* The total number of pages.
*/
count: number;
/**
* Callback when select page from dropdown.
*/
onSelect: (key: number) => void;
/**
* Whether the PaginationDropdown should be disabled.
*/
isDisabled?: boolean;
};

export type PaginationProps = {
/**
* Whether there is a previous page to show.
* @default false
*/
hasPreviouse?: boolean;
hasPrevious?: boolean;
/**
* Whether there is a next page to show.
* @default false
Expand All @@ -37,80 +60,70 @@ export type PaginationProps = {
* 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;
label: string;
/**
* Whether the Pagination component should be disabled.
*/
isDisabled?: boolean;
/**
* The current page.
*/
page?: number;
/**
* The total number of pages.
* The children of `<Pagination />` component only
* accept `<Pagination.Dropdown />`.
*/
count?: number;
children?: ReactElement<PaginationDropdownProps>;
};
```

### Example Usage

_Basic useage:_
_Basic:_

```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}
onPrevious={() => {}}
onNext={() => {}}
hasPrevious
hasNext
/>
);
}
```

_Pagination with known total pages:_
_Pagination with Dropdown:_

```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);
const [page, setPage] = React.useState(1);
const totalPage = 10;
const handleNext = () => setPage((prev) => prev + 1);
const handlePrevious = () => setPage((prev) => prev - 1);
const hasPrevious = page > 1;
const hasNext = totalPage > page;
const handleSelect = (key: number) => setPage(key);
return (
<Pagination
label="Example"
page={pageRef.current}
label="Example Pagination with Dropdown"
onPrevious={handlePrevious}
onNext={handleNext}
hasPrevious={hasPrevious}
hadNext={hadNext}
count={totalPages}
onSelect={handleSelect}
/>
hasNext={hasNext}
>
<Pagination.Dropdown
count={totalPage}
page={page}
onSelect={handleSelect}
/>
</Pagination>
);
}
```
Expand All @@ -128,47 +141,80 @@ export function Component() {
### Anatomy

```tsx
export function PaginationButton(props: PaginationButtonProps) {
const { symbol, ...buttonProps } = props;
return (
<UnstyledButton {...buttonProps}>
<Icon symbol={symbol} size="md" />
</UnstyledButton>
);
}

export function PaginationDropdown(props: PaginationDropdownProps) {
const { page, count, onSelect, isDisabled } = props;
function numberToArray(num: number) {
return Array.from({ length: num }, (_, i) => i + 1);
}
return (
<Menu isDisabled={isDisabled}>
<Menu.Trigger>
<UnstyledButton
aria-label="dropdown"
className={styles.menuButton}
isDisabled={isDisabled}
>
<HorizontalStack blockAlign="center">
<Text variant="body2">{`${page} of ${count}`}</Text>
<Icon symbol={ExpandMoreIcon400} size="2xs" />
</HorizontalStack>
</UnstyledButton>
</Menu.Trigger>
<Menu.Overlay onAction={(key) => onSelect(Number(key))}>
{numberToArray(count).map((pageNumber) => (
<Menu.Item key={pageNumber} arial-label={`page ${pageNumber}`}>
<Text
variant="body2"
color="neutral.900"
>{`${pageNumber} of ${count}`}</Text>
</Menu.Item>
))}
</Menu.Overlay>
</Menu>
);
}

export function Pagination(props: PaginationProps) {
const {
label,
page,
hasPrevious = false,
hasNext = false,
onPrevious,
onNext,
hasPrevious,
hasNext,
count,
label,
isDisabled,
onSelect,
children,
} = 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)}
/>
<HorizontalStack>
<PaginationButton
aria-label="Previous"
onPress={onPrevious}
isDisabled={!hasPrevious || isDisabled}
symbol={KeyboardDoubleArrowLeft}
/>
{children && cloneElement(children, { isDisabled })}
<PaginationButton
aria-label="Next"
onPress={onNext}
isDisabled={!hasNext || isDisabled}
symbol={KeyboardDoubleArrowRight}
/>
</HorizontalStack>
</nav>
);
}
Expand All @@ -182,7 +228,3 @@ export function Pagination(props: PaginationProps) {

- 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.
34 changes: 34 additions & 0 deletions easy-ui-react/src/Pagination/Pagination.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from "react";
import { ArgTypes, Canvas, Meta } from "@storybook/blocks";
import { Pagination } from "./Pagination";
import * as PaginationStories from "./Pagination.stories";

<Meta of={PaginationStories} />

# Pagination

A `<Pagination />` component enabled the user to divide large amounts of content into multiple pages, and navigate between pages.

<Canvas of={PaginationStories.Default} />

## With Page Dropdown

A `<Pagination />` supports a dropdown menu for users to select specific page by including `<Pagination.Dropdown />` as children. `<Pagination.Dropdown />` is the only children `<Pagination />` accept.

<Canvas of={PaginationStories.WithPageDropdown} />

## Disabled

A `<Pagination />` can be disabled using `isDisabled`.

<Canvas of={PaginationStories.Disabled} />

## Properties

### Pagination

<ArgTypes of={Pagination} />

### Pagination.Dropdown

<ArgTypes of={Pagination.Dropdown} />
40 changes: 40 additions & 0 deletions easy-ui-react/src/Pagination/Pagination.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
@use "../styles/common" as *;

.menuButton {
padding: 0 design-token("space.1");
}
.arrowButton,
.menuButton {
color: design-token("color.primary.700");
border: design-token("shape.border_width.1") solid
design-token("color.neutral.200");

&:first-of-type {
border-radius: design-token("shape.border_radius.md") 0 0
design-token("shape.border_radius.md");
border-right: none;
}

&:last-of-type {
border-radius: 0 design-token("shape.border_radius.md")
design-token("shape.border_radius.md") 0;
border-left: none;
}

&:not(:disabled) {
cursor: pointer;
}

&:disabled {
color: design-token("color.neutral.400");
}

&:hover:not(:disabled):not(:active) {
color: design-token("color.primary.500");
}
}

.pagination:not(:has(.menuButton)) .arrowButton:first-of-type {
border-right: design-token("shape.border_width.1") solid
design-token("color.neutral.200");
}
Loading

0 comments on commit eb43221

Please sign in to comment.