feat(components): pagination component added

This commit is contained in:
Junior Garcia 2022-10-12 21:44:59 -03:00
parent dbe37da64f
commit 1808f51ebb
29 changed files with 1647 additions and 3 deletions

View File

@ -0,0 +1,24 @@
# @nextui-org/pagination
A Quick description of the component
> This is an internal utility, not intended for public usage.
## Installation
```sh
yarn add @nextui-org/pagination
# or
npm i @nextui-org/pagination
```
## Contribution
Yes please! See the
[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md)
for details.
## Licence
This project is licensed under the terms of the
[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE).

View File

@ -0,0 +1,63 @@
import * as React from "react";
import {render} from "@testing-library/react";
import {Pagination} from "../src";
describe("Pagination", () => {
it("should render correctly", () => {
const wrapper = render(<Pagination total={10} />);
expect(() => wrapper.unmount()).not.toThrow();
});
it("ref should be forwarded", () => {
const ref = React.createRef<HTMLDivElement>();
render(<Pagination ref={ref} total={10} />);
expect(ref.current).not.toBeNull();
});
it("should render correctly with controls", () => {
const wrapper = render(<Pagination controls={true} total={10} />);
const nextButton = wrapper.getByLabelText("next page button");
const prevButton = wrapper.getByLabelText("previous page button");
expect(nextButton).not.toBeNull();
expect(prevButton).not.toBeNull();
});
it("should render correctly with no controls", () => {
const wrapper = render(<Pagination controls={false} total={10} />);
const nextButton = wrapper.queryByLabelText("next page button");
const prevButton = wrapper.queryByLabelText("previous page button");
expect(nextButton).toBeNull();
expect(prevButton).toBeNull();
});
it("should show dots when total is greater than 10", () => {
const wrapper = render(<Pagination total={10} />);
const dots = wrapper.queryAllByLabelText("dots element");
expect(dots).toHaveLength(1);
});
it("should show 2 dots when total is greater than 10 and the initialPage is in de middle", () => {
const wrapper = render(<Pagination initialPage={6} total={10} />);
const dots = wrapper.queryAllByLabelText("dots element");
expect(dots).toHaveLength(2);
});
it("the pagination highlight should have a aria-hidden attribute", () => {
const {container} = render(<Pagination total={10} />);
const highlight = container.querySelector(".nextui-pagination-highlight");
expect(highlight).toHaveAttribute("aria-hidden", "true");
});
});

View File

@ -0,0 +1,3 @@
{ "replace": { "main": "dist/index.cjs.js", "module": "dist/index.esm.js",
"types": "dist/index.d.ts", "exports": { ".": { "import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js" }, "./package.json": "./package.json" } } }

View File

@ -0,0 +1,52 @@
{
"name": "@nextui-org/pagination",
"version": "1.0.0-beta.11",
"description": "The Pagination component allows you to display active page and navigate between multiple pages.",
"keywords": [
"pagination"
],
"author": "Junior Garcia <jrgarciadev@gmail.com>",
"homepage": "https://nextui.org",
"license": "MIT",
"main": "src/index.ts",
"sideEffects": false,
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nextui-org/nextui.git",
"directory": "packages/components/pagination"
},
"bugs": {
"url": "https://github.com/nextui-org/nextui/issues"
},
"scripts": {
"build": "tsup src/index.ts --format=esm,cjs --dts",
"dev": "yarn build:fast -- --watch",
"clean": "rimraf dist .turbo",
"typecheck": "tsc --noEmit",
"build:fast": "tsup src/index.ts --format=esm,cjs",
"prepack": "clean-package",
"postpack": "clean-package restore"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"dependencies": {
"@nextui-org/system": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/shared-css": "workspace:*",
"@nextui-org/dom-utils": "workspace:*",
"@nextui-org/use-pagination": "workspace:*",
"@nextui-org/grid": "workspace:*",
"@react-aria/utils": "^3.14.0"
},
"devDependencies": {
"clean-package": "2.1.1",
"react": "^17.0.2"
}
}

View File

@ -0,0 +1,5 @@
// export types
export type {PaginationProps} from "./pagination";
// export component
export {default as Pagination} from "./pagination";

View File

@ -0,0 +1,81 @@
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {useState, MouseEvent} from "react";
import {clsx, __DEV__} from "@nextui-org/shared-utils";
import PaginationItem from "./pagination-item";
import {StyledPaginationEllipsis} from "./pagination.styles";
export interface PaginationEllipsisProps extends HTMLNextUIProps<"svg"> {
value?: string | number;
isBefore?: boolean;
onlyDots?: boolean;
animated?: boolean;
bordered?: boolean;
onClick?: (e: MouseEvent) => void;
}
const PaginationEllipsis = forwardRef<PaginationEllipsisProps, "svg">((props, ref) => {
const [showMore, setShowMore] = useState(false);
const {className, value, animated, bordered, onlyDots, isBefore, onClick, ...otherProps} = props;
const domRef = useDOMRef(ref);
return (
<PaginationItem
animated={animated}
bordered={bordered}
onlyDots={onlyDots}
value={value}
onClick={(e) => onClick?.(e)}
onMouseEnter={() => setShowMore(true)}
onMouseLeave={() => setShowMore(false)}
>
{showMore ? (
<StyledPaginationEllipsis
ref={domRef}
className={clsx("nextui-pagination-ellipsis", className)}
fill="none"
focusable="false"
isBefore={isBefore}
isEllipsis={true}
role="presentation"
shapeRendering="geometricPrecision"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
{...otherProps}
>
<path d="M13 17l5-5-5-5" />
<path d="M6 17l5-5-5-5" />
</StyledPaginationEllipsis>
) : (
<StyledPaginationEllipsis
fill="none"
isBefore={isBefore}
isEllipsis={false}
shapeRendering="geometricPrecision"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" fill="currentColor" r="1" />
<circle cx="19" cy="12" fill="currentColor" r="1" />
<circle cx="5" cy="12" fill="currentColor" r="1" />
</StyledPaginationEllipsis>
)}
</PaginationItem>
);
});
if (__DEV__) {
PaginationEllipsis.displayName = "NextUI.PaginationEllipsis";
}
PaginationEllipsis.toString = () => ".nextui-pagination-ellipsis";
export default PaginationEllipsis;

