Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(component): create basic pagination component #188

Merged
merged 12 commits into from
Sep 16, 2019
100 changes: 100 additions & 0 deletions packages/big-design/src/components/Pagination/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { ArrowDropDownIcon, ChevronLeftIcon, ChevronRightIcon } from '@bigcommerce/big-design-icons';
import React, { useEffect, useState } from 'react';

import { MarginProps } from '../../mixins';
import { Box } from '../Box';
import { Dropdown } from '../Dropdown';
import { Flex } from '../Flex';

import { StyledButton, StyledIconButton } from './styled';

export interface PaginationProps extends MarginProps {
currentRange: number;
currentPage: number;
totalItems: number;
rangeOptions: number[];
onPageChange(page: number): void;
onRangeChange(range: number): void;
jordan-massingill marked this conversation as resolved.
Show resolved Hide resolved
}

export const Pagination: React.FC<PaginationProps> = props => {
jordan-massingill marked this conversation as resolved.
Show resolved Hide resolved
const { currentRange, currentPage, totalItems, rangeOptions, onPageChange, onRangeChange } = props;

const [maxPages, setMaxPages] = useState(Math.ceil(totalItems / currentRange));
const [itemRange, setItemRange] = useState([0, 0]);

useEffect(() => {
const firstItemInRange = currentRange * (currentPage - 1) + 1;
let lastItemInRange = currentRange * currentPage;
if (lastItemInRange > totalItems) {
lastItemInRange = totalItems;
}

setItemRange([firstItemInRange, lastItemInRange]);
}, [currentPage, currentRange, totalItems]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong, but if totalItems changes, maxPages will not update. So with that in mind, you'll maybe want to have another useEffect listening to only totalItems and update it whenever that changes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the state of currentPage is dependent on totalItems and currentRange props, it should/does change dynamically without the need for a useEffect listener (at least, that has been my experience with the component). We may need to look at messing around with this to be certain though.


const handlePageIncrease = () => {
let nextPage = currentPage + 1;
if (nextPage > maxPages) {
jordan-massingill marked this conversation as resolved.
Show resolved Hide resolved
nextPage = 1;
}
onPageChange(nextPage);
};

const handlePageDecrease = () => {
let nextPage = currentPage - 1;
if (nextPage < 1) {
jordan-massingill marked this conversation as resolved.
Show resolved Hide resolved
nextPage = maxPages;
}
onPageChange(nextPage);
};

const handleRangeChange = (range: number) => {
setMaxPages(Math.ceil(totalItems / range));
onRangeChange(range);
};

const showRanges = () => {
if (itemRange[0] !== itemRange[1]) {
return `${itemRange[0]} - ${itemRange[1]} of ${totalItems}`;
} else {
return `${itemRange[0]} of ${totalItems}`;
}
};

return (
<Flex flexDirection="row" justifyContent="flex-start" role="navigation" aria-label="pagination">
<Dropdown
onItemClick={handleRangeChange}
trigger={
<StyledButton variant="subtle" iconRight={<ArrowDropDownIcon size="xxLarge" color="secondary70" />}>
{showRanges()}
</StyledButton>
}
>
{rangeOptions.map(range => {
return <Dropdown.Item value={range}>{range} per page</Dropdown.Item>;
})}
</Dropdown>
<Box>
jordan-massingill marked this conversation as resolved.
Show resolved Hide resolved
<StyledIconButton
variant="subtle"
disabled={currentPage <= 1}
aria-label="previous page"
onClick={handlePageDecrease}
>
<ChevronLeftIcon />
</StyledIconButton>

<StyledIconButton
variant="subtle"
disabled={currentPage === maxPages}
aria-label="next page"
onClick={handlePageIncrease}
>
<ChevronRightIcon />
</StyledIconButton>
</Box>
jordan-massingill marked this conversation as resolved.
Show resolved Hide resolved
</Flex>
);
};
1 change: 1 addition & 0 deletions packages/big-design/src/components/Pagination/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Pagination, PaginationProps } from './Pagination';
jordan-massingill marked this conversation as resolved.
Show resolved Hide resolved
12 changes: 12 additions & 0 deletions packages/big-design/src/components/Pagination/styled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import styled from 'styled-components';

import { StyleableButton } from '../Button/private';

export const StyledButton = styled(StyleableButton)`
color: ${({ theme }) => theme.colors.secondary70};
jordan-massingill marked this conversation as resolved.
Show resolved Hide resolved
`;

