mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat: pagination initial structure added, tailwind variants upgraded
This commit is contained in:
parent
d12ee9e768
commit
99dffc3c9d
@ -1,15 +1,20 @@
|
||||
import Pagination from "./pagination";
|
||||
import PaginationItem from "./pagination-item";
|
||||
import PaginationCursor from "./pagination-cursor";
|
||||
|
||||
// export types
|
||||
export type {PaginationProps} from "./pagination";
|
||||
export type {PaginationItemRenderProps} from "./use-pagination";
|
||||
export type {PaginationItemProps} from "./pagination-item";
|
||||
export type {DOTS as PAGINATION_DOTS} from "@nextui-org/use-pagination";
|
||||
export type {PaginationCursorProps} from "./pagination-cursor";
|
||||
|
||||
// misc
|
||||
export type {PaginationItemValue} from "@nextui-org/use-pagination";
|
||||
export {PaginationItemType} from "@nextui-org/use-pagination";
|
||||
|
||||
// export hooks
|
||||
export {usePagination} from "./use-pagination";
|
||||
export {usePaginationItem} from "./use-pagination-item";
|
||||
|
||||
// export component
|
||||
export {Pagination, PaginationItem};
|
||||
export {Pagination, PaginationItem, PaginationCursor};
|
||||
|
||||
26
packages/components/pagination/src/pagination-cursor.tsx
Normal file
26
packages/components/pagination/src/pagination-cursor.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
|
||||
export interface PaginationCursorProps extends HTMLNextUIProps<"span"> {
|
||||
/**
|
||||
* The current active page.
|
||||
*/
|
||||
activePage?: number;
|
||||
}
|
||||
|
||||
const PaginationCursor = forwardRef<PaginationCursorProps, "span">((props, ref) => {
|
||||
const {as, activePage, ...otherProps} = props;
|
||||
|
||||
const Component = as || "span";
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
return (
|
||||
<Component ref={domRef} aria-hidden={true} {...otherProps}>
|
||||
{activePage}
|
||||
</Component>
|
||||
);
|
||||
});
|
||||
|
||||
PaginationCursor.displayName = "NextUI.PaginationCursor";
|
||||
|
||||
export default PaginationCursor;
|
||||
@ -1,63 +0,0 @@
|
||||
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} 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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
PaginationHighlight.displayName = "NextUI.PaginationHighlight";
|
||||
|
||||
export default PaginationHighlight;
|
||||
@ -1,53 +1,61 @@
|
||||
import {forwardRef} from "@nextui-org/system";
|
||||
import {PaginationItemParam} from "@nextui-org/use-pagination";
|
||||
import {PaginationItemValue} from "@nextui-org/use-pagination";
|
||||
import {useCallback} from "react";
|
||||
import {DOTS} from "@nextui-org/use-pagination";
|
||||
import {PaginationItemType} from "@nextui-org/use-pagination";
|
||||
|
||||
import {UsePaginationProps, usePagination} from "./use-pagination";
|
||||
import PaginationItem from "./pagination-item";
|
||||
import PaginationCursor from "./pagination-cursor";
|
||||
|
||||
export interface PaginationProps extends UsePaginationProps {}
|
||||
export interface PaginationProps extends Omit<UsePaginationProps, "ref"> {}
|
||||
|
||||
const Pagination = forwardRef<PaginationProps, "ul">((props, ref) => {
|
||||
const {
|
||||
Component,
|
||||
// showControls,
|
||||
dotsJump,
|
||||
loop,
|
||||
// loop,
|
||||
slots,
|
||||
styles,
|
||||
total,
|
||||
range,
|
||||
active,
|
||||
setPage,
|
||||
activePage,
|
||||
// setPage,
|
||||
// onPrevious,
|
||||
// onNext,
|
||||
disableCursor,
|
||||
disableAnimation,
|
||||
renderItem: renderItemProp,
|
||||
getBaseProps,
|
||||
getItemProps,
|
||||
getCursorProps,
|
||||
} = usePagination({ref, ...props});
|
||||
|
||||
const renderItem = useCallback(
|
||||
(value: PaginationItemParam, index: number) => {
|
||||
const isBefore = index < range.indexOf(active);
|
||||
(value: PaginationItemValue, index: number) => {
|
||||
// const isBefore = index < range.indexOf(activePage);
|
||||
|
||||
if (renderItemProp && typeof renderItemProp === "function") {
|
||||
return renderItemProp({
|
||||
value,
|
||||
index,
|
||||
dotsJump,
|
||||
isDots: value === DOTS,
|
||||
isBefore,
|
||||
isActive: value === active,
|
||||
isPrevious: value === active - 1,
|
||||
isNext: value === active + 1,
|
||||
isActive: value === activePage,
|
||||
isPrevious: value === activePage - 1,
|
||||
isNext: value === activePage + 1,
|
||||
isFirst: value === 1,
|
||||
isLast: value === total,
|
||||
className: slots.item({class: styles?.item}),
|
||||
});
|
||||
}
|
||||
if (value === PaginationItemType.PREV) {
|
||||
return <PaginationItem className={slots.prev({class: styles?.prev})}>{"<"}</PaginationItem>;
|
||||
}
|
||||
if (value === PaginationItemType.NEXT) {
|
||||
return <PaginationItem className={slots.next({class: styles?.next})}>{">"}</PaginationItem>;
|
||||
}
|
||||
|
||||
if (value === DOTS) {
|
||||
return <li>...</li>;
|
||||
if (value === PaginationItemType.DOTS) {
|
||||
return <PaginationItem className={slots.item({class: styles?.item})}>...</PaginationItem>;
|
||||
// return (
|
||||
// <PaginationEllipsis
|
||||
// key={`nextui-pagination-item-${value}-${index}`}
|
||||
@ -66,22 +74,17 @@ const Pagination = forwardRef<PaginationProps, "ul">((props, ref) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<PaginationItem
|
||||
key={`${value}-${index}`}
|
||||
className={slots.item({class: styles?.item})}
|
||||
isActive={value === active}
|
||||
value={value}
|
||||
onPress={() => value !== active && setPage(value)}
|
||||
>
|
||||
<PaginationItem key={`${value}-${index}`} {...getItemProps({value})}>
|
||||
{value}
|
||||
</PaginationItem>
|
||||
);
|
||||
},
|
||||
[active, dotsJump, loop, range, renderItemProp, setPage, slots.item, styles?.item, total],
|
||||
[activePage, dotsJump, getItemProps, range, renderItemProp, slots, styles, total],
|
||||
);
|
||||
|
||||
return (
|
||||
<Component {...getBaseProps()}>
|
||||
{!disableCursor && !disableAnimation && <PaginationCursor {...getCursorProps()} />}
|
||||
{range.map(renderItem)}
|
||||
|
||||
{/* {controls && (
|
||||
|
||||
@ -3,8 +3,9 @@ import type {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
|
||||
import type {PressEvent} from "@react-types/shared";
|
||||
|
||||
import {useMemo} from "react";
|
||||
import {DOTS} from "@nextui-org/use-pagination";
|
||||
import {dataAttr} from "@nextui-org/shared-utils";
|
||||
import {PaginationItemType} from "@nextui-org/use-pagination";
|
||||
import {ringClasses} from "@nextui-org/theme";
|
||||
import {clsx, dataAttr, warn} from "@nextui-org/shared-utils";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {usePress} from "@react-aria/interactions";
|
||||
@ -34,7 +35,7 @@ export interface UsePaginationItemProps extends Omit<HTMLNextUIProps<"li">, "onC
|
||||
* @param e MouseEvent
|
||||
* @deprecated Use `onPress` instead.
|
||||
*/
|
||||
onClick?: () => void;
|
||||
onClick?: HTMLNextUIProps<"li">["onClick"];
|
||||
/**
|
||||
* Callback fired when the item is clicked.
|
||||
* @param e PressEvent
|
||||
@ -52,18 +53,18 @@ export interface UsePaginationItemProps extends Omit<HTMLNextUIProps<"li">, "onC
|
||||
const getItemAriaLabel = (page?: string | number) => {
|
||||
if (!page) return;
|
||||
switch (page) {
|
||||
case DOTS:
|
||||
case PaginationItemType.DOTS:
|
||||
return "dots element";
|
||||
case "<":
|
||||
case PaginationItemType.PREV:
|
||||
return "previous page button";
|
||||
case ">":
|
||||
case PaginationItemType.NEXT:
|
||||
return "next page button";
|
||||
case "first":
|
||||
return "first page button";
|
||||
case "last":
|
||||
return "last page button";
|
||||
default:
|
||||
return `${page} item`;
|
||||
return `pagination item ${page}`;
|
||||
}
|
||||
};
|
||||
|
||||
@ -78,6 +79,7 @@ export function usePaginationItem(props: UsePaginationItemProps) {
|
||||
onPress,
|
||||
onClick,
|
||||
getAriaLabel = getItemAriaLabel,
|
||||
className,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
@ -90,7 +92,9 @@ export function usePaginationItem(props: UsePaginationItemProps) {
|
||||
);
|
||||
|
||||
const handlePress = (e: PressEvent) => {
|
||||
onClick?.();
|
||||
if (onClick) {
|
||||
warn("onClick is deprecated, use onPress instead.", "PaginationItem");
|
||||
}
|
||||
onPress?.(e);
|
||||
};
|
||||
|
||||
@ -112,6 +116,7 @@ export function usePaginationItem(props: UsePaginationItemProps) {
|
||||
"data-active": dataAttr(isActive),
|
||||
"data-focus-visible": dataAttr(isFocusVisible),
|
||||
"data-focused": dataAttr(isFocused),
|
||||
className: clsx(isFocusVisible && [...ringClasses], className),
|
||||
...mergeProps(props, pressProps, focusProps, otherProps),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,32 +1,31 @@
|
||||
import type {ReactNode, Ref} from "react";
|
||||
import type {PaginationSlots, PaginationVariantProps, SlotsToClasses} from "@nextui-org/theme";
|
||||
|
||||
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
|
||||
import {
|
||||
PaginationItemParam,
|
||||
usePagination as useBasePagination,
|
||||
import type {Timer} from "@nextui-org/shared-utils";
|
||||
import type {ReactNode, Ref} from "react";
|
||||
import type {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
|
||||
import type {
|
||||
UsePaginationProps as UseBasePaginationProps,
|
||||
PaginationItemValue,
|
||||
} from "@nextui-org/use-pagination";
|
||||
import {useMemo} from "react";
|
||||
|
||||
import {useEffect, useRef, useMemo} from "react";
|
||||
import {mapPropsVariants} from "@nextui-org/system";
|
||||
import {usePagination as useBasePagination} from "@nextui-org/use-pagination";
|
||||
import {pagination} from "@nextui-org/theme";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {clsx, dataAttr} from "@nextui-org/shared-utils";
|
||||
|
||||
export type PaginationItemRenderProps = {
|
||||
value: PaginationItemParam;
|
||||
value: PaginationItemValue;
|
||||
index: number;
|
||||
dotsJump: number;
|
||||
isActive: boolean;
|
||||
isNext: boolean;
|
||||
isPrevious: boolean;
|
||||
isFirst: boolean;
|
||||
isLast: boolean;
|
||||
isDots: boolean;
|
||||
isBefore: boolean;
|
||||
isNext: boolean;
|
||||
isPrevious: boolean;
|
||||
className: string;
|
||||
};
|
||||
|
||||
interface Props extends HTMLNextUIProps<"ul"> {
|
||||
interface Props extends Omit<HTMLNextUIProps<"ul">, "onChange"> {
|
||||
/**
|
||||
* Ref to the DOM node.
|
||||
*/
|
||||
@ -61,7 +60,9 @@ interface Props extends HTMLNextUIProps<"ul"> {
|
||||
* <Pagination styles={{
|
||||
* base:"base-classes",
|
||||
* wrapper: "wrapper-classes",
|
||||
* prev: "prev-classes", // prev button classes
|
||||
* item: "item-classes",
|
||||
* next: "next-classes", // next button classes
|
||||
* cursor: "cursor-classes", // this is the one that moves when an item is selected
|
||||
* }} />
|
||||
* ```
|
||||
@ -71,6 +72,8 @@ interface Props extends HTMLNextUIProps<"ul"> {
|
||||
|
||||
export type UsePaginationProps = Props & UseBasePaginationProps & PaginationVariantProps;
|
||||
|
||||
export const CURSOR_TRANSITION_TIMEOUT = 300; // in ms
|
||||
|
||||
export function usePagination(originalProps: UsePaginationProps) {
|
||||
const [props, variantProps] = mapPropsVariants(originalProps, pagination.variantKeys);
|
||||
|
||||
@ -78,11 +81,11 @@ export function usePagination(originalProps: UsePaginationProps) {
|
||||
as,
|
||||
ref,
|
||||
styles,
|
||||
showControls = true,
|
||||
dotsJump = 5,
|
||||
loop = false,
|
||||
showControls = false,
|
||||
total = 1,
|
||||
initialPage,
|
||||
initialPage = 1,
|
||||
page,
|
||||
siblings,
|
||||
boundaries,
|
||||
@ -95,20 +98,85 @@ export function usePagination(originalProps: UsePaginationProps) {
|
||||
const Component = as || "ul";
|
||||
|
||||
const domRef = useDOMRef(ref);
|
||||
const cursorRef = useRef<HTMLElement>(null);
|
||||
const itemsRef = useRef<Map<number, HTMLElement>>();
|
||||
|
||||
const {range, active, setPage, previous, next, first, last} = useBasePagination({
|
||||
let cursorTimer: Timer;
|
||||
|
||||
function getItemsRefMap() {
|
||||
if (!itemsRef.current) {
|
||||
// Initialize the Map on first usage.
|
||||
itemsRef.current = new Map();
|
||||
}
|
||||
|
||||
return itemsRef.current;
|
||||
}
|
||||
|
||||
function getItemRef(node: HTMLElement, value: number) {
|
||||
const map = getItemsRefMap();
|
||||
|
||||
if (node) {
|
||||
map.set(value, node);
|
||||
} else {
|
||||
map.delete(value);
|
||||
}
|
||||
}
|
||||
|
||||
function scrollTo(value: number) {
|
||||
const map = getItemsRefMap();
|
||||
|
||||
const node = map.get(value);
|
||||
|
||||
// clean up the cursor timer
|
||||
clearTimeout(cursorTimer);
|
||||
|
||||
if (node) {
|
||||
// get position of the item
|
||||
const {offsetLeft} = node;
|
||||
|
||||
// move the cursor to the item
|
||||
if (cursorRef.current) {
|
||||
cursorRef.current.setAttribute("data-moving", "true");
|
||||
cursorRef.current.style.transform = `translateX(${offsetLeft}px) scale(1.1)`;
|
||||
}
|
||||
|
||||
cursorTimer = setTimeout(() => {
|
||||
// reset the scale of the cursor
|
||||
if (cursorRef.current) {
|
||||
cursorRef.current.setAttribute("data-moving", "false");
|
||||
cursorRef.current.style.transform = `translateX(${offsetLeft}px) scale(1)`;
|
||||
}
|
||||
clearTimeout(cursorTimer);
|
||||
}, CURSOR_TRANSITION_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
const {range, activePage, setPage, previous, next, first, last} = useBasePagination({
|
||||
page,
|
||||
total,
|
||||
initialPage,
|
||||
siblings,
|
||||
boundaries,
|
||||
total,
|
||||
showControls,
|
||||
onChange,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (activePage && !originalProps.disableAnimation) {
|
||||
scrollTo(activePage);
|
||||
}
|
||||
}, [
|
||||
activePage,
|
||||
originalProps.disableAnimation,
|
||||
originalProps.isEven,
|
||||
originalProps.disableCursor,
|
||||
]);
|
||||
|
||||
const slots = useMemo(
|
||||
() =>
|
||||
pagination({
|
||||
...variantProps,
|
||||
disableCursor: originalProps.disableCursor || originalProps.disableAnimation,
|
||||
}),
|
||||
[...Object.values(variantProps)],
|
||||
);
|
||||
@ -116,7 +184,7 @@ export function usePagination(originalProps: UsePaginationProps) {
|
||||
const baseStyles = clsx(styles?.base, className);
|
||||
|
||||
const onNext = () => {
|
||||
if (loop && active === total) {
|
||||
if (loop && activePage === total) {
|
||||
return first();
|
||||
}
|
||||
|
||||
@ -124,7 +192,7 @@ export function usePagination(originalProps: UsePaginationProps) {
|
||||
};
|
||||
|
||||
const onPrevious = () => {
|
||||
if (loop && active === 1) {
|
||||
if (loop && activePage === 1) {
|
||||
return last();
|
||||
}
|
||||
|
||||
@ -135,16 +203,40 @@ export function usePagination(originalProps: UsePaginationProps) {
|
||||
return {
|
||||
...props,
|
||||
ref: domRef,
|
||||
role: "navigation",
|
||||
"data-controls": dataAttr(showControls),
|
||||
"data-loop": dataAttr(loop),
|
||||
"data-dots-jump": dotsJump,
|
||||
"data-total": total,
|
||||
"data-active": active,
|
||||
"data-active-page": activePage,
|
||||
className: slots.base({class: baseStyles}),
|
||||
...otherProps,
|
||||
};
|
||||
};
|
||||
|
||||
const getItemProps: PropGetter = (props = {}) => {
|
||||
return {
|
||||
...props,
|
||||
ref: (node) => getItemRef(node, props.value),
|
||||
isActive: props.value === activePage,
|
||||
className: slots.item({class: styles?.item}),
|
||||
onPress: () => {
|
||||
if (props.value !== activePage) {
|
||||
setPage(props.value);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const getCursorProps: PropGetter = (props = {}) => {
|
||||
return {
|
||||
...props,
|
||||
ref: cursorRef,
|
||||
activePage,
|
||||
className: slots.cursor({class: styles?.cursor}),
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
Component,
|
||||
showControls,
|
||||
@ -154,12 +246,16 @@ export function usePagination(originalProps: UsePaginationProps) {
|
||||
loop,
|
||||
total,
|
||||
range,
|
||||
active,
|
||||
activePage,
|
||||
disableCursor: originalProps.disableCursor,
|
||||
disableAnimation: originalProps.disableAnimation,
|
||||
setPage,
|
||||
onPrevious,
|
||||
onNext,
|
||||
renderItem,
|
||||
getBaseProps,
|
||||
getItemProps,
|
||||
getCursorProps,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import {ComponentStory, ComponentMeta} from "@storybook/react";
|
||||
import {pagination} from "@nextui-org/theme";
|
||||
import {button, pagination} from "@nextui-org/theme";
|
||||
|
||||
import {Pagination, PaginationProps} from "../src";
|
||||
|
||||
@ -8,10 +8,25 @@ export default {
|
||||
title: "Components/Pagination",
|
||||
component: Pagination,
|
||||
argTypes: {
|
||||
page: {
|
||||
control: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
siblings: {
|
||||
control: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
boundaries: {
|
||||
control: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
variant: {
|
||||
control: {
|
||||
type: "select",
|
||||
options: ["solid", "bordered", "light", "flat", "faded", "shadow", "dot"],
|
||||
options: ["flat", "bordered", "light", "faded"],
|
||||
},
|
||||
},
|
||||
color: {
|
||||
@ -32,6 +47,11 @@ export default {
|
||||
options: ["xs", "sm", "md", "lg", "xl"],
|
||||
},
|
||||
},
|
||||
showShadow: {
|
||||
control: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
isDisabled: {
|
||||
control: {
|
||||
type: "boolean",
|
||||
@ -43,6 +63,8 @@ export default {
|
||||
const defaultProps = {
|
||||
...pagination.defaultVariants,
|
||||
total: 10,
|
||||
siblings: 1,
|
||||
boundaries: 1,
|
||||
initialPage: 1,
|
||||
};
|
||||
|
||||
@ -55,6 +77,55 @@ Default.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
export const WithControls = Template.bind({});
|
||||
WithControls.args = {
|
||||
...defaultProps,
|
||||
showControls: true,
|
||||
};
|
||||
|
||||
export const InitialPage = Template.bind({});
|
||||
InitialPage.args = {
|
||||
...defaultProps,
|
||||
initialPage: 3,
|
||||
};
|
||||
|
||||
export const IsEven = Template.bind({});
|
||||
IsEven.args = {
|
||||
...defaultProps,
|
||||
isEven: true,
|
||||
};
|
||||
|
||||
export const Controlled = () => {
|
||||
const [currentPage, setCurrentPage] = React.useState(1);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<p>Page: {currentPage}</p>
|
||||
<Pagination
|
||||
{...defaultProps}
|
||||
showShadow
|
||||
color="secondary"
|
||||
page={currentPage}
|
||||
onChange={setCurrentPage}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className={button({color: "secondary", size: "sm", variant: "flat"})}
|
||||
onClick={() => setCurrentPage((prev) => (prev > 1 ? prev - 1 : prev))}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
className={button({color: "secondary", size: "sm", variant: "flat"})}
|
||||
onClick={() => setCurrentPage((prev) => (prev < defaultProps.total ? prev + 1 : prev))}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// import {Grid} from "@nextui-org/grid";
|
||||
|
||||
// import {Pagination} from "../src";
|
||||
|
||||
@ -53,7 +53,7 @@
|
||||
"lodash.foreach": "^4.5.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isempty": "^4.4.0",
|
||||
"tailwind-variants": "^0.0.31",
|
||||
"tailwind-variants": "^0.1.0",
|
||||
"tailwindcss": "^3.2.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@ -1,106 +1,95 @@
|
||||
import {tv, type VariantProps} from "tailwind-variants";
|
||||
|
||||
import {ringClasses} from "../utils";
|
||||
import {colorVariants, ringClasses} from "../utils";
|
||||
|
||||
/**
|
||||
* Pagination wrapper **Tailwind Variants** component
|
||||
*
|
||||
* const {base, item, cursor} = pagination({...})
|
||||
* const {base,cursor, prev, next, item } = pagination({...})
|
||||
*
|
||||
* @example
|
||||
* <ul className={base()} aria-label="pagination navigation">
|
||||
* <li className={cursor()} aria-hidden="true"/> // this marks the active page
|
||||
* <li role="button" className={item()} aria-label="Go to previous page" data-disabled="true">Prev</li>
|
||||
* <li className={cursor()} aria-hidden="true">{active page}</li> // this marks the active page
|
||||
* <li role="button" className={prev()} aria-label="Go to previous page" data-disabled="true">Prev</li>
|
||||
* <li role="button" className={item()} aria-label="page 1" data-active="true">1</li>
|
||||
* <li role="button" className={item()} aria-label="page 2">2</li>
|
||||
* <li role="button" className={item()} aria-hidden="true">...</li>
|
||||
* <li role="button" className={item()} aria-label="page 10">10</li>
|
||||
* <li role="button" className={item()} aria-label="Go to next page">Next</li>
|
||||
* <li role="button" className={next()} aria-label="Go to next page">Next</li>
|
||||
* </ul>
|
||||
*/
|
||||
const pagination = tv({
|
||||
slots: {
|
||||
base: "flex gap-1",
|
||||
item: [
|
||||
base: "flex flex-wrap relative gap-1 max-w-fit",
|
||||
item: "",
|
||||
prev: "",
|
||||
next: "",
|
||||
cursor: [
|
||||
"absolute",
|
||||
"flex",
|
||||
"overflow-visible",
|
||||
"items-center",
|
||||
"justify-center",
|
||||
"bg-neutral-100",
|
||||
"hover:bg-neutral-200",
|
||||
"text-neutral-contrastText",
|
||||
"origin-center",
|
||||
"left-0",
|
||||
],
|
||||
cursor: "",
|
||||
},
|
||||
variants: {
|
||||
variant: {
|
||||
solid: {},
|
||||
bordered: {
|
||||
item: "border-1.5 !bg-transparent",
|
||||
item: ["border-1.5", "border-neutral", "bg-transparent", "hover:bg-neutral-100"],
|
||||
},
|
||||
light: {
|
||||
item: "!bg-transparent",
|
||||
item: "bg-transparent",
|
||||
},
|
||||
flat: {},
|
||||
faded: {
|
||||
item: "border-1.5",
|
||||
},
|
||||
shadow: {},
|
||||
},
|
||||
color: {
|
||||
neutral: {},
|
||||
primary: {},
|
||||
secondary: {},
|
||||
success: {},
|
||||
warning: {},
|
||||
danger: {},
|
||||
neutral: {
|
||||
cursor: colorVariants.solid.neutral,
|
||||
},
|
||||
primary: {
|
||||
cursor: colorVariants.solid.primary,
|
||||
},
|
||||
secondary: {
|
||||
cursor: colorVariants.solid.secondary,
|
||||
},
|
||||
success: {
|
||||
cursor: colorVariants.solid.success,
|
||||
},
|
||||
warning: {
|
||||
cursor: colorVariants.solid.warning,
|
||||
},
|
||||
danger: {
|
||||
cursor: colorVariants.solid.danger,
|
||||
},
|
||||
},
|
||||
size: {
|
||||
xs: {
|
||||
item: "w-7 h-7 text-xs",
|
||||
},
|
||||
sm: {
|
||||
item: "w-8 h-8 text-sm",
|
||||
},
|
||||
md: {
|
||||
item: "w-9 h-9 text-sm",
|
||||
},
|
||||
lg: {
|
||||
item: "w-10 h-10 text-base",
|
||||
},
|
||||
xl: {
|
||||
item: "w-11 h-11 text-lg",
|
||||
},
|
||||
xs: {},
|
||||
sm: {},
|
||||
md: {},
|
||||
lg: {},
|
||||
xl: {},
|
||||
},
|
||||
radius: {
|
||||
none: {
|
||||
item: "rounded-none",
|
||||
},
|
||||
base: {
|
||||
item: "rounded",
|
||||
},
|
||||
sm: {
|
||||
item: "rounded-sm",
|
||||
},
|
||||
md: {
|
||||
item: "rounded-md",
|
||||
},
|
||||
lg: {
|
||||
item: "rounded-lg",
|
||||
},
|
||||
xl: {
|
||||
item: "rounded-xl",
|
||||
},
|
||||
full: {
|
||||
item: "rounded-full",
|
||||
},
|
||||
none: {},
|
||||
base: {},
|
||||
sm: {},
|
||||
md: {},
|
||||
lg: {},
|
||||
xl: {},
|
||||
full: {},
|
||||
},
|
||||
isEven: {
|
||||
true: {
|
||||
base: "gap-0",
|
||||
item: [
|
||||
"first:rounded-r-none",
|
||||
"last:rounded-l-none",
|
||||
"[&:not(:first-child):not(:last-child)]:rounded-none",
|
||||
"first-of-type:rounded-r-none",
|
||||
"last-of-type:rounded-l-none",
|
||||
"[&:not(:first-of-type):not(:last-of-type)]:rounded-none",
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -114,22 +103,289 @@ const pagination = tv({
|
||||
wrapper: [...ringClasses],
|
||||
},
|
||||
},
|
||||
showShadow: {
|
||||
true: {},
|
||||
},
|
||||
disableAnimation: {
|
||||
true: {},
|
||||
false: {
|
||||
item: "transition-background",
|
||||
cursor: ["transition-transform", "!duration-300"],
|
||||
},
|
||||
},
|
||||
disableCursor: {
|
||||
true: {
|
||||
cursor: "hidden",
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "solid",
|
||||
variant: "flat",
|
||||
color: "primary",
|
||||
size: "md",
|
||||
radius: "xl",
|
||||
isEven: false,
|
||||
isDisabled: false,
|
||||
showShadow: false,
|
||||
disableAnimation: false,
|
||||
disableCursor: false,
|
||||
},
|
||||
compoundVariants: [
|
||||
// showShadow / color
|
||||
{
|
||||
showShadow: true,
|
||||
color: "neutral",
|
||||
class: {
|
||||
cursor: colorVariants.shadow.neutral,
|
||||
},
|
||||
},
|
||||
{
|
||||
showShadow: true,
|
||||
color: "primary",
|
||||
class: {
|
||||
cursor: colorVariants.shadow.primary,
|
||||
},
|
||||
},
|
||||
{
|
||||
showShadow: true,
|
||||
color: "secondary",
|
||||
class: {
|
||||
cursor: colorVariants.shadow.secondary,
|
||||
},
|
||||
},
|
||||
{
|
||||
showShadow: true,
|
||||
color: "success",
|
||||
class: {
|
||||
cursor: colorVariants.shadow.success,
|
||||
},
|
||||
},
|
||||
{
|
||||
showShadow: true,
|
||||
color: "warning",
|
||||
class: {
|
||||
cursor: colorVariants.shadow.warning,
|
||||
},
|
||||
},
|
||||
{
|
||||
showShadow: true,
|
||||
color: "danger",
|
||||
class: {
|
||||
cursor: colorVariants.shadow.danger,
|
||||
},
|
||||
},
|
||||
// isEven / bordered
|
||||
{
|
||||
isEven: true,
|
||||
variant: "bordered",
|
||||
class: {
|
||||
item: "[&:not(:first-of-type)]:border-l-0",
|
||||
},
|
||||
},
|
||||
/**
|
||||
* --------------------------------------------------------
|
||||
* disableCursor
|
||||
* the styles will be applied to the active item
|
||||
* --------------------------------------------------------
|
||||
*/
|
||||
// disableCursor / color
|
||||
{
|
||||
disableCursor: true,
|
||||
color: "neutral",
|
||||
class: {
|
||||
item: [
|
||||
"data-[active=true]:bg-neutral-400",
|
||||
"data-[active=true]:border-neutral-400",
|
||||
"data-[active=true]:text-neutral-contrastText",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
disableCursor: true,
|
||||
color: "primary",
|
||||
class: {
|
||||
item: [
|
||||
"data-[active=true]:bg-primary",
|
||||
"data-[active=true]:border-primary",
|
||||
"data-[active=true]:text-primary-contrastText",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
disableCursor: true,
|
||||
color: "secondary",
|
||||
class: {
|
||||
item: [
|
||||
"data-[active=true]:bg-secondary",
|
||||
"data-[active=true]:border-secondary",
|
||||
"data-[active=true]:text-secondary-contrastText",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
disableCursor: true,
|
||||
color: "success",
|
||||
class: {
|
||||
item: [
|
||||
"data-[active=true]:bg-success",
|
||||
"data-[active=true]:border-success",
|
||||
"data-[active=true]:text-success-contrastText",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
disableCursor: true,
|
||||
color: "warning",
|
||||
class: {
|
||||
item: [
|
||||
"data-[active=true]:bg-warning",
|
||||
"data-[active=true]:border-warning",
|
||||
"data-[active=true]:text-warning-contrastText",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
disableCursor: true,
|
||||
color: "danger",
|
||||
class: {
|
||||
item: [
|
||||
"data-[active=true]:bg-danger",
|
||||
"data-[active=true]:border-danger",
|
||||
"data-[active=true]:text-danger-contrastText",
|
||||
],
|
||||
},
|
||||
},
|
||||
// shadow / color
|
||||
{
|
||||
disableCursor: true,
|
||||
showShadow: true,
|
||||
color: "neutral",
|
||||
class: {
|
||||
item: ["data-[active=true]:shadow-lg", "data-[active=true]:shadow-neutral/50"],
|
||||
},
|
||||
},
|
||||
{
|
||||
disableCursor: true,
|
||||
showShadow: true,
|
||||
color: "primary",
|
||||
class: {
|
||||
item: ["data-[active=true]:shadow-lg", "data-[active=true]:shadow-primary/40"],
|
||||
},
|
||||
},
|
||||
{
|
||||
disableCursor: true,
|
||||
showShadow: true,
|
||||
color: "secondary",
|
||||
class: {
|
||||
item: ["data-[active=true]:shadow-lg", "data-[active=true]:shadow-secondary/40"],
|
||||
},
|
||||
},
|
||||
{
|
||||
disableCursor: true,
|
||||
showShadow: true,
|
||||
color: "success",
|
||||
class: {
|
||||
item: ["data-[active=true]:shadow-lg", "data-[active=true]:shadow-success/40"],
|
||||
},
|
||||
},
|
||||
{
|
||||
disableCursor: true,
|
||||
showShadow: true,
|
||||
color: "warning",
|
||||
class: {
|
||||
item: ["data-[active=true]:shadow-lg", "data-[active=true]:shadow-warning/40"],
|
||||
},
|
||||
},
|
||||
{
|
||||
disableCursor: true,
|
||||
showShadow: true,
|
||||
color: "danger",
|
||||
class: {
|
||||
item: ["data-[active=true]:shadow-lg", "data-[active=true]:shadow-danger/40"],
|
||||
},
|
||||
},
|
||||
],
|
||||
compoundSlots: [
|
||||
// without variant
|
||||
{
|
||||
slots: ["item", "prev", "next"],
|
||||
class: [
|
||||
"flex",
|
||||
"flex-wrap",
|
||||
"truncate",
|
||||
"box-border",
|
||||
"outline-none",
|
||||
"items-center",
|
||||
"justify-center",
|
||||
"bg-neutral-100",
|
||||
"hover:bg-neutral-200",
|
||||
"active:bg-neutral-300",
|
||||
"text-neutral-contrastText",
|
||||
],
|
||||
},
|
||||
// size
|
||||
{
|
||||
slots: ["item", "cursor", "prev", "next"],
|
||||
size: "xs",
|
||||
class: "w-7 h-7 text-xs",
|
||||
},
|
||||
{
|
||||
slots: ["item", "cursor", "prev", "next"],
|
||||
size: "sm",
|
||||
class: "w-8 h-8 text-sm",
|
||||
},
|
||||
{
|
||||
slots: ["item", "cursor", "prev", "next"],
|
||||
size: "md",
|
||||
class: "w-9 h-9 text-sm",
|
||||
},
|
||||
{
|
||||
slots: ["item", "cursor", "prev", "next"],
|
||||
size: "lg",
|
||||
class: "w-10 h-10 text-base",
|
||||
},
|
||||
{
|
||||
slots: ["item", "cursor", "prev", "next"],
|
||||
size: "xl",
|
||||
class: "w-11 h-11 text-base",
|
||||
},
|
||||
// radius
|
||||
{
|
||||
slots: ["item", "cursor", "prev", "next"],
|
||||
radius: "none",
|
||||
class: "rounded-none",
|
||||
},
|
||||
{
|
||||
slots: ["item", "cursor", "prev", "next"],
|
||||
radius: "base",
|
||||
class: "rounded-base",
|
||||
},
|
||||
{
|
||||
slots: ["item", "cursor", "prev", "next"],
|
||||
radius: "sm",
|
||||
class: "rounded-sm",
|
||||
},
|
||||
{
|
||||
slots: ["item", "cursor", "prev", "next"],
|
||||
radius: "md",
|
||||
class: "rounded",
|
||||
},
|
||||
{
|
||||
slots: ["item", "cursor", "prev", "next"],
|
||||
radius: "lg",
|
||||
class: "rounded-lg",
|
||||
},
|
||||
{
|
||||
slots: ["item", "cursor", "prev", "next"],
|
||||
radius: "xl",
|
||||
class: "rounded-xl",
|
||||
},
|
||||
{
|
||||
slots: ["item", "cursor", "prev", "next"],
|
||||
radius: "full",
|
||||
class: "rounded-full",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export type PaginationVariantProps = VariantProps<typeof pagination>;
|
||||
|
||||
@ -14,6 +14,7 @@ import {semanticColors, commonColors} from "./colors";
|
||||
import {animations} from "./animations";
|
||||
import {utilities} from "./utilities";
|
||||
import {removeDefaultKeys} from "./utils/object";
|
||||
import {baseStyles} from "./utils/styles";
|
||||
|
||||
interface MaybeNested<K extends keyof any = string, V = string> {
|
||||
[key: string]: V | MaybeNested<K, V>;
|
||||
@ -141,7 +142,13 @@ const corePlugin = (config: ConfigObject | ConfigFunction = {}) => {
|
||||
const resolved = resolveConfig(config);
|
||||
|
||||
return plugin(
|
||||
({addUtilities, addVariant}) => {
|
||||
({addBase, addUtilities, addVariant}) => {
|
||||
// add base styles
|
||||
addBase({
|
||||
[":root, [data-theme]"]: {
|
||||
...baseStyles,
|
||||
},
|
||||
});
|
||||
// add the css variables to "@layer utilities"
|
||||
addUtilities({...resolved.utilities, ...utilities});
|
||||
// add the theme as variant e.g. "theme-[name]:text-2xl"
|
||||
|
||||
@ -1,3 +1,12 @@
|
||||
/**
|
||||
* This is the base styles for all elements.
|
||||
* Is meant to be used with the `addBase` method from tailwindcss.
|
||||
*/
|
||||
export const baseStyles = {
|
||||
color: "hsl(var(--nextui-foreground))",
|
||||
backgroundColor: "hsl(var(--nextui-background))",
|
||||
};
|
||||
|
||||
/**
|
||||
* focus styles when the element is focused by keyboard.
|
||||
*/
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
import {useMemo, useCallback, useState, useEffect} from "react";
|
||||
import {range} from "@nextui-org/shared-utils";
|
||||
|
||||
export enum PaginationItemType {
|
||||
DOTS = "dots",
|
||||
PREV = "prev",
|
||||
NEXT = "next",
|
||||
}
|
||||
|
||||
export interface UsePaginationProps {
|
||||
/**
|
||||
* The total number of pages.
|
||||
@ -25,17 +31,29 @@ export interface UsePaginationProps {
|
||||
* @default 1
|
||||
*/
|
||||
boundaries?: number;
|
||||
/**
|
||||
* If `true`, the range will include "prev" and "next" buttons.
|
||||
* @default false
|
||||
*/
|
||||
showControls?: boolean;
|
||||
/**
|
||||
* Callback fired when the page changes.
|
||||
*/
|
||||
onChange?: (page: number) => void;
|
||||
}
|
||||
|
||||
export const DOTS = "dots";
|
||||
export type PaginationItemParam = number | typeof DOTS;
|
||||
export type PaginationItemValue = number | PaginationItemType;
|
||||
|
||||
export function usePagination(props: UsePaginationProps) {
|
||||
const {page, total, siblings = 1, boundaries = 1, initialPage = 1, onChange} = props;
|
||||
const {
|
||||
page,
|
||||
total,
|
||||
siblings = 1,
|
||||
boundaries = 1,
|
||||
initialPage = 1,
|
||||
showControls = false,
|
||||
onChange,
|
||||
} = props;
|
||||
const [activePage, setActivePage] = useState(page || initialPage);
|
||||
|
||||
const onChangeActivePage = (newPage: number) => {
|
||||
@ -67,11 +85,22 @@ export function usePagination(props: UsePaginationProps) {
|
||||
const first = () => setPage(1);
|
||||
const last = () => setPage(total);
|
||||
|
||||
const paginationRange = useMemo((): PaginationItemParam[] => {
|
||||
const formatRange = useCallback(
|
||||
(range: PaginationItemValue[]) => {
|
||||
if (showControls) {
|
||||
return [PaginationItemType.PREV, ...range, PaginationItemType.NEXT];
|
||||
}
|
||||
|
||||
return range;
|
||||
},
|
||||
[showControls],
|
||||
);
|
||||
|
||||
const paginationRange = useMemo((): PaginationItemValue[] => {
|
||||
const totalPageNumbers = siblings * 2 + 3 + boundaries * 2;
|
||||
|
||||
if (totalPageNumbers >= total) {
|
||||
return range(1, total);
|
||||
return formatRange(range(1, total));
|
||||
}
|
||||
const leftSiblingIndex = Math.max(activePage - siblings, boundaries);
|
||||
const rightSiblingIndex = Math.min(activePage + siblings, total - boundaries);
|
||||
@ -87,27 +116,35 @@ export function usePagination(props: UsePaginationProps) {
|
||||
if (!shouldShowLeftDots && shouldShowRightDots) {
|
||||
const leftItemCount = siblings * 2 + boundaries + 2;
|
||||
|
||||
return [...range(1, leftItemCount), DOTS, ...range(total - (boundaries - 1), total)];
|
||||
return formatRange([
|
||||
...range(1, leftItemCount),
|
||||
PaginationItemType.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 formatRange([
|
||||
...range(1, boundaries),
|
||||
PaginationItemType.DOTS,
|
||||
...range(total - rightItemCount, total),
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
return formatRange([
|
||||
...range(1, boundaries),
|
||||
DOTS,
|
||||
PaginationItemType.DOTS,
|
||||
...range(leftSiblingIndex, rightSiblingIndex),
|
||||
DOTS,
|
||||
PaginationItemType.DOTS,
|
||||
...range(total - boundaries + 1, total),
|
||||
];
|
||||
}, [total, siblings, activePage]);
|
||||
]);
|
||||
}, [total, activePage, siblings, boundaries, formatRange]);
|
||||
|
||||
return {
|
||||
range: paginationRange,
|
||||
active: activePage,
|
||||
activePage,
|
||||
setPage,
|
||||
next,
|
||||
previous,
|
||||
|
||||
@ -13,7 +13,6 @@ export const decorators = [
|
||||
];
|
||||
|
||||
export const parameters = {
|
||||
viewMode: 'docs',
|
||||
actions: {argTypesRegex: "^on[A-Z].*"},
|
||||
controls: {
|
||||
matchers: {
|
||||
|
||||
@ -33,9 +33,6 @@
|
||||
"prepack": "clean-package",
|
||||
"postpack": "clean-package restore"
|
||||
},
|
||||
"dependencies": {
|
||||
"deepmerge": "4.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18"
|
||||
},
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
export {default as deepMerge} from "deepmerge";
|
||||
|
||||
export * from "./assertion";
|
||||
export * from "./refs";
|
||||
export * from "./clsx";
|
||||
@ -13,3 +11,4 @@ export * from "./context";
|
||||
export * from "./numbers";
|
||||
export * from "./console";
|
||||
export * from "./react";
|
||||
export * from "./types";
|
||||
|
||||
1
packages/utilities/shared-utils/src/types.ts
Normal file
1
packages/utilities/shared-utils/src/types.ts
Normal file
@ -0,0 +1 @@
|
||||
export type Timer = ReturnType<typeof setTimeout>;
|
||||
430
pnpm-lock.yaml
generated
430
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user