-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add pagination nav component (#6199)
* feat: add pagination nav component * fix(pagination-nav): fix prettier issues * refactor(pagination-nav): convert to functional component * refactor(pagination-nav): replace text props with translateWithId * fix(pagination-nav): improve overflow behaviour * fix(pagination-nav): add aria-current to active page * test(pagination-nav): add tests * fix(pagination-nav): fix overflow behavour when only 4 items can be shown * chore(pagination-nav): add prop types to internal components * fix(pagination-nav): add focus state to active page * fix(pagination-nav): add "active" to accessibility label * chore(pagination-nav): fix prettier issue * fix(pagination-nav): announce active page when using direction buttons * fix(pagination-nav): fix unnecessary re-render for first and last page * fix(pagination-nav): read full state after page change Co-authored-by: Alessandra Davila <aledavila@ibm.com> Co-authored-by: DAK <40970507+dakahn@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
- Loading branch information
1 parent
5eccad9
commit 007b1e6
Showing
5 changed files
with
778 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
38 changes: 38 additions & 0 deletions
38
packages/react/src/components/PaginationNav/PaginationNav-story.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
/** | ||
* Copyright IBM Corp. 2020 | ||
* | ||
* This source code is licensed under the Apache-2.0 license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
import React from 'react'; | ||
import { storiesOf } from '@storybook/react'; | ||
import { action } from '@storybook/addon-actions'; | ||
|
||
import { withKnobs, boolean, number } from '@storybook/addon-knobs'; | ||
import PaginationNav from '../PaginationNav'; | ||
|
||
const props = () => ({ | ||
loop: boolean( | ||
'Allow user to loop through the items when reaching first / last (loop)', | ||
false | ||
), | ||
page: number('The current page (page)', 0), | ||
totalItems: number('Total number of items (totalItems)', 10), | ||
itemsShown: number( | ||
'Number of items to be shown (minimum 4) (itemsShown)', | ||
10 | ||
), | ||
onChange: action('onChange'), | ||
}); | ||
|
||
storiesOf('PaginationNav', module) | ||
.addDecorator(withKnobs) | ||
.addDecorator((story) => <div style={{ width: '800px' }}>{story()}</div>) | ||
.add('PaginationNav', () => <PaginationNav {...props()} />, { | ||
info: { | ||
text: ` | ||
Pagination Nav is a group of pagination buttons. | ||
`, | ||
}, | ||
}); |
273 changes: 273 additions & 0 deletions
273
packages/react/src/components/PaginationNav/PaginationNav-test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,273 @@ | ||
/** | ||
* Copyright IBM Corp. 2020 | ||
* | ||
* This source code is licensed under the Apache-2.0 license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
import React from 'react'; | ||
import PaginationNav from '../PaginationNav'; | ||
import { mount } from 'enzyme'; | ||
import { settings } from 'carbon-components'; | ||
import { expect } from 'window-or-global'; | ||
|
||
const { prefix } = settings; | ||
|
||
describe('PaginationNav', () => { | ||
const props = { | ||
className: 'extra-class', | ||
totalItems: 24, | ||
itemsShown: 8, | ||
page: 1, | ||
}; | ||
|
||
const renderPaginationNav = (additionalProps = {}) => | ||
mount(<PaginationNav {...props} {...additionalProps} />); | ||
|
||
describe('renders as expected', () => { | ||
const pagination = renderPaginationNav(); | ||
|
||
describe('container', () => { | ||
it('should render the expected classes', () => { | ||
const container = pagination.childAt(0); | ||
expect(container.hasClass(`${prefix}--pagination-nav`)).toBe(true); | ||
expect(container.hasClass(`extra-class`)).toBe(true); | ||
}); | ||
}); | ||
|
||
describe('items', () => { | ||
it('should render n page items, where n = props.itemsShown', () => { | ||
const pages = pagination.find(`.${prefix}--pagination-nav__page`); | ||
expect(pages.length).toBe(props.itemsShown); | ||
}); | ||
|
||
it('should render a "previous" button as first item', () => { | ||
const button = pagination | ||
.find(`.${prefix}--pagination-nav__list-item`) | ||
.first() | ||
.childAt(0) | ||
.render(); | ||
|
||
expect(button.hasClass(`${prefix}--btn`)).toBe(true); | ||
expect(button.text()).toBe('Previous'); | ||
}); | ||
|
||
it('should render a "Next" button as last item', () => { | ||
const button = pagination | ||
.find(`.${prefix}--pagination-nav__list-item`) | ||
.last() | ||
.childAt(0) | ||
.render(); | ||
|
||
expect(button.hasClass(`${prefix}--btn`)).toBe(true); | ||
expect(button.text()).toBe('Next'); | ||
}); | ||
|
||
it('should render the expected classes for the active page', () => { | ||
const activePage = pagination | ||
.find(`.${prefix}--pagination-nav__page`) | ||
.at(props.page); | ||
expect( | ||
activePage.hasClass(`${prefix}--pagination-nav__page--active`) | ||
).toBe(true); | ||
}); | ||
|
||
it('should disable "Previous" button when on first page and props.loop = false', () => { | ||
let i = 0; | ||
|
||
const pNav = renderPaginationNav({ | ||
page: 0, | ||
loop: false, | ||
onChange: () => { | ||
i++; | ||
}, | ||
}); | ||
|
||
const button = pNav | ||
.find(`.${prefix}--pagination-nav__list-item`) | ||
.first() | ||
.childAt(0); | ||
|
||
expect(button.props().disabled).toBe(true); | ||
expect(i).toBe(0); | ||
button.simulate('click'); | ||
expect(i).toBe(0); | ||
}); | ||
}); | ||
|
||
it('should disable "Next" button when on last page and props.loop = false', () => { | ||
let i = 0; | ||
|
||
const pNav = renderPaginationNav({ | ||
page: 23, | ||
loop: false, | ||
onChange: () => { | ||
i++; | ||
}, | ||
}); | ||
|
||
const button = pNav | ||
.find(`.${prefix}--pagination-nav__list-item`) | ||
.last() | ||
.childAt(0); | ||
|
||
expect(button.props().disabled).toBe(true); | ||
expect(i).toBe(0); | ||
button.simulate('click'); | ||
expect(i).toBe(0); | ||
}); | ||
}); | ||
|
||
describe('behaves as expected', () => { | ||
describe('direction buttons', () => { | ||
it('should move to next page when "Next" button is pressed', () => { | ||
const pagination = renderPaginationNav(); | ||
|
||
let pages = pagination.find(`.${prefix}--pagination-nav__page`); | ||
let activePage = pagination.find( | ||
`.${prefix}--pagination-nav__page--active` | ||
); | ||
expect(activePage.matchesElement(pages.get(props.page))).toBe(true); | ||
|
||
pagination | ||
.find(`.${prefix}--pagination-nav__list-item`) | ||
.last() | ||
.childAt(0) | ||
.simulate('click'); | ||
|
||
pages = pagination.find(`.${prefix}--pagination-nav__page`); | ||
activePage = pagination.find( | ||
`.${prefix}--pagination-nav__page--active` | ||
); | ||
expect(activePage.matchesElement(pages.get(props.page + 1))).toBe(true); | ||
}); | ||
|
||
it('should move to previous page when "Previous" button is pressed', () => { | ||
const pagination = renderPaginationNav(); | ||
|
||
let pages = pagination.find(`.${prefix}--pagination-nav__page`); | ||
let activePage = pagination.find( | ||
`.${prefix}--pagination-nav__page--active` | ||
); | ||
expect(activePage.matchesElement(pages.get(props.page))).toBe(true); | ||
|
||
pagination | ||
.find(`.${prefix}--pagination-nav__list-item`) | ||
.first() | ||
.childAt(0) | ||
.simulate('click'); | ||
|
||
pages = pagination.find(`.${prefix}--pagination-nav__page`); | ||
activePage = pagination.find( | ||
`.${prefix}--pagination-nav__page--active` | ||
); | ||
expect(activePage.matchesElement(pages.get(props.page - 1))).toBe(true); | ||
}); | ||
|
||
it('should move to page when user clicks on one', () => { | ||
const pagination = renderPaginationNav(); | ||
|
||
let pages = pagination.find(`.${prefix}--pagination-nav__page`); | ||
let activePage = pagination.find( | ||
`.${prefix}--pagination-nav__page--active` | ||
); | ||
expect(activePage.matchesElement(pages.get(props.page))).toBe(true); | ||
|
||
pagination | ||
.find(`.${prefix}--pagination-nav__page`) | ||
.at(4) | ||
.simulate('click'); | ||
|
||
pages = pagination.find(`.${prefix}--pagination-nav__page`); | ||
activePage = pagination.find( | ||
`.${prefix}--pagination-nav__page--active` | ||
); | ||
expect(activePage.matchesElement(pages.get(4))).toBe(true); | ||
}); | ||
|
||
it('should emit onChange when moved to new page', () => { | ||
let i = 0; | ||
|
||
const pagination = renderPaginationNav({ | ||
onChange: () => { | ||
i++; | ||
}, | ||
}); | ||
|
||
expect(i).toBe(0); | ||
pagination | ||
.find(`.${prefix}--pagination-nav__list-item`) | ||
.first() | ||
.childAt(0) | ||
.simulate('click'); | ||
expect(i).toBe(1); | ||
pagination | ||
.find(`.${prefix}--pagination-nav__list-item`) | ||
.last() | ||
.childAt(0) | ||
.simulate('click'); | ||
expect(i).toBe(2); | ||
pagination | ||
.find(`.${prefix}--pagination-nav__list-item`) | ||
.at(2) | ||
.childAt(0) | ||
.simulate('click'); | ||
expect(i).toBe(3); | ||
}); | ||
|
||
it('should move to last page when "Previous" button is pressed on first page and props.loop = true', () => { | ||
const pagination = renderPaginationNav({ | ||
page: 0, | ||
loop: true, | ||
}); | ||
|
||
let pages = pagination.find(`.${prefix}--pagination-nav__page`); | ||
let activePage = pagination.find( | ||
`.${prefix}--pagination-nav__page--active` | ||
); | ||
expect(activePage.matchesElement(pages.get(0))).toBe(true); | ||
|
||
pagination | ||
.find(`.${prefix}--pagination-nav__list-item`) | ||
.first() | ||
.childAt(0) | ||
.simulate('click'); | ||
|
||
pages = pagination.find(`.${prefix}--pagination-nav__page`); | ||
activePage = pagination.find( | ||
`.${prefix}--pagination-nav__page--active` | ||
); | ||
expect(activePage.matchesElement(pages.get(pages.length - 1))).toBe( | ||
true | ||
); | ||
}); | ||
|
||
it('should move to first page when "Next" button is pressed on last page and props.loop = true', () => { | ||
const pagination = renderPaginationNav({ | ||
page: 23, | ||
loop: true, | ||
}); | ||
|
||
let pages = pagination.find(`.${prefix}--pagination-nav__page`); | ||
let activePage = pagination.find( | ||
`.${prefix}--pagination-nav__page--active` | ||
); | ||
expect(activePage.matchesElement(pages.get(pages.length - 1))).toBe( | ||
true | ||
); | ||
|
||
pagination | ||
.find(`.${prefix}--pagination-nav__list-item`) | ||
.last() | ||
.childAt(0) | ||
.simulate('click'); | ||
|
||
pages = pagination.find(`.${prefix}--pagination-nav__page`); | ||
activePage = pagination.find( | ||
`.${prefix}--pagination-nav__page--active` | ||
); | ||
expect(activePage.matchesElement(pages.get(0))).toBe(true); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.