Skip to content

Commit

Permalink
Merge pull request medly#169 from medly/feature-pagination
Browse files Browse the repository at this point in the history
feat(core): update pagination component as per new design
  • Loading branch information
gmukul01 authored Oct 8, 2020
2 parents 2e396bb + 9f84caa commit 4f039ac
Show file tree
Hide file tree
Showing 23 changed files with 1,012 additions and 820 deletions.
21 changes: 18 additions & 3 deletions packages/core/src/components/Pagination/Pagination.stories.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Pagination } from './Pagination';
import { defaultTheme } from '@medly-components/theme';
import { boolean, number } from '@storybook/addon-knobs';
import { Preview, Story, Meta, Props } from '@storybook/addon-docs/blocks';
import * as stories from './Pagination.stories';
import { ThemeInterface } from './Pagination.stories';
import { useState } from 'react';

<Meta title="Core" component={Pagination} />

Expand All @@ -10,7 +12,20 @@ import * as stories from './Pagination.stories';
### Basic Pagination

<Preview withToolbar>
<Story name="Pagination">{stories.Basic()}</Story>
<Story name="Pagination">
{() => {
const [activePage, setActivePage] = useState(1);
return (
<Pagination
activePage={activePage}
totalItems={number('Total Items', 1000)}
itemsPerPage={number('Items per page', 20)}
hidePrevNextLinks={boolean('Hide Prev Next Links', false)}
onPageClick={setActivePage}
/>
);
}}
</Story>
</Preview>

### Props
Expand All @@ -19,4 +34,4 @@ import * as stories from './Pagination.stories';

### Theme

There is no exclusive theme for pagination, it is actually using button theme.
<Props of={ThemeInterface} />
23 changes: 5 additions & 18 deletions packages/core/src/components/Pagination/Pagination.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,7 @@
import { boolean, number } from '@storybook/addon-knobs';
import React, { useState } from 'react';
import { defaultTheme, PaginationTheme } from '@medly-components/theme/src';
import React from 'react';

import { Pagination } from './Pagination';

