mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(components): pagination component added
This commit is contained in:
parent
dbe37da64f
commit
1808f51ebb
24
packages/components/pagination/README.md
Normal file
24
packages/components/pagination/README.md
Normal 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).
|
||||
63
packages/components/pagination/__tests__/pagination.test.tsx
Normal file
63
packages/components/pagination/__tests__/pagination.test.tsx
Normal 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");
|
||||
});
|
||||
});
|
||||
3
packages/components/pagination/clean-package.config.json
Normal file
3
packages/components/pagination/clean-package.config.json
Normal 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" } } }
|
||||
52
packages/components/pagination/package.json
Normal file
52
packages/components/pagination/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
5
packages/components/pagination/src/index.ts
Normal file
5
packages/components/pagination/src/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// export types
|
||||
export type {PaginationProps} from "./pagination";
|
||||
|
||||
// export component
|
||||
export {default as Pagination} from "./pagination";
|
||||
81
packages/components/pagination/src/pagination-ellipsis.tsx
Normal file
81
packages/components/pagination/src/pagination-ellipsis.tsx
Normal 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;
|
||||
67
packages/components/pagination/src/pagination-highlight.tsx
Normal file
67
packages/components/pagination/src/pagination-highlight.tsx
Normal 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;
|
||||
62
packages/components/pagination/src/pagination-icon.tsx
Normal file
62
packages/components/pagination/src/pagination-icon.tsx
Normal 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;
|
||||
93
packages/components/pagination/src/pagination-item.tsx
Normal file
93
packages/components/pagination/src/pagination-item.tsx
Normal 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;
|
||||
13
packages/components/pagination/src/pagination.animations.ts
Normal file
13
packages/components/pagination/src/pagination.animations.ts
Normal 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)",
|
||||
},
|
||||
});
|
||||
366
packages/components/pagination/src/pagination.styles.ts
Normal file
366
packages/components/pagination/src/pagination.styles.ts
Normal 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",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
127
packages/components/pagination/src/pagination.tsx
Normal file
127
packages/components/pagination/src/pagination.tsx
Normal 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;
|
||||
144
packages/components/pagination/src/use-pagination.ts
Normal file
144
packages/components/pagination/src/use-pagination.ts
Normal 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>;
|
||||
161
packages/components/pagination/stories/pagination.stories.tsx
Normal file
161
packages/components/pagination/stories/pagination.stories.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
9
packages/components/pagination/tsconfig.json
Normal file
9
packages/components/pagination/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@stitches/react": ["../../../node_modules/@stitches/react"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "index.ts"]
|
||||
}
|
||||
13
packages/components/pagination/tsup.config.ts
Normal file
13
packages/components/pagination/tsup.config.ts
Normal 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,
|
||||
});
|
||||
@ -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",
|
||||
|
||||
@ -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";
|
||||
|
||||
24
packages/hooks/use-pagination/README.md
Normal file
24
packages/hooks/use-pagination/README.md
Normal 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).
|
||||
102
packages/hooks/use-pagination/__tests__/use-pagination.test.tsx
Normal file
102
packages/hooks/use-pagination/__tests__/use-pagination.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
3
packages/hooks/use-pagination/clean-package.config.json
Normal file
3
packages/hooks/use-pagination/clean-package.config.json
Normal 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" } } }
|
||||
46
packages/hooks/use-pagination/package.json
Normal file
46
packages/hooks/use-pagination/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
119
packages/hooks/use-pagination/src/index.ts
Normal file
119
packages/hooks/use-pagination/src/index.ts
Normal 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>;
|
||||
4
packages/hooks/use-pagination/tsconfig.json
Normal file
4
packages/hooks/use-pagination/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"include": ["src", "index.ts"]
|
||||
}
|
||||
13
packages/hooks/use-pagination/tsup.config.ts
Normal file
13
packages/hooks/use-pagination/tsup.config.ts
Normal 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,
|
||||
});
|
||||
@ -11,3 +11,4 @@ export * from "./dimensions";
|
||||
export * from "./css-transition";
|
||||
export * from "./functions";
|
||||
export * from "./context";
|
||||
export * from "./numbers";
|
||||
|
||||
11
packages/utilities/shared-utils/src/numbers.ts
Normal file
11
packages/utilities/shared-utils/src/numbers.ts
Normal 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);
|
||||
}
|
||||
@ -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
36
pnpm-lock.yaml
generated
@ -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:*
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user