Skip to content

Commit

Permalink
feat: add pagination nav component (#6199)
Browse files Browse the repository at this point in the history
* 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
4 people authored Jul 10, 2020
1 parent 5eccad9 commit 007b1e6
Show file tree
Hide file tree
Showing 5 changed files with 778 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@
background-color: $background-color-active;
color: $text-color-active;
font-weight: 600;
outline: none;
}

.#{$prefix}--pagination-nav__icon {
Expand Down
38 changes: 38 additions & 0 deletions packages/react/src/components/PaginationNav/PaginationNav-story.js
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 packages/react/src/components/PaginationNav/PaginationNav-test.js
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);
});
});
});
});
Loading

0 comments on commit 007b1e6

Please sign in to comment.