[ui][pagination] implementation

pull/314/head
Yangshun Tay 2 years ago
parent e0a3f4c15c
commit 641a565e5c

@ -0,0 +1,234 @@
import React, { useState } from 'react';
import type { ComponentMeta } from '@storybook/react';
import { Pagination } from '@tih/ui';
export default {
argTypes: {},
component: Pagination,
title: 'Pagination',
} as ComponentMeta<typeof Pagination>;
// eslint-disable-next-line @typescript-eslint/no-empty-function
function emptyFunction() {}
export function Basic({
current,
end,
start,
pagePadding,
}: Pick<
React.ComponentProps<typeof Pagination>,
'current' | 'end' | 'pagePadding' | 'start'
>) {
return (
<div className="space-y-4">
<Pagination
current={current}
end={end}
label="Pagination"
pagePadding={pagePadding}
start={start}
onSelect={emptyFunction}
/>
</div>
);
}
Basic.args = {
current: 3,
end: 10,
pagePadding: 1,
start: 1,
};
export function Interaction() {
const [currentPage, setCurrentPage] = useState(5);
return (
<div className="space-y-4">
<div>
<Pagination
current={currentPage}
end={10}
label="Pagination"
start={1}
onSelect={(page) => setCurrentPage(page)}
/>
</div>
</div>
);
}
export function PageRanges() {
return (
<div className="space-y-4">
<div>
<Pagination
current={5}
end={10}
label="Pagination"
start={1}
onSelect={emptyFunction}
/>
</div>
<div>
<Pagination
current={1}
end={10}
label="Pagination"
start={1}
onSelect={emptyFunction}
/>
</div>
<div>
<Pagination
current={2}
end={10}
label="Pagination"
start={1}
onSelect={emptyFunction}
/>
</div>
<div>
<Pagination
current={9}
end={10}
label="Pagination"
start={1}
onSelect={emptyFunction}
/>
</div>
<div>
<Pagination
current={10}
end={10}
label="Pagination"
start={1}
onSelect={emptyFunction}
/>
</div>
<div>
<Pagination
current={1}
end={1}
label="Pagination"
start={1}
onSelect={emptyFunction}
/>
</div>
<div>
<Pagination
current={1}
end={1}
label="Pagination"
pagePadding={2}
start={1}
onSelect={emptyFunction}
/>
</div>
<div>
<Pagination
current={1}
end={2}
label="Pagination"
start={1}
onSelect={emptyFunction}
/>
</div>
<div>
<Pagination
current={2}
end={2}
label="Pagination"
start={1}
onSelect={emptyFunction}
/>
</div>
<div>
<Pagination
current={1}
end={3}
label="Pagination"
start={1}
onSelect={emptyFunction}
/>
</div>
<div>
<Pagination
current={2}
end={3}
label="Pagination"
start={1}
onSelect={emptyFunction}
/>
</div>
</div>
);
}
export function PagePadding() {
return (
<div className="space-y-4">
<div>
<Pagination
current={5}
end={10}
label="Pagination"
pagePadding={2}
start={1}
onSelect={emptyFunction}
/>
</div>
<div>
<Pagination
current={5}
end={20}
label="Pagination"
pagePadding={2}
start={1}
onSelect={emptyFunction}
/>
</div>
<div>
<Pagination
current={10}
end={20}
label="Pagination"
pagePadding={2}
start={1}
onSelect={emptyFunction}
/>
</div>
<div>
<Pagination
current={10}
end={20}
label="Pagination"
pagePadding={3}
start={1}
onSelect={emptyFunction}
/>
</div>
<div>
<Pagination
current={5}
end={10}
label="Pagination"
pagePadding={3}
start={1}
onSelect={emptyFunction}
/>
</div>
<div>
<Pagination
current={1}
end={1}
label="Pagination"
pagePadding={2}
start={1}
onSelect={emptyFunction}
/>
</div>
</div>
);
}

