Skip to content
This repository has been archived by the owner on Dec 7, 2020. It is now read-only.

Commit

Permalink
Merge pull request #219 from uktrade/pagination
Browse files Browse the repository at this point in the history
feat: Add Pagination component
  • Loading branch information
rafenden authored Nov 4, 2019
2 parents 7aa0af0 + 1d134d9 commit 168de37
Show file tree
Hide file tree
Showing 6 changed files with 465 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,8 @@ export { default as FieldUneditable } from './forms/elements/FieldUneditable'
export { default as Metadata } from './metadata/Metadata'
export { default as MetadataItem } from './metadata/MetadataItem'

// Pagination
export { default as Pagination } from './pagination/Pagination'

// Status message
export { default as StatusMessage } from './status-message/StatusMessage'
159 changes: 159 additions & 0 deletions src/pagination/Pagination.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import { GREY_1, GREY_3, GREY_4, LINK_COLOUR, TEXT_COLOUR } from 'govuk-colours'
import { FONT_SIZE, MEDIA_QUERIES, SPACING } from '@govuk-react/constants'
import Link from '@govuk-react/link'

import computeVisiblePieces from './computeVisiblePieces'

import {
PAGINATION_PIECE_ELLIPSIS,
PAGINATION_PIECE_NEXT,
PAGINATION_PIECE_PAGE_NUMBER,
PAGINATION_PIECE_PREVIOUS,
} from './constants'

const StyledNav = styled('nav')`
text-align: center;
line-height: 1;
display: flex;
justify-content: space-around;
padding: ${SPACING.SCALE_3};
${MEDIA_QUERIES.TABLET} {
display: block;
}
`

const StyledPaginationList = styled('ul')`
margin: 0;
list-style: none;
`

const StyledPaginationPiece = styled('li')`
display: inline-block;
& + & {
margin-left: ${SPACING.SCALE_1};
}
`

const StyledPaginationLink = styled(Link)`
font-weight: bold;
font-size: ${FONT_SIZE.SIZE_16};
display: inline-block;
padding: ${SPACING.SCALE_1} ${SPACING.SCALE_3};
background-color: ${GREY_4};
line-height: 1.9em;
color: ${LINK_COLOUR};
text-decoration: none;
:hover {
background-color: ${GREY_3};
}
`

const StyledActivePaginationLink = styled(StyledPaginationLink)`
:link {
color: ${TEXT_COLOUR};
background-color: transparent;
}
`

const StyledPagesTruncation = styled('span')`
font-weight: bold;
font-size: ${FONT_SIZE.SIZE_16};
display: inline-block;
padding: ${SPACING.SCALE_2};
background-color: transparent;
line-height: ${FONT_SIZE.SIZE_24};
color: ${GREY_1};
`

function Pagination({ totalPages, activePage, onPageClick, getPageUrl }) {
const visiblePieces = computeVisiblePieces(totalPages, activePage)

if (totalPages === 1) {
return null
}

return (
<StyledNav aria-label={`pagination: total ${totalPages} pages`}>
<StyledPaginationList>
{visiblePieces.map(
({ type, pageNumber, isActive, isDisabled }, index) => {
const key = `${type}-${index}`
const onClick = (event) => onPageClick(pageNumber, event)

const PageNumberLink = isActive
? StyledActivePaginationLink
: StyledPaginationLink

if (isDisabled) {
return null
}

return (
<StyledPaginationPiece>
{type === PAGINATION_PIECE_PREVIOUS && (
<StyledPaginationLink
key={key}
data-test="prev"
onClick={onClick}
href={getPageUrl(pageNumber)}
>
Previous
</StyledPaginationLink>
)}

{type === PAGINATION_PIECE_ELLIPSIS && (
<StyledPagesTruncation key={key} data-test="ellipsis">
</StyledPagesTruncation>
)}

{type === PAGINATION_PIECE_PAGE_NUMBER && (
<PageNumberLink
key={key}
data-test={isActive ? 'page-number-active' : 'page-number'}
onClick={onClick}
href={getPageUrl(pageNumber)}
>
{pageNumber}
</PageNumberLink>
)}

{type === PAGINATION_PIECE_NEXT && (
<StyledPaginationLink
key={key}
data-test="next"
onClick={onClick}
href={getPageUrl(pageNumber)}
>
Next
</StyledPaginationLink>
)}
</StyledPaginationPiece>
)
}
)}
</StyledPaginationList>
</StyledNav>
)
}

Pagination.propTypes = {
totalPages: PropTypes.number.isRequired,
activePage: PropTypes.number,
onPageClick: PropTypes.func,
getPageUrl: PropTypes.func,
}

Pagination.defaultProps = {
activePage: 1,
onPageClick: null,
getPageUrl: () => '#',
}