View File

@ -0,0 +1,67 @@
import {useState, useEffect, useMemo} from "react";
import {mergeProps} from "@react-aria/utils";
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, __DEV__} from "@nextui-org/shared-utils";
import {StyledPaginationHighlight} from "./pagination.styles";
export interface PaginationHighlightProps extends HTMLNextUIProps<"div"> {
active: number;
rounded?: boolean;
animated?: boolean;
noMargin?: boolean;
shadow?: boolean;
}
const PaginationHighlight = forwardRef<PaginationHighlightProps, "div">((props, ref) => {
const {className, css, active, shadow, noMargin, rounded, ...otherProps} = props;
const [selfActive, setSelfActive] = useState(active);
const [moveClassName, setMoveClassName] = useState("");
const domRef = useDOMRef(ref);
useEffect(() => {
if (active !== selfActive) {
setSelfActive(active);
setMoveClassName(`nextui-pagination-highlight--moving`);
const timer = setTimeout(() => {
setMoveClassName("");
clearTimeout(timer);
}, 350);
}
}, [active]);
const leftValue = useMemo(
() =>
noMargin
? `var(--nextui--paginationSize) * ${selfActive}`
: `var(--nextui--paginationSize) * ${selfActive} + ${selfActive * 4 + 2}px`,
[selfActive, noMargin],
);
return (
<StyledPaginationHighlight
ref={domRef}
aria-hidden={true}
className={clsx("nextui-pagination-highlight", moveClassName, className)}
css={{
left: "var(--nextui--paginationLeft)",
...css,
}}
noMargin={noMargin}
rounded={rounded}
shadow={shadow}
style={mergeProps({"--nextui--paginationLeft": `calc(${leftValue})`}, props?.style || {})}
{...otherProps}
/>
);
});
if (__DEV__) {
PaginationHighlight.displayName = "NextUI.PaginationHighlight";
}
PaginationHighlight.toString = () => ".nextui-pagination-highlight";
export default PaginationHighlight;

View File

@ -0,0 +1,62 @@
import {MouseEvent} from "react";
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, __DEV__} from "@nextui-org/shared-utils";
import PaginationItem from "./pagination-item";
import {StyledPaginationIcon} from "./pagination.styles";
export interface PaginationIconProps extends HTMLNextUIProps<"svg"> {
isPrev?: boolean;
disabled?: boolean;
onlyDots?: boolean;
animated?: boolean;
bordered?: boolean;
onClick?: (e: MouseEvent) => void;
}
const PaginationIcon = forwardRef<PaginationIconProps, "svg">((props, ref) => {
const {className, isPrev, disabled, onlyDots, animated, bordered, onClick, ...otherProps} = props;
const domRef = useDOMRef(ref);
return (
<PaginationItem
preserveContent
animated={animated}
bordered={bordered}
disabled={disabled}
onlyDots={onlyDots}
value={isPrev ? "<" : ">"}
onClick={(e) => onClick?.(e)}
>
<StyledPaginationIcon
ref={domRef}
className={clsx("nextui-pagination-icon", className)}
fill="none"
focusable="false"
isPrev={isPrev}
role="presentation"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...otherProps}
>
<path
d="M15.5 19l-7-7 7-7"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
/>
</StyledPaginationIcon>
</PaginationItem>
);
});
if (__DEV__) {
PaginationIcon.displayName = "NextUI.PaginationIcon";
}
PaginationIcon.toString = () => ".nextui-pagination-icon";
export default PaginationIcon;

View File