@ -0,0 +1,142 @@
import clsx from 'clsx';
import type { ReactElement } from 'react';
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/20/solid';
type Props = Readonly<{
current: number;
end: number;
label: string;
onSelect: (page: number, event: React.MouseEvent<HTMLElement>) => void;
pagePadding?: number;
start: number;
}>;
function PaginationPage({
isCurrent = false,
label,
onClick,
}: Readonly<{
isCurrent?: boolean;
label: number;
onClick: (event: React.MouseEvent<HTMLElement>) => void;
}>) {
return (
<button
aria-current={isCurrent}
className={clsx(
'focus:ring-primary-500 focus:border-primary-500 relative inline-flex items-center border px-4 py-2 text-sm font-medium focus:z-20 focus:outline-none focus:ring-1',
isCurrent
? 'border-primary-500 bg-primary-50 text-primary-600 z-10'
: 'border-slate-300 bg-white text-slate-500 hover:bg-slate-50',
)}
disabled={isCurrent}
type="button"
onClick={onClick}>
{label}
</button>
);
}
function PaginationEllipsis() {
return (
<span className="relative inline-flex items-center border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700">
...
</span>
);
}
export default function Pagination({
current,
end,
label,
onSelect,
pagePadding = 1,
start = 1,
}: Props) {
const pageNumberSet = new Set();
const pageNumberList: Array<number | string> = [];
const elements: Array<ReactElement> = [];
let lastAddedPage = 0;
function addPage(page: number) {
if (page < start || page > end) {
return;
}
if (!pageNumberSet.has(page)) {
lastAddedPage = page;
pageNumberList.push(page);
pageNumberSet.add(page);
elements.push(
<PaginationPage
isCurrent={current === page}
label={page}
onClick={(event) => {
onSelect(page, event);
}}
/>,
);
}
}
for (let i = start; i <= start + pagePadding; i++) {
addPage(i);
}
if (lastAddedPage < current - pagePadding) {
elements.push(<PaginationEllipsis />);
}
for (let i = current - pagePadding; i <= current + pagePadding; i++) {
addPage(i);
}
if (lastAddedPage < end - pagePadding) {
elements.push(<PaginationEllipsis />);
}
for (let i = end - pagePadding; i <= end; i++) {
addPage(i);
}
const isPrevButtonDisabled = current === start;
const isNextButtonDisabled = current === end;
return (
<nav
aria-label={label}
className="isolate inline-flex -space-x-px rounded-md shadow-sm">
<button
aria-label="Previous"
className={clsx(
'relative inline-flex items-center rounded-l-md border border-slate-300 px-2 py-2 text-sm font-medium focus:z-20',
isPrevButtonDisabled
? 'text-slate-300'
: 'focus:ring-primary-500 focus:border-primary-500 bg-white text-slate-500 hover:bg-slate-50 focus:outline-none focus:ring-1',
)}
disabled={isPrevButtonDisabled}
type="button"
onClick={(event) => {
onSelect(current - 1, event);
}}>
<ChevronLeftIcon aria-hidden="true" className="h-5 w-5" />
</button>
{elements}
<button
aria-label="Next"
className={clsx(
'relative inline-flex items-center rounded-r-md border border-slate-300 px-2 py-2 text-sm font-medium focus:z-20',
isNextButtonDisabled
? 'text-slate-300'
: 'focus:ring-primary-500 focus:border-primary-500 bg-white text-slate-500 hover:bg-slate-50 focus:outline-none focus:ring-1',
)}
disabled={isNextButtonDisabled}
type="button"
onClick={(event) => {
onSelect(current + 1, event);
}}>
<ChevronRightIcon aria-hidden="true" className="h-5 w-5" />
</button>
</nav>
);
}

@ -19,6 +19,9 @@ export { default as DropdownMenu } from './DropdownMenu/DropdownMenu';
// HorizontalDivider
export * from './HorizontalDivider/HorizontalDivider';
export { default as HorizontalDivider } from './HorizontalDivider/HorizontalDivider';
// Pagination
export * from './Pagination/Pagination';
export { default as Pagination } from './Pagination/Pagination';
// Select
export * from './Select/Select';
export { default as Select } from './Select/Select';

Loading…
Cancel
Save