export default Pagination
23 changes: 23 additions & 0 deletions src/pagination/__stories__/Pagination.stories.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React, { useState } from 'react'
import { storiesOf } from '@storybook/react'

import Pagination from '../Pagination'

const collectionStories = storiesOf('Pagination', module)

const PaginationWithState = () => {
const [activePage, setActivePage] = useState(1)

return (
<Pagination
activePage={activePage}
totalPages={1000}
onPageClick={(page, event) => {
setActivePage(page)
event.preventDefault()
}}
/>
)
}

collectionStories.add('Default', () => <PaginationWithState />)
203 changes: 203 additions & 0 deletions src/pagination/__tests__/Pagination.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import React from 'react'
import { mount } from 'enzyme/build'
import Pagination from '../Pagination'

describe('Pagination', () => {
let wrapper

describe('when 7 pages are passed and the active page is 3', () => {
beforeAll(() => {
wrapper = mount(<Pagination activePage={3} totalPages={7} />)
})

test('should render the previous button', () => {
expect(
wrapper
.find('a')
.first()
.text()
).toBe('Previous')
})

test('should render the next button', () => {
expect(
wrapper
.find('a')
.last()
.text()
).toBe('Next')
})

test('should render all the pagination pieces', () => {
expect(wrapper.find('li')).toHaveLength(9)
})

test('should render the truncated page link', () => {
expect(wrapper.find('li span')).toHaveLength(1)
})

test('should render the active page', () => {
expect(wrapper.find('a[data-test="page-number-active"]').text()).toEqual(
'3'
)
})
})

describe('when 3 pages are passed and the active page is 2', () => {
beforeAll(() => {
wrapper = mount(<Pagination activePage={2} totalPages={3} />)
})

test('should render the previous button', () => {
expect(
wrapper
.find('a')
.first()
.text()
).toBe('Previous')
})

test('should render the next button', () => {
expect(
wrapper
.find('a')
.last()
.text()
).toBe('Next')
})

test('should render all the pagination pieces', () => {
expect(wrapper.find('li')).toHaveLength(5)
})

test('should render the active page', () => {
expect(wrapper.find('a[data-test="page-number-active"]').text()).toEqual(
'2'
)
})
})

describe('when just 1 page is passed', () => {
beforeAll(() => {
wrapper = mount(<Pagination totalPages={1} />)
})

test('should not render the component', () => {
expect(wrapper.find('nav').exists()).toBe(false)
})
})

describe('when 2 pages are passed and the active page is page 1', () => {
beforeAll(() => {
wrapper = mount(<Pagination totalPages={2} activePage={1} />)
})

test('should not render the previous button', () => {
expect(wrapper.findWhere((a) => a.text() === 'Previous')).toHaveLength(0)
})

test('should render the next button', () => {
expect(
wrapper
.find('a')
.last()
.text()
).toBe('Next')
})
})

describe('when 2 pages are passed and the active page is page 2', () => {
beforeAll(() => {
wrapper = mount(<Pagination totalPages={2} activePage={2} />)
})

test('should not render the next button', () => {
expect(wrapper.findWhere((a) => a.text() === 'Next')).toHaveLength(0)
})

test('should render the previous button', () => {
expect(
wrapper
.find('a')
.first()
.text()
).toBe('Previous')
})
})

describe('when 1000 pages are passed and the active page is 99', () => {
beforeAll(() => {
wrapper = mount(<Pagination totalPages={1000} activePage={99} />)
})

test('should render the previous button', () => {
expect(
wrapper
.find('a')
.first()
.text()
).toBe('Previous')
})

test('should render the next button', () => {
expect(
wrapper
.find('a')
.last()
.text()
).toBe('Next')
})

test('should render all the page links (including two truncated ones)', () => {
expect(wrapper.find('li')).toHaveLength(11)
})

test('should render two truncated page links', () => {
expect(wrapper.find('li span[data-test="ellipsis"]')).toHaveLength(2)
})

test('should render the active page', () => {
expect(wrapper.find('a[data-test="page-number-active"]').text()).toEqual(
'99'
)
})
})

describe('when 1000 pages are passed and the active page is 999', () => {
beforeAll(() => {
wrapper = mount(<Pagination totalPages={1000} activePage={999} />)
})

test('should render the previous button', () => {
expect(
wrapper
.find('a')
.first()
.text()
).toBe('Previous')
})

test('should render the next button', () => {
expect(
wrapper
.find('a')
.last()
.text()
).toBe('Next')
})

test('should render all the page links', () => {
expect(wrapper.find('li')).toHaveLength(9)
})

test('should render one truncated page link', () => {
expect(wrapper.find('li span[data-test="ellipsis"]')).toHaveLength(1)
})

test('should render the active page', () => {
expect(wrapper.find('a[data-test="page-number-active"]').text()).toEqual(
'999'
)
})
})
})
Loading

0 comments on commit 168de37

Please sign in to comment.