@ -0,0 +1,93 @@
import {useMemo, MouseEvent} from "react";
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, __DEV__} from "@nextui-org/shared-utils";
import {DOTS} from "@nextui-org/use-pagination";
import {StyledPaginationItem, StyledPaginationItemContent} from "./pagination.styles";
export interface PaginationItemProps extends HTMLNextUIProps<"button"> {
active?: boolean;
value?: string | number;
onlyDots?: boolean;
disabled?: boolean;
bordered?: boolean;
animated?: boolean;
preserveContent?: boolean;
onClick?: (e: MouseEvent) => void;
}
const getItemAriaLabel = (page?: string | number) => {
if (!page) return;
switch (page) {
case DOTS:
return "dots element";
case "<":
return "previous page button";
case ">":
return "next page button";
case "first":
return "first page button";
case "last":
return "last page button";
default:
return `${page} item`;
}
};
const PaginationItem = forwardRef<PaginationItemProps, "button">((props, ref) => {
const {
children,
className,
active,
value,
animated,
bordered,
disabled,
onlyDots,
preserveContent = false,
onClick,
...otherProps
} = props;
const domRef = useDOMRef(ref);
const ariaLabel = useMemo(
() => (active ? `${getItemAriaLabel(value)} active` : getItemAriaLabel(value)),
[value, active],
);
const clickHandler = (event: React.MouseEvent) => {
if (disabled) return;
onClick?.(event);
};
return (
<StyledPaginationItem
ref={domRef}
active={active}
animated={animated}
aria-label={ariaLabel}
bordered={bordered}
className={clsx("nextui-pagination-item", className)}
disabled={disabled}
onlyDots={onlyDots}
preserveContent={preserveContent}
tabIndex={disabled ? -1 : 0}
onClick={clickHandler}
{...otherProps}
>
<StyledPaginationItemContent className="nextui-pagination-item-count">
{children}
</StyledPaginationItemContent>
</StyledPaginationItem>
);
});
if (__DEV__) {
PaginationItem.displayName = "NextUI.PaginationItem";
}
PaginationItem.toString = () => ".nextui-pagination-item";
export default PaginationItem;

View File

@ -0,0 +1,13 @@
import {keyframes} from "@nextui-org/system";
export const scale = keyframes({
"0%": {
transform: "scale(1)",
},
"60%": {
transform: "scale($$paginationScaleTransform)",
},
"100%": {
transform: "scale(1)",
},
});

View File

