Skip to content

Commit

Permalink
feat(Pagination): add basic feature and unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
shervinchen committed May 9, 2024
1 parent 063d6b2 commit 0e2851a
Show file tree
Hide file tree
Showing 13 changed files with 441 additions and 77 deletions.
6 changes: 6 additions & 0 deletions demo/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import {
DemoModalNotCloseOnOverlayClick,
DemoModalWidth,
} from './modal';
import { DemoPaginationDefault } from './pagination';

const Container: FC<PropsWithChildren<{ title: string }>> = ({
title,
Expand Down Expand Up @@ -325,6 +326,11 @@ function App() {
<DemoModalWidth />
</Wrapper>
</Container>
<Container title="Pagination">
<Wrapper title="Default">
<DemoPaginationDefault />
</Wrapper>
</Container>
</div>
);
}
Expand Down
10 changes: 10 additions & 0 deletions demo/pagination/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Unit from '../Unit';
import { Pagination } from '@/packages';

export function DemoPaginationDefault() {
return (
<Unit layout="row">
<Pagination defaultPage={1} count={124} limit={10} />
</Unit>
);
}
70 changes: 7 additions & 63 deletions packages/BaseStyle/BaseStyle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,81 +9,25 @@ const BaseStyle: FC<PropsWithChildren<unknown>> = ({ children }) => {
{children}
<style jsx global>
{`
html,
body {
background-color: ${theme.palette.background};
color: ${theme.palette.foreground};
}
html {
font-size: 16px;
}
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
margin: 0;
font-size: 1rem;
line-height: 1.5;
margin: 0;
padding: 0;
min-height: 100%;
position: relative;
overflow-x: hidden;
background-color: ${theme.palette.background};
color: ${theme.palette.foreground};
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI',
'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
-webkit-text-size-adjust: 100%;
}
*,
*:before,
*:after {
box-sizing: border-box;
text-rendering: geometricPrecision;
-webkit-tap-highlight-color: transparent;
}
span {
font-size: inherit;
color: inherit;
font-weight: inherit;
}
img {
max-width: 100%;
}
a {
cursor: pointer;
font-size: inherit;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-box-align: center;
align-items: center;
color: inherit;
text-decoration: none;
}
a:hover {
text-decoration: none;
}
button,
input,
select,
textarea {
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: inherit;
margin: 0;
}
button:focus,
input:focus,
select:focus,
textarea:focus {
outline: none;
}
`}
</style>
Expand Down
50 changes: 50 additions & 0 deletions packages/Pagination/Pagination.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import css from 'styled-jsx/css';
import { useTheme } from '../Theme';

export const usePaginationItemStyles = () => {
const theme = useTheme();

return css.resolve`
.raw-pagination-item {
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
min-width: 40px;
height: 40px;
padding: 0 8px;
border: 1px solid ${theme.palette.accents2};
border-radius: 6px;
font-size: 14px;
color: ${theme.palette.accents7};
background-color: ${theme.palette.background};
transition-property: border-color, background, color;
transition-duration: 0.2s;
transition-timing-function: ease;
user-select: none;
text-decoration: none;
cursor: pointer;
}
.raw-pagination-item-disabled {
background-color: ${theme.palette.accents2};
color: ${theme.palette.accents5};
cursor: not-allowed;
}
.raw-pagination-item.raw-pagination-item-active {
background-color: ${theme.palette.foreground};
border-color: ${theme.palette.foreground};
color: ${theme.palette.background};
}
.raw-pagination-item:not(.raw-pagination-item-active):not(
.raw-pagination-item-disabled
):hover {
border-color: ${theme.palette.foreground};
color: ${theme.palette.foreground};
}
.raw-pagination-item:not(.raw-pagination-item-active):not(
.raw-pagination-item-disabled
):active {
background-color: ${theme.palette.accents2};
}
`;
};
136 changes: 129 additions & 7 deletions packages/Pagination/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,138 @@
import React, { FC, PropsWithChildren } from 'react';
import React, { FC, useMemo } from 'react';
import { ChevronLeft, ChevronRight } from 'react-feather';
import classNames from 'classnames';
import { PaginationProps } from './Pagination.types';
import { useControlled } from '../utils/hooks';
import PaginationPages from './PaginationPages';
import { usePaginationItemStyles } from './Pagination.styles';

const Pagination: FC<PropsWithChildren<PaginationProps>> = ({
const range = (start: number, end: number) => {
const length = end - start + 1;
return Array.from({ length }, (_, i) => start + i);
};

const Pagination: FC<PaginationProps> = ({
page,
defaultPage,
count,
limit,
defaultPage = 1,
count = 1,
limit = 10,
onChange,
children,
className = '',
...restProps
}) => {
return <div {...restProps}></div>;
const [internalPage, setInternalPage] = useControlled<number>({
defaultValue: defaultPage,
value: page,
});
const classes = classNames('raw-pagination', className);
const { className: paginationItemClassName, styles: paginationItemStyles } =
usePaginationItemStyles();
const pageCount = Math.ceil(count / limit);
const boundaryCount = 5;
const startPages = range(1, boundaryCount);
const endPages = range(pageCount - boundaryCount + 1, pageCount);

const pages: number[] = useMemo(() => {
return [
...new Set(
[
...(internalPage < boundaryCount ? startPages : [1]),
internalPage,
internalPage - 1,
internalPage + 1,
...(internalPage > pageCount - boundaryCount + 1
? endPages
: [pageCount]),
]
.filter((value) => value >= 1 && value <= pageCount)
.sort((a, b) => a - b)
),
].reduce<number[]>((previousValue, currentValue, currentIndex, array) => {
previousValue.push(currentValue);
if (
array[currentIndex + 1] &&
array[currentIndex + 1] - array[currentIndex] > 1
) {
previousValue.push(0);
}
return previousValue;
}, []);
}, [internalPage, pageCount, startPages, endPages]);

const clickPageNumberHandler = (pageNumber: number) => {
setInternalPage(pageNumber);
onChange?.(pageNumber);
};

const clickPreviousPageHandler = () => {
if (internalPage === 1) {
return;
}
clickPageNumberHandler(internalPage - 1);
};

const clickNextPageHandler = () => {
if (internalPage === pageCount) {
return;
}
clickPageNumberHandler(internalPage + 1);
};

return (
<nav
role="navigation"
aria-label="pagination"
className={classes}
{...restProps}
>
<ul className="raw-pagination-list">
<li>
<a
className={classNames(
'raw-pagination-item',
internalPage === 1 && 'raw-pagination-item-disabled',
paginationItemClassName
)}
aria-label="Go to previous page"
onClick={clickPreviousPageHandler}
>
<ChevronLeft size={16} />
</a>
</li>
<PaginationPages
pages={pages}
internalPage={internalPage}
clickPageNumberHandler={clickPageNumberHandler}
/>
<li>
<a
className={classNames(
'raw-pagination-item',
internalPage === pageCount && 'raw-pagination-item-disabled',
paginationItemClassName
)}
aria-label="Go to next page"
onClick={clickNextPageHandler}
>
<ChevronRight size={16} />
</a>
</li>
</ul>
{paginationItemStyles}
<style jsx>
{`
.raw-pagination-list {
display: flex;
align-items: center;
gap: 8px;
list-style: none;
padding: 0;
margin: 0;
}
`}
</style>
</nav>
);
};

export default Pagination;
7 changes: 7 additions & 0 deletions packages/Pagination/Pagination.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface BasePaginationProps {
defaultPage?: number;
count?: number;
limit?: number;
className?: string;
onChange?: (page: number) => void;
}

Expand All @@ -14,3 +15,9 @@ type NativePaginationProps = Omit<
>;

export type PaginationProps = BasePaginationProps & NativePaginationProps;

export interface PaginationPagesProps {
pages: number[];
internalPage: number;
clickPageNumberHandler: (page: number) => void;
}
59 changes: 59 additions & 0 deletions packages/Pagination/PaginationPages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, { FC } from 'react';
import { MoreHorizontal } from 'react-feather';
import classNames from 'classnames';
import { PaginationPagesProps } from './Pagination.types';
import { useTheme } from '../Theme';
import { usePaginationItemStyles } from './Pagination.styles';

const PaginationPages: FC<PaginationPagesProps> = ({
pages,
internalPage,
clickPageNumberHandler,
}) => {
const theme = useTheme();
const { className: paginationItemClassName, styles: paginationItemStyles } =
usePaginationItemStyles();

return (
<>
{pages.map((pageItem, index) => {
const isCurrentPage = pageItem === internalPage;
return (
<li key={index}>
{pageItem !== 0 ? (
<a
className={classNames(
'raw-pagination-item',
isCurrentPage && 'raw-pagination-item-active',
paginationItemClassName
)}
aria-current={isCurrentPage ? 'page' : undefined}
onClick={() => clickPageNumberHandler(pageItem)}
>
{pageItem}
</a>
) : (
<span className="raw-pagination-ellipsis">
<MoreHorizontal size={16} color={theme.palette.accents7} />
</span>
)}
</li>
);
})}
{paginationItemStyles}
<style jsx>
{`
.raw-pagination-ellipsis {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
}
`}
</style>
</>
);
};

export default PaginationPages;
Loading

0 comments on commit 0e2851a

Please sign in to comment.