export const Basic = () => {
const [activePage, setActivePage] = useState(1);

return (
<Pagination
activePage={activePage}
totalItems={number('Total Items', 1000)}
itemsPerPage={number('Items per page', 20)}
pageRangeDisplayed={number('Page Range Displayed', 5)}
hideFirstLastLinks={boolean('Hide First Last Links', false)}
hidePrevNextLinks={boolean('Hide Prev Next Links', false)}
onPageClick={setActivePage}
/>
);
export const ThemeInterface: React.FC<PaginationTheme> = () => null;
ThemeInterface.defaultProps = {
...defaultTheme.pagination
};
86 changes: 86 additions & 0 deletions packages/core/src/components/Pagination/Pagination.styled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { ChevronLeftIcon, ChevronRightIcon, SvgIcon } from '@medly-components/icons';
import { css, styled } from '@medly-components/utils';
import List from '../List';

export const ListWrapper = styled(List)`
& > li {
margin: 0;
}
`;

export const getPageNumberButtonStyleByState = (
itemKey: 'pageNumber' | 'overlayPageNumber',
state: 'default' | 'active' | 'pressed' | 'hovered'
) => css`
color: ${({ theme: { pagination } }) => pagination[itemKey].color[state]};
background: ${({ theme: { pagination } }) => pagination[itemKey].bgColor[state]};
`;

const getPageNavButtonStyleByState = (state: 'default' | 'pressed' | 'hovered' | 'disabled') => css`
background: ${({ theme: { pagination } }) => pagination.pageNav.bgColor[state]};
${SvgIcon} {
* {
fill: ${({ theme: { pagination } }) => pagination.pageNav.color[state]};
}
}
`;

const pageNavButtonState = css`
&:hover {
${getPageNavButtonStyleByState('hovered')}
}
&:active {
${getPageNavButtonStyleByState('pressed')}
}
`;

export const BaseButton = styled.button`
height: 4rem;
width: 4rem;
display: block;
border-radius: 50%;
user-select: none;
border: none;
padding: 0;
text-align: center;
transition: all 100ms ease-out;
${getPageNumberButtonStyleByState('pageNumber', 'default')}
&:hover {
cursor: pointer;
}
&:focus {
outline: none;
}
&:disabled {
cursor: not-allowed;
}
`;
export const PageNumberButton = styled(BaseButton)<{ isActive?: boolean }>`
margin: 4px;
height: 3.2rem;
width: 3.2rem;
${props => getPageNumberButtonStyleByState('pageNumber', props.isActive ? 'active' : 'default')};
&:hover {
${props => !props.isActive && getPageNumberButtonStyleByState('pageNumber', 'hovered')};
}
&:active {
font-weight: ${({ theme }) => theme.font.weights.Strong};
${getPageNumberButtonStyleByState('pageNumber', 'pressed')}
}
`;

export const PageNavButton = styled(BaseButton)<{ disabled: boolean }>`
& > ${ChevronLeftIcon.Style}, ${ChevronRightIcon.Style} {
margin: 4px;
padding: 4px;
}
${getPageNavButtonStyleByState('default')}
${props => (props.disabled ? getPageNavButtonStyleByState('disabled') : pageNavButtonState)};
`;
73 changes: 30 additions & 43 deletions packages/core/src/components/Pagination/Pagination.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,78 +3,65 @@ import React from 'react';
import { Pagination } from './Pagination';

describe('Pagination component', () => {
const renderer = (props: any = { totalItems: 0 }) => {
const mockOnPageClick = jest.fn();
return {
mockOnPageClick,
...render(<Pagination {...props} onPageClick={mockOnPageClick} />)
};
};

afterEach(cleanup);

it('should render correctly with all the default props', () => {
const mockOnPageClick = jest.fn();
const { container } = render(<Pagination totalItems={150} onPageClick={mockOnPageClick} />);
const { container } = renderer({ totalItems: 150 });
expect(container).toMatchSnapshot();
});

it('should render correctly when total items is 0', () => {
const mockOnPageClick = jest.fn();
const { container } = render(<Pagination totalItems={0} onPageClick={mockOnPageClick} />);
const { container } = renderer();
expect(container).toMatchSnapshot();
});

it('should render correctly with all the props given', () => {
const mockOnPageClick = jest.fn();
const { container } = render(
<Pagination
totalItems={150}
activePage={1}
itemsPerPage={10}
pageRangeDisplayed={4}
hideFirstLastLinks
hidePrevNextLinks
onPageClick={mockOnPageClick}
/>
);
const { container } = renderer({ totalItems: 150, activePage: 1, itemsPerPage: 10, hidePrevNextLinks: true });
expect(container).toMatchSnapshot();
});

it('should call onClick handler with correct page number when any page link is clicked', () => {
const mockOnPageClick = jest.fn();
const { getByText } = render(<Pagination activePage={3} totalItems={150} onPageClick={mockOnPageClick} />);

const { getByText, mockOnPageClick } = renderer({ totalItems: 150, activePage: 3, itemsPerPage: 10 });
fireEvent.click(getByText('4').closest('button'));

expect(mockOnPageClick).toBeCalledWith(4);
});

it('should call onClick handler with first page when first link is clicked', () => {
const mockOnPageClick = jest.fn();
const { getByText } = render(<Pagination activePage={3} totalItems={150} onPageClick={mockOnPageClick} />);

fireEvent.click(getByText('First').closest('button'));

it('should call onClick handler with first page when first page is clicked', () => {
const { getByText, mockOnPageClick } = renderer({ totalItems: 150, activePage: 3, itemsPerPage: 10 });
fireEvent.click(getByText('1').closest('button'));
expect(mockOnPageClick).toBeCalledWith(1);
});

it('should call onClick handler with last page when last link is clicked', () => {
const mockOnPageClick = jest.fn();
const { getByText } = render(<Pagination activePage={3} totalItems={150} onPageClick={mockOnPageClick} />);

fireEvent.click(getByText('Last').closest('button'));

expect(mockOnPageClick).toBeCalledWith(8);
it('should call onClick handler with last page when last page is clicked', () => {
const { getByText, mockOnPageClick } = renderer({ totalItems: 150, activePage: 3, itemsPerPage: 10 });
fireEvent.click(getByText('15').closest('button'));
expect(mockOnPageClick).toBeCalledWith(15);
});

it('should call onClick handler with prev page when prev link is clicked', () => {
const mockOnPageClick = jest.fn();
const { getByText } = render(<Pagination activePage={3} totalItems={150} onPageClick={mockOnPageClick} />);

fireEvent.click(getByText('Prev').closest('button'));

const { mockOnPageClick, container } = renderer({ totalItems: 150, activePage: 3, itemsPerPage: 10 });
fireEvent.click(container.querySelectorAll('svg')[0].closest('button'));
expect(mockOnPageClick).toBeCalledWith(2);
});

it('should call onClick handler with next page when next link is clicked', () => {
const mockOnPageClick = jest.fn();
const { getByText } = render(<Pagination activePage={3} totalItems={150} onPageClick={mockOnPageClick} />);

fireEvent.click(getByText('Next').closest('button'));

const { mockOnPageClick, container } = renderer({ totalItems: 150, activePage: 3, itemsPerPage: 10 });
fireEvent.click(container.querySelectorAll('svg')[1].closest('button'));
expect(mockOnPageClick).toBeCalledWith(4);
});

it('should call onClick handler with expected page when selected from ellipsis popover', () => {
const { mockOnPageClick, getByText } = renderer({ totalItems: 150, activePage: 3, itemsPerPage: 10 });
fireEvent.click(getByText('...').closest('button'));
fireEvent.click(getByText('7').closest('button'));
expect(mockOnPageClick).toBeCalledWith(7);
});
});
103 changes: 37 additions & 66 deletions packages/core/src/components/Pagination/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -1,100 +1,71 @@
import { ChevronLeftIcon, ChevronRightIcon } from '@medly-components/icons';
import { WithStyle } from '@medly-components/utils';
import React, { FC, useMemo } from 'react';
import Button from '../Button';
import List from '../List';
import Popover from '../Popover';
import Text from '../Text';
import { paginator } from './helper';
import { ListWrapper, PageNavButton, PageNumberButton } from './Pagination.styled';
import PaginationPopup from './PaginationPopup';
import { PaginationProps } from './types';

export const Pagination: FC<PaginationProps> & WithStyle = React.memo(
React.forwardRef((props, ref) => {
const links = [],
{
hideFirstLastLinks,
hidePrevNextLinks,
activePage,
itemsPerPage,
totalItems,
pageRangeDisplayed,
onPageClick,
...restProps
} = props,
pagesConfig = useMemo(() => paginator(totalItems, activePage, itemsPerPage, pageRangeDisplayed), [
{ hidePrevNextLinks, activePage, itemsPerPage, totalItems, onPageClick, ...restProps } = props,
{ currentPage, linkItems, totalPages } = useMemo(() => paginator(totalItems, activePage, itemsPerPage), [
totalItems,
activePage,
itemsPerPage,
pageRangeDisplayed
itemsPerPage
]);

const onClickHandler = (page: number) => () => {
onPageClick(page);
};
const onClickHandler = (page: number | '...') => () => page !== '...' && onPageClick(page);

for (let i = pagesConfig.startPage; i <= pagesConfig.endPage; i++) {
links.push(
<Button size="S" key={i} onClick={onClickHandler(i)} variant={i === pagesConfig.currentPage ? 'solid' : 'outlined'}>
{i}
</Button>
);
for (let i = 0; i < linkItems.length; i++) {
if (linkItems[i] === '...')
links.push(
<Popover interactionType="click">
<PaginationPopup
prevPageNumber={linkItems[i - 1] as number}
nextPageNumber={linkItems[i + 1] as number}
onClickHandler={onPageClick}
></PaginationPopup>
</Popover>
);
else
links.push(
<PageNumberButton key={i} onClick={onClickHandler(linkItems[i])} isActive={linkItems[i] === currentPage}>
<Text textVariant="h5" textAlign="center" textWeight={linkItems[i] === currentPage ? 'Strong' : 'Medium'}>
{linkItems[i]}
</Text>
</PageNumberButton>
);
}

if (!hidePrevNextLinks) {
links.unshift(
<Button
size="S"
key="prev"
disabled={pagesConfig.currentPage < 2}
onClick={onClickHandler(pagesConfig.currentPage - 1)}
variant="outlined"
>
Prev
</Button>
);
links.push(
<Button
size="S"
key="next"
disabled={pagesConfig.currentPage === pagesConfig.totalPages}
onClick={onClickHandler(pagesConfig.currentPage + 1)}
variant="outlined"
>
Next
</Button>
);
}

if (!hideFirstLastLinks) {
links.unshift(
<Button size="S" key="first" disabled={pagesConfig.currentPage < 2} onClick={onClickHandler(1)} variant="outlined">
First
</Button>
<PageNavButton key="first" disabled={currentPage < 2} onClick={onClickHandler(currentPage - 1)}>
<ChevronLeftIcon size="M" />
</PageNavButton>
);
links.push(
<Button
size="S"
key="last"
disabled={pagesConfig.currentPage === pagesConfig.totalPages}
onClick={onClickHandler(pagesConfig.totalPages)}
variant="outlined"
>
Last
</Button>
<PageNavButton key="last" disabled={currentPage === totalPages} onClick={onClickHandler(currentPage + 1)}>
<ChevronRightIcon size="M" />
</PageNavButton>
);
}

return (
<List ref={ref} variant="horizontal" {...restProps}>
<ListWrapper ref={ref} variant="horizontal" {...restProps}>
{links}
</List>
</ListWrapper>
);
})
);

Pagination.displayName = 'Pagination';
Pagination.Style = List.Style;
Pagination.Style = ListWrapper.Style;
Pagination.defaultProps = {
activePage: 1,
itemsPerPage: 20,
pageRangeDisplayed: 5,
hideFirstLastLinks: false,
hidePrevNextLinks: false
};
Loading

0 comments on commit 4f039ac

Please sign in to comment.