diff --git a/src/pagination/Pagination.ts b/src/pagination/Pagination.ts new file mode 100644 index 000000000..edd936078 --- /dev/null +++ b/src/pagination/Pagination.ts @@ -0,0 +1,29 @@ +import { BoxHTMLProps, BoxOptions, useBox } from "reakit"; +import { createComponent, createHook } from "reakit-system"; + +import { PAGINATION_KEYS } from "./__keys"; +import { PaginationStateReturn } from "./PaginationState"; + +export type PaginationOptions = BoxOptions & PaginationStateReturn; + +export type PaginationHTMLProps = BoxHTMLProps; + +export type PaginationProps = PaginationOptions & PaginationHTMLProps; + +export const usePagination = createHook( + { + name: "Pagination", + compose: useBox, + keys: PAGINATION_KEYS, + + useProps(_, htmlProps) { + return { "aria-label": "pagination navigation", ...htmlProps }; + }, + }, +); + +export const Pagination = createComponent({ + as: "nav", + memo: true, + useHook: usePagination, +}); diff --git a/src/pagination/PaginationItem.ts b/src/pagination/PaginationItem.ts new file mode 100644 index 000000000..21181e3b3 --- /dev/null +++ b/src/pagination/PaginationItem.ts @@ -0,0 +1,52 @@ +import { callAllHandlers } from "@chakra-ui/utils"; +import React from "react"; +import { createComponent, createHook } from "reakit-system"; +import { ButtonHTMLProps, ButtonOptions, useButton } from "reakit"; + +import { PAGINATION_KEYS } from "./__keys"; +import { PaginationStateReturn } from "./PaginationState"; + +export type PaginationItemOptions = ButtonOptions & + PaginationStateReturn & { + page: number; + }; + +export type PaginationItemHTMLProps = ButtonHTMLProps; + +export type PaginationItemProps = PaginationItemOptions & + PaginationItemHTMLProps; + +export const usePaginationItem = createHook< + PaginationItemOptions, + PaginationItemHTMLProps +>({ + name: "PaginationItem", + compose: useButton, + keys: PAGINATION_KEYS, + + useProps( + { currentPage, page, goTo }, + { onClick: htmlOnClick, ...htmlProps }, + ) { + const isCurrent = currentPage === page; + + const onClick = React.useCallback(() => { + if (!isCurrent) { + goTo?.(page); + } + }, [goTo, isCurrent, page]); + + return { + "aria-label": isCurrent ? `Page ${page}` : `Go to Page ${page}`, + "aria-current": isCurrent ? true : undefined, + onClick: callAllHandlers(onClick, htmlOnClick), + ...htmlProps, + }; + }, +}); + +export const PaginationItem = createComponent({ + as: "button", + memo: true, + useHook: usePaginationItem, +}); diff --git a/src/pagination/PaginationNext.ts b/src/pagination/PaginationNext.ts new file mode 100644 index 000000000..f3ed2fe7f --- /dev/null +++ b/src/pagination/PaginationNext.ts @@ -0,0 +1,41 @@ +import React from "react"; +import { callAllHandlers } from "@chakra-ui/utils"; +import { createComponent, createHook } from "reakit-system"; +import { ButtonHTMLProps, ButtonOptions, useButton } from "reakit"; + +import { PAGINATION_KEYS } from "./__keys"; +import { PaginationStateReturn } from "./PaginationState"; + +export type PaginationNextOptions = ButtonOptions & PaginationStateReturn; + +export type PaginationNextHTMLProps = ButtonHTMLProps; + +export type PaginationNextProps = PaginationNextOptions & + PaginationNextHTMLProps; + +export const usePaginationNext = createHook< + PaginationNextOptions, + PaginationNextHTMLProps +>({ + name: "PaginationNext", + compose: useButton, + keys: PAGINATION_KEYS, + + useOptions(options, htmlProps) { + return { disabled: htmlProps.disabled || options.isAtMax, ...options }; + }, + + useProps({ next }, { onClick: htmlOnClick, ...htmlProps }) { + return { + "aria-label": "Next Page", + onClick: callAllHandlers(htmlOnClick, next), + ...htmlProps, + }; + }, +}); + +export const PaginationNext = createComponent({ + as: "button", + memo: true, + useHook: usePaginationNext, +}); diff --git a/src/pagination/PaginationPrev.ts b/src/pagination/PaginationPrev.ts new file mode 100644 index 000000000..4187bde7b --- /dev/null +++ b/src/pagination/PaginationPrev.ts @@ -0,0 +1,41 @@ +import { callAllHandlers } from "@chakra-ui/utils"; +import React from "react"; +import { createComponent, createHook } from "reakit-system"; +import { ButtonHTMLProps, ButtonOptions, useButton } from "reakit"; + +import { PAGINATION_KEYS } from "./__keys"; +import { PaginationStateReturn } from "./PaginationState"; + +export type PaginationPrevOptions = ButtonOptions & PaginationStateReturn; + +export type PaginationPrevHTMLProps = ButtonHTMLProps; + +export type PaginationPrevProps = PaginationPrevOptions & + PaginationPrevHTMLProps; + +export const usePaginationPrev = createHook< + PaginationPrevOptions, + PaginationPrevHTMLProps +>({ + name: "PaginationPrev", + compose: useButton, + keys: PAGINATION_KEYS, + + useOptions(options, htmlProps) { + return { disabled: htmlProps.disabled || options.isAtMin, ...options }; + }, + + useProps({ prev }, { onClick: htmlOnClick, ...htmlProps }) { + return { + "aria-label": "Previous Page", + onClick: callAllHandlers(htmlOnClick, prev), + ...htmlProps, + }; + }, +}); + +export const PaginationPrev = createComponent({ + as: "button", + memo: true, + useHook: usePaginationPrev, +}); diff --git a/src/pagination/PaginationState.ts b/src/pagination/PaginationState.ts new file mode 100644 index 000000000..6cc23a8e8 --- /dev/null +++ b/src/pagination/PaginationState.ts @@ -0,0 +1,43 @@ +import React from "react"; + +export interface UsePaginationProps { + totalItems?: number; + perPage?: number; + defaultPage?: number; +} + +export const usePaginationState = (props: UsePaginationProps = {}) => { + const { + totalItems = 50, + perPage = 10, + defaultPage: currentPageProp = 1, + } = props; + + const totalPages = Math.ceil(totalItems / perPage); + console.log("%c totalPages", "color: #aa00ff", totalPages); + + const [currentPage, setCurrentPage] = React.useState(currentPageProp); + + const pages = Array(totalPages) + .fill("") + .map((_, i) => i + 1); + + const isAtMax = currentPage >= totalPages; + const isAtMin = currentPage <= 1; + + const next = React.useCallback(() => { + setCurrentPage(prevPage => prevPage + 1); + }, []); + + const prev = React.useCallback(() => { + setCurrentPage(prevPage => prevPage - 1); + }, []); + + const goTo = React.useCallback(page => { + setCurrentPage(page); + }, []); + + return { currentPage, isAtMax, isAtMin, next, prev, goTo, pages }; +}; + +export type PaginationStateReturn = ReturnType; diff --git a/src/pagination/__keys.ts b/src/pagination/__keys.ts new file mode 100644 index 000000000..4f4e4797a --- /dev/null +++ b/src/pagination/__keys.ts @@ -0,0 +1,13 @@ +const PAGINATION_STATE_KEYS = [ + "currentPage", + "isAtMin", + "isAtMax", + "next", + "prev", + "goTo", + "id", + "page", + "pages", +] as const; + +export const PAGINATION_KEYS = PAGINATION_STATE_KEYS; diff --git a/src/pagination/index.ts b/src/pagination/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/pagination/stories/Pagination.stories.tsx b/src/pagination/stories/Pagination.stories.tsx new file mode 100644 index 000000000..8bca010cf --- /dev/null +++ b/src/pagination/stories/Pagination.stories.tsx @@ -0,0 +1,86 @@ +import React from "react"; + +import { Meta } from "@storybook/react"; +import { Pagination } from "../Pagination"; +import { PaginationPrev } from "../PaginationPrev"; +import { PaginationNext } from "../PaginationNext"; +import { PaginationItem } from "../PaginationItem"; +import { usePaginationState } from "../PaginationState"; + +export default { + title: "Component/Pagination", +} as Meta; + +// export const Default = () => { +// const state = usePaginationState(); +// console.log("%c state", "color: #00a3cc", state); + +// return ( +// +//
    +//
  • +// {"<"} +//
  • +//
  • +// +// 1 +// +//
  • +//
  • +// +// 2 +// +//
  • +//
  • +// +// 3 +// +//
  • +//
  • +// +// 4 +// +//
  • +//
  • +// +// 5 +// +//
  • +//
  • +// {">"} +//
  • +//
+//
+// ); +// }; + +export const Dynamic = () => { + const state = usePaginationState(); + console.log("%c state", "color: #00fe600", state); + + return ( + +
    +
  • + {"<"} +
  • + {state.pages.map(page => ( +
  • + + {page} + +
  • + ))} +
  • + {">"} +
  • +
+
+ ); +};