@ -0,0 +1,366 @@
import {styled} from "@nextui-org/system";
import {sharedFocus} from "@nextui-org/shared-css";
import {scale} from "./pagination.animations";
export const StyledPaginationEllipsis = styled("svg", {
color: "currentColor",
stroke: "currentColor",
variants: {
isEllipsis: {
true: {
transform: "0deg",
},
},
isBefore: {
true: {},
},
},
compoundVariants: [
{
// isEllipsis && isBefore
isEllipsis: true,
isBefore: true,
css: {
transform: "rotate(180deg)",
},
},
],
});
export const StyledPaginationIcon = styled("svg", {
transform: "rotate(180deg)",
variants: {
isPrev: {
true: {
transform: "rotate(0deg)",
},
},
},
});
export const StyledPaginationItemContent = styled("span", {
position: "relative",
display: "inline-flex",
alignItems: "center",
top: 0,
left: 0,
zIndex: "$2",
});
export const StyledPaginationItem = styled(
"button",
{
border: "none",
position: "relative",
display: "inline-flex",
margin: "0 $$paginationItemMargin",
ai: "center",
jc: "center",
padding: 0,
boxSizing: "border-box",
tt: "capitalize",
us: "none",
whiteSpace: "nowrap",
ta: "center",
verticalAlign: "middle",
bs: "none",
outline: "none",
height: "$$paginationSize",
minWidth: "$$paginationSize",
fs: "inherit",
cursor: "pointer",
br: "$$paginationItemRadius",
color: "$text",
bg: "$accents0",
"@motion": {
transition: "none",
},
"&:hover": {
bg: "$accents1",
},
[`& ${StyledPaginationIcon}`]: {
size: "$$paginationFontSize",
},
[`& ${StyledPaginationEllipsis}`]: {
size: "$$paginationFontSize",
},
variants: {
active: {
true: {
fontWeight: "$bold",
cursor: "default",
boxShadow: "$sm",
[`& ${StyledPaginationItemContent}`]: {
color: "$white",
},
},
},
disabled: {
true: {
color: "$accents5",
cursor: "not-allowed",
},
},
bordered: {
true: {
bg: "transparent",
border: "$$paginationItemBorderWeight solid $accents2",
},
},
onlyDots: {
true: {},
},
preserveContent: {
true: {},
},
animated: {
true: {
transition: "transform 0.25s ease 0s, background 0.25s ease 0s, box-shadow 0.25s ease 0s",
},
false: {
transition: "none",
},
},
},
compoundVariants: [
// onlyDots && !preserveContent
{
onlyDots: true,
preserveContent: false,
css: {
[`& ${StyledPaginationItemContent}`]: {
display: "none",
},
},
},
// animated && !disabled && !active
{
animated: true,
disabled: false,
active: false,
css: {
"&:active": {
transform: "scale($$paginationScaleTransform)",
fs: "calc($$paginationFontSize * 0.9)",
},
},
},
],
},
sharedFocus,
);
export const StyledPaginationHighlight = styled("div", {
position: "absolute",
contain: "strict",
top: "0px",
zIndex: "$1",
bg: "$$paginationColor",
br: "$$paginationItemRadius",
height: "$$paginationSize",
minWidth: "$$paginationSize",
animationName: `${scale}`,
animationDirection: "normal",
"&.nextui-pagination-highlight--moving": {
transform: "scale($$paginationScaleTransform)",
},
"@motion": {
transition: "none",
"&.nextui-pagination-highlight--moving": {
transform: "scale(1)",
},
},
variants: {
animated: {
true: {
animationDuration: "350ms",
animationTimingFunction: "ease",
transition: "left 350ms ease 0s, transform 300ms ease 0s",
},
false: {
animationDuration: "none",
animationTimingFunction: "none",
transition: "none",
"&.nextui-pagination-highlight--moving": {
transform: "scale(1)",
},
},
},
noMargin: {
true: {
br: "$squared",
},
},
rounded: {
true: {},
},
shadow: {
true: {
normalShadowVar: "$$paginationShadowColor",
},
},
},
compoundVariants: [
{
// rounded && noMargin
rounded: true,
noMargin: true,
css: {
br: "$pill",
},
},
],
});
export const StyledPagination = styled("nav", {
m: 0,
p: 0,
d: "inline-flex",
position: "relative",
fontVariant: "tabular-nums",
fontFeatureSettings: "tnum",
variants: {
color: {
default: {
$$paginationColor: "$colors$primary",
$$paginationShadowColor: "$colors$primaryShadow",
},
primary: {
$$paginationColor: "$colors$primary",
$$paginationShadowColor: "$colors$primaryShadow",
},
secondary: {
$$paginationColor: "$colors$secondary",
$$paginationShadowColor: "$colors$secondaryShadow",
},
success: {
$$paginationColor: "$colors$success",
$$paginationShadowColor: "$colors$successShadow",
},
warning: {
$$paginationColor: "$colors$warning",
$$paginationShadowColor: "$colors$warningShadow",
},
error: {
$$paginationColor: "$colors$error",
$$paginationShadowColor: "$colors$errorShadow",
},
gradient: {
$$paginationColor: "$colors$gradient",
$$paginationShadowColor: "$colors$primaryShadow",
},
},
size: {
xs: {
$$paginationWidth: "$space$10",
$$paginationFontSize: "$space$5",
fs: "$$paginationFontSize",
},
sm: {
$$paginationWidth: "$space$12",
$$paginationFontSize: "$space$6",
fs: "$$paginationFontSize",
},
md: {
$$paginationWidth: "$space$13",
$$paginationFontSize: "$space$7",
fs: "$$paginationFontSize",
},
lg: {
$$paginationWidth: "$space$14",
$$paginationFontSize: "$space$8",
fs: "$$paginationFontSize",
},
xl: {
$$paginationWidth: "$space$15",
$$paginationFontSize: "$space$9",
fs: "$$paginationFontSize",
},
},
borderWeight: {
light: {
$$paginationItemBorderWeight: "$borderWeights$light",
},
normal: {
$$paginationItemBorderWeight: "$borderWeights$normal",
},
bold: {
$$paginationItemBorderWeight: "$borderWeights$bold",
},
extrabold: {
$$paginationItemBorderWeight: "$borderWeights$extrabold",
},
black: {
$$paginationItemBorderWeight: "$borderWeights$black",
},
},
bordered: {
true: {},
},
onlyDots: {
true: {
$$paginationSize: "calc($$paginationWidth / 2)",
$$paginationItemRadius: "$radii$pill",
$$paginationScaleTransform: 1.05,
},
false: {
$$paginationSize: "$$paginationWidth",
$$paginationScaleTransform: 1.1,
},
},
rounded: {
true: {
$$paginationItemRadius: "$radii$pill",
},
false: {
$$paginationItemRadius: "$radii$squared",
},
},
noMargin: {
true: {
$$paginationItemRadius: "0px",
$$paginationItemMargin: "0",
[`& ${StyledPaginationItem}:first-of-type`]: {
btlr: "$squared",
bblr: "$squared",
},
[`& ${StyledPaginationItem}:last-of-type`]: {
btrr: "$squared",
bbrr: "$squared",
},
},
false: {
$$paginationItemMargin: "$space$1",
},
},
},
compoundVariants: [
{
// bordered && noMargin
bordered: true,
noMargin: true,
css: {
[`& ${StyledPaginationItem}:not(:last-child)`]: {
borderRight: 0,
},
},
},
{
// noMargin && rounded
noMargin: true,
rounded: true,
css: {
$$paginationItemRadius: "0px",
},
},
{
// !rounded && noMargin
rounded: false,
noMargin: true,
css: {
$$paginationItemRadius: "0px",
},
},
],
});

View File

