Skip to content
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

refactor(pagination): ♻️ add jsdoc to pagination #138

Merged
merged 6 commits into from
Nov 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const path = require("path");
const tsconfig = path.resolve(__dirname, "../tsconfig.json");
const tsconfig = path.resolve(__dirname, "../tsconfig.storybook.json");

module.exports = {
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
Expand All @@ -13,13 +13,8 @@ module.exports = {
// Need to configure typescript manually otherwise addons will not infer from types
// https://github.com/storybookjs/storybook/issues/11146#issuecomment-643878741
typescript: {
check: false,
checkOptions: { tsconfig },
reactDocgen: "react-docgen-typescript",
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
tsconfigPath: tsconfig,
propFilter: (prop: { name: string }) => !/^(testID)$/.test(prop.name),
},
},
};
46 changes: 25 additions & 21 deletions src/pagination/PaginationButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,21 @@ import { ButtonHTMLProps, ButtonOptions, useButton } from "reakit";
import { PAGINATION_BUTTON_KEYS } from "./__keys";
import { PaginationStateReturn } from "./PaginationState";

export type TGoto = "next" | "prev" | "last" | "first" | number;
export type TGoto = "nextPage" | "prevPage" | "lastPage" | "firstPage" | number;

export type PaginationButtonOptions = ButtonOptions &
Pick<
PaginationStateReturn,
| "currentPage"
| "move"
| "next"
| "prev"
| "first"
| "last"
| "isAtMax"
| "isAtMin"
| "movePage"
| "nextPage"
| "prevPage"
| "firstPage"
| "lastPage"
| "isAtLastPage"
| "isAtFirstPage"
> & {
goto: TGoto;
getAriaLabel?: (goto: TGoto, isCurrent: boolean) => string;
};

export type PaginationButtonHTMLProps = ButtonHTMLProps;
Expand All @@ -38,15 +37,15 @@ export const usePaginationButton = createHook<
keys: PAGINATION_BUTTON_KEYS,