// in case we need to add separate styles to the icon buttons or to the dropdown trigger
export const StyledIconButton = styled(StyleableButton)`
jordan-massingill marked this conversation as resolved.
Show resolved Hide resolved
color: ${({ theme }) => theme.colors.secondary70};
`;
1 change: 1 addition & 0 deletions packages/big-design/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './Grid';
export * from './Input';
export * from './Link';
export * from './Modal';
export * from './Pagination';
export * from './Panel';
export * from './ProgressBar';
export * from './ProgressCircle';
Expand Down
28 changes: 28 additions & 0 deletions packages/docs/PropTables/PaginationPropTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';

import { PropTable } from '../components';

export const PaginationPropTable: React.FC = () => {
return (
<PropTable>
<PropTable.Prop name="currentRange" types="number" defaults="" required>
Indicates how many items are displayed per page
jordan-massingill marked this conversation as resolved.
Show resolved Hide resolved
</PropTable.Prop>
<PropTable.Prop name="currentPage" types="number" defaults="" required>
Indicates the page currently/initially displayed
</PropTable.Prop>
<PropTable.Prop name="totalItems" types="number" defaults="" required>
Indicates how many items in total will be displayed
</PropTable.Prop>
<PropTable.Prop name="rangeOptions" types="number[]" defaults="" required>
Indicates options for per-page ranges
</PropTable.Prop>
<PropTable.Prop name="onPageChange" types="(page: number) => void" required>
Function that will be called when a navigation arrow is clicked
</PropTable.Prop>
<PropTable.Prop name="onRangeChange" types="(range: number) => void" required>
Function that will be called when a new per-page range is selected
</PropTable.Prop>
</PropTable>
);
};
1 change: 1 addition & 0 deletions packages/docs/PropTables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from './InputPropTable';
export * from './MarginPropTable';
export * from './ModalPropTable';
export * from './PaddingPropTable';
export * from './PaginationPropTable';
export * from './PanelPropTable';
export * from './ProgressBarPropTable';
export * from './ProgressCirclePropTable';
Expand Down
3 changes: 3 additions & 0 deletions packages/docs/components/SideNav/SideNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export const SideNav: React.FC = () => {
<SideNavLink href="/Modal/ModalPage" as="/modal">
Modal
</SideNavLink>
<SideNavLink href="/Pagination/PaginationPage" as="/Pagination">
Pagination
</SideNavLink>
<SideNavLink href="/Panel/PanelPage" as="/panel">
Panel
</SideNavLink>
Expand Down
1 change: 1 addition & 0 deletions packages/docs/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ module.exports = {
'/margin': { page: '/Margin/MarginPage' },
'/modal': { page: '/Modal/ModalPage' },
'/padding': { page: '/Padding/PaddingPage' },
'/pagination': { page: '/Pagination/PaginationPage' },
'/panel': { page: '/Panel/PanelPage' },
'/progress-bar': { page: '/Progress/ProgressBarPage' },
'/progress-circle': { page: '/Progress/ProgressCirclePage' },
Expand Down
75 changes: 75 additions & 0 deletions packages/docs/pages/Pagination/PaginationPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { H0, H1, H2, Link, Pagination, Text } from '@bigcommerce/big-design';
import React from 'react';

import { CodePreview, Collapsible } from '../../components';
import { MarginPropTable, PaginationPropTable } from '../../PropTables';

export default () => (
<>
<H0>Pagination</H0>

<Text>
Pagination allows for navigation through pages of content.{' '}
<Link href="https://design.bigcommerce.com/components/pagination" target="_blank">
Pagination Design Guidelines
</Link>
.
</Text>

<CodePreview>
{/* jsx-to-string:start */}
{function Example() {
const items = ['Item1', 'Item2', 'Item3', 'Item4', 'Item5'];
const ranges = [2, 3, 4];

const [range, setRange] = React.useState(ranges[0]);
const [page, setPage] = React.useState(1);
const [currentItems, setCurrentItems] = React.useState(['']);

{
/* This is similar to componentDidMount and componentDidUpdate */
jordan-massingill marked this conversation as resolved.
Show resolved Hide resolved
}
React.useEffect(() => {
let lastItem = page * range;
const firstItem = lastItem - range;
if (lastItem > items.length) {
lastItem = items.length;
}

setCurrentItems(items.slice(firstItem, lastItem));
}, [page, items, range]);

return (
<>
<Pagination
currentPage={page}
currentRange={range}
rangeOptions={ranges}
totalItems={items.length}
onPageChange={newPage => setPage(newPage)}
onRangeChange={newRange => setRange(newRange)}
/>
<ul>
{currentItems.map(item => (
<li>{item}</li>
))}
</ul>
</>
);
}}
{/* jsx-to-string:end */}
</CodePreview>

<H1>API</H1>

<H2>Pagination</H2>

<PaginationPropTable />

<H2>Inherited Props</H2>

<Collapsible title="Inherited Props">
<MarginPropTable />
</Collapsible>
</>
);