@ -0,0 +1,127 @@
import type {PaginationItemParam} from "@nextui-org/use-pagination";
import {useCallback} from "react";
import {forwardRef} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {DOTS} from "@nextui-org/use-pagination";
import {clsx, __DEV__} from "@nextui-org/shared-utils";
import PaginationItem from "./pagination-item";
import PaginationEllipsis from "./pagination-ellipsis";
import PaginationHighlight from "./pagination-highlight";
import PaginationIcon from "./pagination-icon";
import {StyledPagination} from "./pagination.styles";
import {UsePaginationProps, usePagination} from "./use-pagination";
export interface PaginationProps extends UsePaginationProps {}
const Pagination = forwardRef<PaginationProps, "nav">((props, ref) => {
const {
className,
controls,
animated,
rounded,
bordered,
shadow,
onlyDots,
dotsJump,
noMargin,
loop,
total,
range,
active,
setPage,
onPrevious,
onNext,
...otherProps
} = usePagination(props);
const domRef = useDOMRef(ref);
const renderItem = useCallback(
(value: PaginationItemParam, index: number) => {
if (value === DOTS) {
const isBefore = index < range.indexOf(active);
return (
<PaginationEllipsis
key={`nextui-pagination-item-${value}-${index}`}
animated={animated}
bordered={bordered}
isBefore={isBefore}
onlyDots={onlyDots}
value={value}
onClick={() =>
isBefore
? setPage(active - dotsJump >= 1 ? active - dotsJump : 1)
: setPage(active + dotsJump <= total ? active + dotsJump : total)
}
/>
);
}
return (
<PaginationItem
key={`nextui-pagination-item-${value}-${index}`}
active={value === active}
animated={animated}
bordered={bordered}
onlyDots={onlyDots}
value={value}
onClick={() => value !== active && setPage(value)}
>
{value}
</PaginationItem>
);
},
[total, onlyDots, active, bordered, animated],
);
return (
<StyledPagination
ref={domRef}
bordered={bordered}
className={clsx("nextui-pagination", className)}
noMargin={noMargin}
onlyDots={onlyDots}
rounded={rounded}
{...otherProps}
>
{controls && (
<PaginationIcon
isPrev
animated={animated}
bordered={bordered}
disabled={!loop && active === 1}
onlyDots={onlyDots}
onClick={onPrevious}
/>
)}
<PaginationHighlight
active={controls ? range.indexOf(active) + 1 : range.indexOf(active)}
animated={animated}
noMargin={noMargin}
rounded={rounded}
shadow={shadow}
/>
{range.map(renderItem)}
{controls && (
<PaginationIcon
animated={animated}
bordered={bordered}
disabled={!loop && active === total}
onlyDots={onlyDots}
onClick={onNext}
/>
)}
</StyledPagination>
);
});
if (__DEV__) {
Pagination.displayName = "NextUI.Pagination";
}
Pagination.toString = () => ".nextui-pagination";
export default Pagination;

View File

@ -0,0 +1,144 @@
import type {NormalColors, NormalSizes, NormalWeights} from "@nextui-org/shared-utils";
import type {HTMLNextUIProps} from "@nextui-org/system";
import {
usePagination as useBasePagination,
UsePaginationProps as UseBasePaginationProps,
} from "@nextui-org/use-pagination";
export interface UsePaginationProps extends HTMLNextUIProps<"nav", UseBasePaginationProps> {
/**
* The pagination color.
* @default "default"
*/
color?: NormalColors;
/**
* The pagination size.
* @default "md"
*/
size?: NormalSizes;
/**
* The border weight of the bordered pagination.
* @default "normal"
*/
borderWeight?: NormalWeights;
/**
* Show only dots as pagination elements
* @default false
*/
onlyDots?: boolean;
/**
* Number of pages that are added or subtracted on the '...' button.
* @default 5
*/
dotsJump?: number;
/**
* Whether the pagination is bordered.
* @default false
*/
bordered?: boolean;
/**
* Whether the pagination is rounded.
* @default false
*/
rounded?: boolean;
/**
* Whether to show a shadow effect.
* @default false
*/
shadow?: boolean;
/**
* Whether the pagination should have animations.
* @default true
*/
animated?: boolean;
/**
* Non disable next/previous controls
* @default false
*/
loop?: boolean;
/**
* Whether the pagination should have a margin.
* @default false
*/
noMargin?: boolean;
/**
* Whether the pagination should display controls (left/right arrows).
* @default true
*/
controls?: boolean;
}
export function usePagination(props: UsePaginationProps) {
const {
color = "default",
size = "md",
borderWeight = "normal",
rounded = false,
noMargin = false,
bordered = false,
animated = true,
shadow = false,
onlyDots = false,
controls = true,
dotsJump = 5,
loop = false,
total = 1,
initialPage,
page,
siblings,
boundaries,
onChange,
...otherProps
} = props;
const {range, active, setPage, previous, next, first, last} = useBasePagination({
page,
initialPage,
siblings: onlyDots ? 10 : siblings,
boundaries: onlyDots ? 10 : boundaries,
total,
onChange,
});
const onNext = () => {
if (loop && active === total) {
return first();
}
return next();
};
const onPrevious = () => {
if (loop && active === 1) {
return last();
}
return previous();
};
return {
color,
size,
bordered,
shadow,
onlyDots,
controls,
dotsJump,
animated,
noMargin,
borderWeight,
loop,
total,
active,
rounded,
range,
setPage,
onNext,
onPrevious,
...otherProps,
};
}
export type UsePaginationReturn = ReturnType<typeof usePagination>;

View File