useOptions(options, { disabled: htmlDisabled }) {
const { goto, isAtMax, isAtMin } = options;
const { goto, isAtLastPage, isAtFirstPage } = options;
let disabled = false;

if (goto === "next" || goto === "last") {
disabled = isAtMax;
if (goto === "nextPage" || goto === "lastPage") {
disabled = isAtLastPage;
}

if (goto === "prev" || goto === "first") {
disabled = isAtMin;
if (goto === "prevPage" || goto === "firstPage") {
disabled = isAtFirstPage;
}

return {
Expand All @@ -56,21 +55,26 @@ export const usePaginationButton = createHook<
},

useProps(options, { onClick: htmlOnClick, ...htmlProps }) {
const { currentPage, goto, getAriaLabel } = options;
const { currentPage, goto } = options;
const isCurrent = currentPage === goto;

const onClick = React.useCallback(() => {
if (options.disabled) return;

if (isNumber(goto)) {
options.move?.(goto);
} else if (["next", "prev", "last", "first"].includes(goto)) {
options.movePage?.(goto);
return;
}

if (["nextPage", "prevPage", "lastPage", "firstPage"].includes(goto)) {
options[goto]?.();
return;
}
}, [goto, options]);

const ariaLabel =
getAriaLabel?.(goto, isCurrent) ?? isCurrent
? `Page ${goto}`
: `Go to ${goto === "prev" ? "previous" : goto} Page`;
const ariaLabel = isCurrent
? `Page ${goto}`
: `Go to ${goto === "prevPage" ? "previous" : goto} Page`;

return {
"aria-label": ariaLabel,
Expand Down
139 changes: 95 additions & 44 deletions src/pagination/PaginationState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,86 @@
* Based on the logic from [usePagination Hook](https://github.com/mui-org/material-ui/blob/master/packages/material-ui-lab/src/Pagination/usePagination.js)
*/
import React from "react";
import { useControllableState } from "@chakra-ui/hooks";
import {
SealedInitialState,
useSealedState,
} from "reakit-utils/useSealedState";

export type PaginationState = {
/**
* The current active page
*
* @default 1
*/
currentPage: number;
/**
* All the page with start & end ellipsis
*/
pages: (string | number)[];
/**
* True, if the currentPage is at first page
*/
isAtFirstPage: boolean;
/**
* True, if the currentPage is at last page
*/
isAtLastPage: boolean;
};

export type PaginationAction = {
/**
* Go to the specified page number
*/
movePage: (page: number) => void;
/**
* Go to next page
*/
nextPage: () => void;
/**
* Go to previous page
*/
prevPage: () => void;
/**
* Go to first page
*/
firstPage: () => void;
/**
* Go to last page
*/
lastPage: () => void;
};

export interface UsePaginationProps {
export type PaginationInitialState = Pick<
Partial<PaginationState>,
"currentPage"
> & {
/**
* Total no. of pages
*/
count?: number;
defaultPage?: number;
/**
* No. of boundary pages to be visible
*/
boundaryCount?: number;
page?: number;
/**
* No. of sibiling pages allowed before/after the current page
*/
siblingCount?: number;
onChange?(value: number): void;
}
};

export type PaginationStateReturn = PaginationState & PaginationAction;

export const usePaginationState = (props: UsePaginationProps = {}) => {
export const usePaginationState = (
props: SealedInitialState<PaginationInitialState> = {},
): PaginationStateReturn => {
const {
currentPage: initialCurrentPage = 1,
count = 1,
defaultPage = 1,
boundaryCount = 1,
siblingCount = 1,
page: currentPage,
onChange,
} = props;
} = useSealedState(props);

const [page, setPage] = useControllableState({
value: currentPage,
defaultValue: defaultPage,
onChange,
shouldUpdate: (prev, next) => prev !== next,
});
const [currentPage, setCurrentPage] = React.useState(initialCurrentPage);

const startPages = range(1, Math.min(boundaryCount, count));
const endPages = range(
Expand All @@ -40,7 +93,7 @@ export const usePaginationState = (props: UsePaginationProps = {}) => {
const siblingsStart = Math.max(
Math.min(
// Natural start
page - siblingCount,
currentPage - siblingCount,
// Lower boundary when page is high
count - boundaryCount - siblingCount * 2 - 1,
),
Expand All @@ -51,7 +104,7 @@ export const usePaginationState = (props: UsePaginationProps = {}) => {
const siblingsEnd = Math.min(
Math.max(
// Natural end
page + siblingCount,
currentPage + siblingCount,
// Upper boundary when page is low
boundaryCount + siblingCount * 2 + 2,
),
Expand Down Expand Up @@ -84,44 +137,42 @@ export const usePaginationState = (props: UsePaginationProps = {}) => {
...endPages,
];

const next = React.useCallback(() => {
setPage(prevPage => prevPage + 1);
}, [setPage]);
const nextPage = React.useCallback(() => {
setCurrentPage(prevPage => prevPage + 1);
}, [setCurrentPage]);

const prev = React.useCallback(() => {
setPage(prevPage => prevPage - 1);
}, [setPage]);
const prevPage = React.useCallback(() => {
setCurrentPage(prevPage => prevPage - 1);
}, [setCurrentPage]);

const first = React.useCallback(() => {
setPage(1);
}, [setPage]);
const firstPage = React.useCallback(() => {
setCurrentPage(1);
}, [setCurrentPage]);

const last = React.useCallback(() => {
setPage(count);
}, [count, setPage]);
const lastPage = React.useCallback(() => {
setCurrentPage(count);
}, [count, setCurrentPage]);

const move = React.useCallback(
const movePage = React.useCallback(
page => {
if (page >= 1 && page <= count) setPage(page);
if (page >= 1 && page <= count) setCurrentPage(page);
},
[count, setPage],
[count, setCurrentPage],
);

return {
pages,
currentPage: page,
isAtMax: page >= count,
isAtMin: page <= 1,
next,
prev,
move,
first,
last,
currentPage,
isAtLastPage: currentPage >= count,
isAtFirstPage: currentPage <= 1,
nextPage,
prevPage,
movePage,
firstPage,
lastPage,
};
};

export type PaginationStateReturn = ReturnType<typeof usePaginationState>;

function range(start: number, end: number) {
const length = end - start + 1;

Expand Down
22 changes: 9 additions & 13 deletions src/pagination/__keys.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
// Automatically generated
const PAGINATION_STATE_KEYS = [
"pages",
"currentPage",
"isAtMax",
"isAtMin",
"next",
"prev",
"move",
"first",
"last",
"pages",
"isAtFirstPage",
"isAtLastPage",
"movePage",
"nextPage",
"prevPage",
"firstPage",
"lastPage",
] as const;
export const PAGINATION_KEYS = PAGINATION_STATE_KEYS;
export const PAGINATION_BUTTON_KEYS = [
...PAGINATION_KEYS,
"goto",
"getAriaLabel",
] as const;
export const PAGINATION_BUTTON_KEYS = [...PAGINATION_KEYS, "goto"] as const;
14 changes: 7 additions & 7 deletions src/pagination/__tests__/Pagination.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ import {
Pagination,
PaginationButton,
usePaginationState,
UsePaginationProps,
} from "..";
PaginationInitialState,
} from "../index";
import { repeat } from "../../utils/test-utils";

const PaginationComp: React.FC<UsePaginationProps> = props => {
const PaginationComp: React.FC<PaginationInitialState> = props => {
const state = usePaginationState({ count: 10, ...props });

return (
<Pagination {...state}>
<ul style={{ display: "flex", listStyle: "none" }}>
<li>
<PaginationButton goto="first" {...state}>
<PaginationButton goto="firstPage" {...state}>
First
</PaginationButton>
</li>
<li>
<PaginationButton goto="prev" {...state}>
<PaginationButton goto="prevPage" {...state}>
Previous
</PaginationButton>
</li>
Expand All @@ -46,12 +46,12 @@ const PaginationComp: React.FC<UsePaginationProps> = props => {
);
})}
<li>
<PaginationButton goto="next" {...state}>
<PaginationButton goto="nextPage" {...state}>
Next
</PaginationButton>
</li>
<li>
<PaginationButton goto="last" {...state}>
<PaginationButton goto="lastPage" {...state}>
Last
</PaginationButton>
</li>
Expand Down
Loading