Skip to content

Commit

Permalink
feat(Pagination): User can now enter numbers into the input field wit…
Browse files Browse the repository at this point in the history
…hout highlighting the page numb

Fixes #2344
  • Loading branch information
rebeccaalpert committed Jul 9, 2019
1 parent dd49c7e commit fe4baf1
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 129 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { css } from '@patternfly/react-styles';
import { AngleLeftIcon, AngleDoubleLeftIcon, AngleRightIcon, AngleDoubleRightIcon } from '@patternfly/react-icons';
import { Button, ButtonVariant } from '../Button';
import { pluralize } from '../../helpers';
import { KEY_CODES } from '../../helpers/constants';

export interface NavigationProps extends React.HTMLProps<HTMLElement> {
/** Additional classes for the container */
Expand All @@ -25,7 +26,7 @@ export interface NavigationProps extends React.HTMLProps<HTMLElement> {
/** Accessible label for the pagination component */
paginationTitle?: string;
/** The number of the current page */
page: number;
page: number | React.ReactText;
/** Function called when user sets page */
onSetPage: (event: React.SyntheticEvent<HTMLButtonElement>, page: number) => void;
/** Function called when user clicks to navigate to next page */
Expand All @@ -40,97 +41,148 @@ export interface NavigationProps extends React.HTMLProps<HTMLElement> {
onPageInput?: (event: React.SyntheticEvent<HTMLButtonElement>, page: number) => void;
}

export const Navigation: React.FunctionComponent<NavigationProps> = ({
page,
onSetPage,
className = '',
lastPage = 0,
pagesTitle = '',
toLastPage = 'Go to last page',
toNextPage = 'Go to next page',
toFirstPage = 'Go to first page',
toPreviousPage = 'Go to previous page',
currPage = 'Current page',
paginationTitle = 'Pagination',
onNextClick = () => undefined,
onPreviousClick = () => undefined,
onFirstClick = () => undefined,
onLastClick = () => undefined,
onPageInput = () => undefined,
...props
}: NavigationProps) => (
<nav className={css(styles.paginationNav, className)} aria-label={paginationTitle} {...props}>
<Button
variant={ButtonVariant.plain}
isDisabled={page === 1}
aria-label={toFirstPage}
data-action="first"
onClick={event => {
onFirstClick(event, 1);
onSetPage(event, 1);
}}
>
<AngleDoubleLeftIcon />
</Button>
<Button
variant={ButtonVariant.plain}
isDisabled={page === 1}
data-action="previous"
onClick={event => {
const newPage = page - 1 >= 1 ? page - 1 : 1;
onPreviousClick(event, newPage);
onSetPage(event, newPage);
}}
aria-label={toPreviousPage}
>
<AngleLeftIcon />
</Button>
<div className={css(styles.paginationNavPageSelect)}>
<input
className={css(styles.formControl)}
aria-label={currPage}
type="number"
min="1"
max={lastPage}
value={page}
onChange={event => {
let inputPage = Number.parseInt(event.target.value, 10);
inputPage = Number.isNaN(inputPage) ? page : inputPage;
inputPage = inputPage > lastPage ? lastPage : inputPage;
inputPage = inputPage < 1 ? 1 : inputPage;
onSetPage(event, Number.isNaN(inputPage) ? page : inputPage);
onPageInput(event, Number.isNaN(inputPage) ? page : inputPage);
}}
/>
<span aria-hidden="true">
of {pluralize(lastPage, pagesTitle)}
</span>
</div>
<Button
variant={ButtonVariant.plain}
isDisabled={page === lastPage}
aria-label={toNextPage}
data-action="next"
onClick={event => {
const newPage = page + 1 <= lastPage ? page + 1 : lastPage;
onNextClick(event, newPage);
onSetPage(event, newPage);
}}
>
<AngleRightIcon />
</Button>
<Button
variant={ButtonVariant.plain}
isDisabled={page === lastPage}
aria-label={toLastPage}
data-action="last"
onClick={event => {
onLastClick(event, lastPage);
onSetPage(event, lastPage);
}}
>
<AngleDoubleRightIcon />
</Button>
</nav>
);
export interface NavigationState {
userInputPage?: React.ReactText | number;
}

export class Navigation extends React.Component<NavigationProps, NavigationState> {
constructor(props: NavigationProps) {
super(props);
this.state = { userInputPage: this.props.page };
}

static defaultProps = {
className: '',
lastPage: 0,
pagesTitle: '',
toLastPage: 'Go to last page',
toNextPage: 'Go to next page',
toFirstPage: 'Go to first page',
toPreviousPage: 'Go to previous page',
currPage: 'Current page',
paginationTitle: 'Pagination',
onNextClick: () => undefined as any,
onPreviousClick: () => undefined as any,
onFirstClick: () => undefined as any,
onLastClick: () => undefined as any,
onPageInput: () => undefined as any,
};

private parseInteger(input: React.ReactText, lastPage: number): number | string {
let inputPage = Number.parseInt(input as string, 10);
if (!Number.isNaN(inputPage)) {
inputPage = inputPage > lastPage ? lastPage : inputPage;
inputPage = inputPage < 1 ? 1 : inputPage;
}
return inputPage;
}

onChange(event: React.ChangeEvent<HTMLInputElement>, lastPage: number): void {
const inputPage = this.parseInteger(event.target.value, lastPage);
this.setState({ userInputPage: Number.isNaN(inputPage as number) ? event.target.value : inputPage });
}

onKeyDown(event: React.KeyboardEvent<HTMLInputElement>, page: number | string, lastPage: number, onPageInput: (event: React.SyntheticEvent<HTMLButtonElement>, page: number) => void, onSetPage: (event: React.SyntheticEvent<HTMLButtonElement>, page: number) => void): void {
if (event.keyCode === KEY_CODES.ENTER) {
const inputPage = this.parseInteger(this.state.userInputPage, lastPage) as number;
onPageInput(event, Number.isNaN(inputPage) ? page as number : inputPage);
onSetPage(event, Number.isNaN(inputPage) ? page as number : inputPage);
}
}

render () {
const {
page,
lastPage,
pagesTitle,
toLastPage,
toNextPage,
toFirstPage,
toPreviousPage,
currPage,
paginationTitle,
onSetPage,
onNextClick,
onPreviousClick,
onFirstClick,
onLastClick,
onPageInput,
className,
...props
} = this.props;
const { userInputPage } = this.state;
return (
<nav className={css(styles.paginationNav, className)} aria-label={paginationTitle} {...props}>
<Button
variant={ButtonVariant.plain}
isDisabled={page === 1}
aria-label={toFirstPage}
data-action="first"
onClick={event => {
onFirstClick(event, 1);
onSetPage(event, 1);
this.setState({ userInputPage: 1 });
}}
>
<AngleDoubleLeftIcon />
</Button>
<Button
variant={ButtonVariant.plain}
isDisabled={page === 1}
data-action="previous"
onClick={event => {
const newPage = page as number - 1 >= 1 ? page as number - 1 : 1;
onPreviousClick(event, newPage);
onSetPage(event, newPage);
this.setState({ userInputPage: newPage });
}}
aria-label={toPreviousPage}
>
<AngleLeftIcon />
</Button>
<div className={css(styles.paginationNavPageSelect)}>
<input
className={css(styles.formControl)}
aria-label={currPage}
type="number"
min="1"
max={lastPage}
value={userInputPage}
onKeyDown={event => this.onKeyDown(event, page, lastPage, onPageInput, onSetPage)}
onChange={event => this.onChange(event, lastPage)}
/>
<span aria-hidden="true">
of {pluralize(lastPage, pagesTitle)}
</span>
</div>
<Button
variant={ButtonVariant.plain}
isDisabled={page === lastPage}
aria-label={toNextPage}
data-action="next"
onClick={event => {
const newPage = page as number + 1 <= lastPage ? page as number + 1 : lastPage;
onNextClick(event, newPage);
onSetPage(event, newPage);
this.setState({ userInputPage: newPage });
}}
>
<AngleRightIcon />
</Button>
<Button
variant={ButtonVariant.plain}
isDisabled={page === lastPage}
aria-label={toLastPage}
data-action="last"
onClick={event => {
onLastClick(event, lastPage);
onSetPage(event, lastPage);
this.setState({ userInputPage: lastPage });
}}
>
<AngleDoubleRightIcon />
</Button>
</nav>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ describe('API', () => {
wrapper
.find('input')
.first()
.simulate('change', { target: { value: '1' } });
.simulate('change', { target: { value: '1' } })
.simulate('keydown', { keyCode: 13 });
expect(onSetPage.mock.calls).toHaveLength(1);
expect(onSetPage.mock.calls[0][1]).toBe(1);
});
Expand All @@ -121,7 +122,8 @@ describe('API', () => {
wrapper
.find('input')
.first()
.simulate('change', { target: { value: 'a' } });
.simulate('change', { target: { value: 'a' } })
.simulate('keydown', { keyCode: 13 });
expect(onSetPage.mock.calls).toHaveLength(1);
expect(onSetPage.mock.calls[0][1]).toBe(1);
});
Expand All @@ -131,7 +133,8 @@ describe('API', () => {
wrapper
.find('input')
.first()
.simulate('change', { target: { value: '10' } });
.simulate('change', { target: { value: '10' } })
.simulate('keydown', { keyCode: 13 });
expect(onSetPage.mock.calls).toHaveLength(1);
expect(onSetPage.mock.calls[0][1]).toBe(4);
});
Expand All @@ -141,7 +144,8 @@ describe('API', () => {
wrapper
.find('input')
.first()
.simulate('change', { target: { value: '-10' } });
.simulate('change', { target: { value: '-10' } })
.simulate('keydown', { keyCode: 13 });
expect(onSetPage.mock.calls).toHaveLength(1);
expect(onSetPage.mock.calls[0][1]).toBe(1);
});
Expand Down
Loading

0 comments on commit fe4baf1

Please sign in to comment.