@ -0,0 +1,161 @@
import React from "react";
import {Meta} from "@storybook/react";
import {Grid} from "@nextui-org/grid";
import {Pagination} from "../src";
export default {
title: "Navigation/Pagination",
component: Pagination,
} as Meta;
export const Default = () => <Pagination initialPage={1} total={20} />;
export const Colors = () => (
<>
<Grid xs={12}>
<Pagination color="primary" total={10} />
</Grid>
<Grid xs={12}>
<Pagination color="secondary" total={10} />
</Grid>
<Grid xs={12}>
<Pagination color="success" total={10} />
</Grid>
<Grid xs={12}>
<Pagination color="warning" total={10} />
</Grid>
<Grid xs={12}>
<Pagination color="error" total={10} />
</Grid>
<Grid xs={12}>
<Pagination color="gradient" total={10} />
</Grid>
</>
);
export const Sizes = () => (
<>
<Grid xs={12}>
<Pagination size="xs" total={10} />
</Grid>
<Grid xs={12}>
<Pagination size="sm" total={5} />
</Grid>
<Grid xs={12}>
<Pagination initialPage={6} size="md" total={10} />
</Grid>
<Grid xs={12}>
<Pagination initialPage={6} size="lg" total={10} />
</Grid>
<Grid xs={12}>
<Pagination initialPage={6} size="xl" total={30} />
</Grid>
</>
);
export const Rounded = () => (
<>
<Grid xs={12}>
<Pagination rounded size="xs" total={10} />
</Grid>
<Grid xs={12}>
<Pagination rounded size="sm" total={5} />
</Grid>
<Grid xs={12}>
<Pagination rounded initialPage={6} size="md" total={10} />
</Grid>
<Grid xs={12}>
<Pagination rounded initialPage={6} size="lg" total={10} />
</Grid>
<Grid xs={12}>
<Pagination rounded initialPage={6} size="xl" total={30} />
</Grid>
</>
);
export const Bordered = () => (
<>
<Grid xs={12}>
<Pagination bordered initialPage={1} total={20} />
</Grid>
<Grid xs={12}>
<Pagination bordered rounded initialPage={1} total={20} />
</Grid>
</>
);
export const Shadow = () => (
<>
<Grid xs={12}>
<Pagination shadow color="primary" total={10} />
</Grid>
<Grid xs={12}>
<Pagination rounded shadow color="secondary" total={10} />
</Grid>
<Grid xs={12}>
<Pagination shadow color="success" total={10} />
</Grid>
<Grid xs={12}>
<Pagination rounded shadow color="warning" total={10} />
</Grid>
<Grid xs={12}>
<Pagination shadow color="error" total={10} />
</Grid>
<Grid xs={12}>
<Pagination rounded shadow color="gradient" total={10} />
</Grid>
</>
);
export const OnlyDots = () => (
<>
<Grid xs={12}>
<Pagination onlyDots color="primary" size="xs" total={10} />
</Grid>
<Grid xs={12}>
<Pagination onlyDots shadow color="secondary" size="sm" total={10} />
</Grid>
<Grid xs={12}>
<Pagination onlyDots color="success" size="md" total={10} />
</Grid>
<Grid xs={12}>
<Pagination onlyDots shadow color="warning" size="lg" total={10} />
</Grid>
<Grid xs={12}>
<Pagination onlyDots color="error" size="xl" total={10} />
</Grid>
</>
);
export const Loop = () => (
<>
<Grid xs={12}>
<Pagination loop initialPage={1} total={6} />
</Grid>
</>
);
export const NoMargin = () => (
<>
<Grid xs={12}>
<Pagination noMargin shadow color="secondary" initialPage={1} total={6} />
</Grid>
</>
);
export const NoControls = () => (
<>
<Grid xs={12}>
<Pagination shadow color="success" controls={false} initialPage={1} total={20} />
</Grid>
</>
);
export const NoAnimated = () => (
<>
<Grid xs={12}>
<Pagination animated={false} initialPage={1} total={6} />
</Grid>
</>
);

View File

@ -0,0 +1,9 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"paths": {
"@stitches/react": ["../../../node_modules/@stitches/react"]
}
},
"include": ["src", "index.ts"]
}

View File

@ -0,0 +1,13 @@
import {defineConfig} from "tsup";
import {findUpSync} from "find-up";
export default defineConfig({
clean: true,
minify: false,
treeshake: true,
format: ["cjs", "esm"],
outExtension(ctx) {
return {js: `.${ctx.format}.js`};
},
inject: process.env.JSX ? [findUpSync("react-shim.js")!] : undefined,
});

View File

@ -58,7 +58,8 @@
"@nextui-org/snippet": "workspace:*",
"@nextui-org/spacer": "workspace:*",
"@nextui-org/text": "workspace:*",
"@nextui-org/user": "workspace:*"
"@nextui-org/user": "workspace:*",
"@nextui-org/pagination": "workspace:*"
},
"peerDependencies": {
"react": ">=16.8.0",

View File

@ -9,3 +9,4 @@ export * from "@nextui-org/grid";
export * from "@nextui-org/text";
export * from "@nextui-org/link";
export * from "@nextui-org/code";
export * from "@nextui-org/pagination";

View File

@ -0,0 +1,24 @@
# @nextui-org/use-pagination
A Quick description of the component
> This is an internal utility, not intended for public usage.
## Installation
```sh
yarn add @nextui-org/use-pagination
# or
npm i @nextui-org/use-pagination
```
## Contribution
Yes please! See the
[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md)
for details.
## Licence
This project is licensed under the terms of the
[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE).

View File

@ -0,0 +1,102 @@
import {renderHook, act} from "@testing-library/react-hooks";
import {usePagination} from "../src";
describe("usePagination", () => {
it("should work correctly", () => {
const {result} = renderHook(() => usePagination({total: 10}));
act(() => result.current.setPage(5));
expect(result.current.active).toBe(5);
});
it("should return correct initial state", () => {
const {result} = renderHook(() => usePagination({total: 10}));
expect(result.current.range).toStrictEqual([1, 2, 3, 4, 5, "dots", 10]);
expect(result.current.active).toBe(1);
});
it("should not change range length between page changes", () => {
const {result} = renderHook(() => usePagination({total: 10}));
[...new Array(10)].forEach(() => {
expect(result.current.range.length).toBe(7);
act(() => result.current.next());
});
});
it("should return the correct initial state with custom parameters", () => {
const {result} = renderHook(() =>
usePagination({
total: 20,
siblings: 2,
boundaries: 2,
initialPage: 7,
}),
);
expect(result.current.range).toStrictEqual([1, 2, "dots", 5, 6, 7, 8, 9, "dots", 19, 20]);
expect(result.current.active).toBe(7);
});
it("should work correctly with custom siblings", () => {
const {result} = renderHook(() =>
usePagination({
total: 20,
page: 7,
siblings: 2,
}),
);
expect(result.current.range).toStrictEqual([1, "dots", 5, 6, 7, 8, 9, "dots", 20]);
expect(result.current.active).toBe(7);
});
it("should work correctly without siblings", () => {
const {result} = renderHook(() =>
usePagination({
total: 20,
page: 7,
siblings: 0,
}),
);
expect(result.current.range).toStrictEqual([1, "dots", 7, "dots", 20]);
expect(result.current.active).toBe(7);
});
it("should work correctly with custom boundaries", () => {
const {result} = renderHook(() =>
usePagination({
total: 20,
page: 7,
boundaries: 2,
}),
);
expect(result.current.range).toStrictEqual([1, 2, "dots", 6, 7, 8, "dots", 19, 20]);
expect(result.current.active).toBe(7);
});
it("should work correctly without boundaries", () => {
const {result} = renderHook(() =>
usePagination({
total: 20,
page: 7,
boundaries: 0,
}),
);
expect(result.current.range).toStrictEqual(["dots", 6, 7, 8, "dots"]);
expect(result.current.active).toBe(7);
});
it("should call onChange function correctly", () => {
const onChange = jest.fn();
const {result} = renderHook(() => usePagination({total: 10, onChange}));
act(() => result.current.setPage(5));
expect(onChange).toBeCalledWith(5);
});
});

View File

@ -0,0 +1,3 @@
{ "replace": { "main": "dist/index.cjs.js", "module": "dist/index.esm.js",
"types": "dist/index.d.ts", "exports": { ".": { "import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js" }, "./package.json": "./package.json" } } }

View File

@ -0,0 +1,46 @@
{
"name": "@nextui-org/use-pagination",
"version": "1.0.0-beta.11",
"description": "State management hook for Pagination component, it lets you manage pagination with controlled and uncontrolled state",
"keywords": [
"use-pagination"
],
"author": "Junior Garcia <jrgarciadev@gmail.com>",
"homepage": "https://nextui.org",
"license": "MIT",
"main": "src/index.ts",
"sideEffects": false,
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nextui-org/nextui.git",
"directory": "packages/hooks/use-pagination"
},
"bugs": {
"url": "https://github.com/nextui-org/nextui/issues"
},
"scripts": {
"build": "tsup src/index.ts --format=esm,cjs --dts",
"dev": "yarn build:fast -- --watch",
"clean": "rimraf dist .turbo",
"typecheck": "tsc --noEmit",
"build:fast": "tsup src/index.ts --format=esm,cjs",
"prepack": "clean-package",
"postpack": "clean-package restore"
},
"dependencies": {
"@nextui-org/shared-utils": "workspace:*"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"devDependencies": {
"clean-package": "2.1.1",
"react": "^17.0.2"
}
}

View File

@ -0,0 +1,119 @@
import {useMemo, useCallback, useState, useEffect} from "react";
import {range} from "@nextui-org/shared-utils";
export interface UsePaginationProps {
/**
* The total number of pages.
*/
total: number;
/**
* The selected page on initial render.
* @default 1
*/
initialPage?: number;
/**
* The controlled selected page.
*/
page?: number;
/**
* The number of pages to show on each side of the current page.
* @default 1
*/
siblings?: number;
/**
* The number of pages to show at the beginning and end of the pagination.
* @default 1
*/
boundaries?: number;
/**
* Callback fired when the page changes.
*/
onChange?: (page: number) => void;
}
export const DOTS = "dots";
export type PaginationItemParam = number | typeof DOTS;
export function usePagination(props: UsePaginationProps) {
const {page, total, siblings = 1, boundaries = 1, initialPage = 1, onChange} = props;
const [activePage, setActivePage] = useState(page || initialPage);
const onChangeActivePage = (newPage: number) => {
setActivePage(newPage);
onChange && onChange(newPage);
};
useEffect(() => {
if (page && page !== activePage) {
setActivePage(page);
}
}, [page]);
const setPage = useCallback(
(pageNumber: number) => {
if (pageNumber <= 0) {
onChangeActivePage(1);
} else if (pageNumber > total) {
onChangeActivePage(total);
} else {
onChangeActivePage(pageNumber);
}
},
[total, activePage],
);
const next = () => setPage(activePage + 1);
const previous = () => setPage(activePage - 1);
const first = () => setPage(1);
const last = () => setPage(total);
const paginationRange = useMemo((): PaginationItemParam[] => {
const totalPageNumbers = siblings * 2 + 3 + boundaries * 2;
if (totalPageNumbers >= total) {
return range(1, total);
}
const leftSiblingIndex = Math.max(activePage - siblings, boundaries);
const rightSiblingIndex = Math.min(activePage + siblings, total - boundaries);
/*
* We do not want to show dots if there is only one position left
* after/before the left/right page count as that would lead to a change if our Pagination
* component size which we do not want
*/
const shouldShowLeftDots = leftSiblingIndex > boundaries + 2;
const shouldShowRightDots = rightSiblingIndex < total - (boundaries + 1);
if (!shouldShowLeftDots && shouldShowRightDots) {
const leftItemCount = siblings * 2 + boundaries + 2;
return [...range(1, leftItemCount), DOTS, ...range(total - (boundaries - 1), total)];
}
if (shouldShowLeftDots && !shouldShowRightDots) {
const rightItemCount = boundaries + 1 + 2 * siblings;
return [...range(1, boundaries), DOTS, ...range(total - rightItemCount, total)];
}
return [
...range(1, boundaries),
DOTS,
...range(leftSiblingIndex, rightSiblingIndex),
DOTS,
...range(total - boundaries + 1, total),
];
}, [total, siblings, activePage]);
return {
range: paginationRange,
active: activePage,
setPage,
next,
previous,
first,
last,
};
}
export type UsePaginationReturn = ReturnType<typeof usePagination>;

View File

@ -0,0 +1,4 @@
{
"extends": "../../../tsconfig.json",
"include": ["src", "index.ts"]
}

View File

@ -0,0 +1,13 @@
import {defineConfig} from "tsup";
import {findUpSync} from "find-up";
export default defineConfig({
clean: true,
minify: false,
treeshake: true,
format: ["cjs", "esm"],
outExtension(ctx) {
return {js: `.${ctx.format}.js`};
},
inject: process.env.JSX ? [findUpSync("react-shim.js")!] : undefined,
});

View File

@ -11,3 +11,4 @@ export * from "./dimensions";
export * from "./css-transition";
export * from "./functions";
export * from "./context";
export * from "./numbers";

View File

@ -0,0 +1,11 @@
/**
* Returns an array of numbers, starting at `start` and ending at `end`.
* @param start number
* @param end number
* @returns number[]
*/
export function range(start: number, end: number) {
const length = end - start + 1;
return Array.from({length}, (_, index) => index + start);
}

View File

@ -1,7 +1,7 @@
export interface Use{{capitalize hookName}}Props {}
export interface {{capitalize hookName}}Props {}
export function {{camelCase hookName}}(props: Use{{capitalize hookName}}Props = {}) {
export function {{camelCase hookName}}(props: {{capitalize hookName}}Props = {}) {
const {...otherProps} = props;
return {...otherProps};

36
pnpm-lock.yaml generated
View File

@ -637,6 +637,29 @@ importers:
clean-package: 2.1.1
react: 17.0.2
packages/components/pagination:
specifiers:
'@nextui-org/dom-utils': workspace:*
'@nextui-org/grid': workspace:*
'@nextui-org/shared-css': workspace:*
'@nextui-org/shared-utils': workspace:*
'@nextui-org/system': workspace:*
'@nextui-org/use-pagination': workspace:*
'@react-aria/utils': ^3.14.0
clean-package: 2.1.1
react: ^17.0.2
dependencies:
'@nextui-org/dom-utils': link:../../utilities/dom-utils
'@nextui-org/grid': link:../grid
'@nextui-org/shared-css': link:../../utilities/shared-css
'@nextui-org/shared-utils': link:../../utilities/shared-utils
'@nextui-org/system': link:../../core/system
'@nextui-org/use-pagination': link:../../hooks/use-pagination
'@react-aria/utils': 3.14.0_react@17.0.2
devDependencies:
clean-package: 2.1.1
react: 17.0.2
packages/components/row:
specifiers:
'@nextui-org/dom-utils': workspace:*
@ -742,6 +765,7 @@ importers:
'@nextui-org/image': workspace:*
'@nextui-org/link': workspace:*
'@nextui-org/loading': workspace:*
'@nextui-org/pagination': workspace:*
'@nextui-org/row': workspace:*
'@nextui-org/snippet': workspace:*
'@nextui-org/spacer': workspace:*
@ -766,6 +790,7 @@ importers:
'@nextui-org/image': link:../../components/image
'@nextui-org/link': link:../../components/link
'@nextui-org/loading': link:../../components/loading
'@nextui-org/pagination': link:../../components/pagination
'@nextui-org/row': link:../../components/row
'@nextui-org/snippet': link:../../components/snippet
'@nextui-org/spacer': link:../../components/spacer
@ -804,6 +829,17 @@ importers:
clean-package: 2.1.1
react: 17.0.2
packages/hooks/use-pagination:
specifiers:
'@nextui-org/shared-utils': workspace:*
clean-package: 2.1.1
react: ^17.0.2
dependencies:
'@nextui-org/shared-utils': link:../../utilities/shared-utils
devDependencies:
clean-package: 2.1.1
react: 17.0.2
packages/hooks/use-real-shape:
specifiers:
'@nextui-org/dom-utils': workspace:*