Skip to content

Commit

Permalink
Feat(web-react): Introduce UncontrolledPagination component
Browse files Browse the repository at this point in the history
  • Loading branch information
literat committed Oct 24, 2023
1 parent 0fe93c4 commit d8368d1
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 13 deletions.
24 changes: 24 additions & 0 deletions packages/web-react/src/components/Pagination/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@
</Pagination>
```

## Uncontrolled Pagination

```jsx
<UncontrolledPagination
totalPages={10}
defaultPage={5}
onChange={(pageNumber) => {
console.log(pageNumber);
}}
/>
```

## Pagination props

| Name | Type | Default | Required | Description |
Expand Down Expand Up @@ -132,6 +144,18 @@ This component extends the `PaginationButtonLink` component.
| -------------------- | -------- | ------- | -------- | ------------------------------- |
| `accessibilityLabel` | `string` | `Next` || Accessibility label of the link |

## UncontrolledPagination props

| Name | Type | Default | Required | Description |
| ---------------------------- | ------------------------------ | ---------- | -------- | ------------------------------------------------------------------- |
| `accessibilityLabel` | `string` | - || Accessibility label of the page links |
| `accessibilityLabelNext` | `string` | `Next` || Accessibility label of the next link |
| `accessibilityLabelPrevious` | `string` | `Previous` || Accessibility label of the previous link |
| `defaultPage` | `number` | `1` || The number of the page selected as current page at the first render |
| `onChange` | `(pageNumber: number) => void` | - || On page change callback |
| `totalPages` | `number` | 0 || Total count of pages |
| `visiblePages` | `number` | `5` || Number of displayed pages |

## Icons Provider

To display the icons correctly, the component needs to be wrapped with IconsProvider.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react';
import { SpiritUncontrolledPaginationProps } from '../../types/pagination';
import Pagination from './Pagination';
import PaginationItem from './PaginationItem';
import PaginationLink from './PaginationLink';
import PaginationLinkNext from './PaginationLinkNext';
import PaginationLinkPrevious from './PaginationLinkPrevious';
import { usePagination } from './usePagination';

export const UncontrolledPagination = (props: SpiritUncontrolledPaginationProps): JSX.Element => {
const {
accessibilityLabel,
accessibilityLabelPrevious = 'Previous',
accessibilityLabelNext = 'Next',
defaultPage = 1,
onChange,
totalPages = 0,
visiblePages = 5,
...rest
} = props;
const { currentPage, pages, handlePageChange } = usePagination({
defaultPage,
onChange,
totalPages,
visiblePages,
});

return (
<Pagination {...rest}>
{currentPage !== 1 && (
<PaginationLinkPrevious
accessibilityLabel={accessibilityLabelPrevious}
onClick={(event) => {
event.preventDefault();
handlePageChange(currentPage - 1);
}}
/>
)}
{pages?.map((pageNumber: number) => (
<PaginationItem key={pageNumber}>
<PaginationLink
accessibilityLabel={`${accessibilityLabel} ${pageNumber}`}
href="#"
isCurrent={currentPage === pageNumber}
pageNumber={pageNumber}
onClick={(event) => {
event.preventDefault();
handlePageChange(pageNumber);
}}
/>
</PaginationItem>
))}
{currentPage !== totalPages && (
<PaginationLinkNext
accessibilityLabel={accessibilityLabelNext}
onClick={(event) => {
event.preventDefault();
handlePageChange(currentPage + 1);
}}
/>
)}
</Pagination>
);
};

export default UncontrolledPagination;
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import UncontrolledPagination from '../UncontrolledPagination';

describe('UncontrolledPagination', () => {
const onPageChange = jest.fn();

beforeEach(() => {
onPageChange.mockClear();
});

it('renders pagination items with test page selected', () => {
render(
<UncontrolledPagination accessibilityLabel="test page" totalPages={10} defaultPage={5} onChange={onPageChange} />,
);

const items = screen.getAllByRole('button');
expect(items).toHaveLength(2);

const selectedPageItem = screen.getByText('test page 5').parentElement;
expect(selectedPageItem).toHaveClass('Pagination__link Pagination__link--current');
});

it('renders disabled items for the first and last page', () => {
const { container } = render(
<UncontrolledPagination accessibilityLabel="test page" totalPages={10} defaultPage={1} onChange={onPageChange} />,
);

const items = screen.getAllByRole('button');
expect(items).toHaveLength(1);

const firstPageItem = screen.getByText('test page 1').parentElement;
const lastItemPage = container.querySelector('.Button--square');

expect(firstPageItem).toHaveClass('Pagination__link Pagination__link--current');
expect(lastItemPage).toHaveClass('Button Button--secondary Button--medium Button--square');
});

it('calls the onPageChange function when an item is clicked', () => {
render(
<UncontrolledPagination accessibilityLabel="test page" totalPages={10} defaultPage={5} onChange={onPageChange} />,
);

const items = screen.getAllByRole('button');
expect(items).toHaveLength(2);

const nextPageItem = screen.getByText('test page 6');

fireEvent.click(nextPageItem);

expect(onPageChange).toHaveBeenCalledWith(6);
});
});
8 changes: 5 additions & 3 deletions packages/web-react/src/components/Pagination/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export { default as Pagination } from './Pagination';
export { default as PaginationButtonLink } from './PaginationButtonLink';
export { default as PaginationItem } from './PaginationItem';
export { default as PaginationLink } from './PaginationLink';
export { default as PaginationButtonLink } from './PaginationButtonLink';
export { default as PaginationLinkPrevious } from './PaginationLinkPrevious';
export { default as PaginationLinkNext } from './PaginationLinkNext';
export * from './usePaginationStyleProps';
export { default as PaginationLinkPrevious } from './PaginationLinkPrevious';
export * from './UncontrolledPagination';
export { default as UncontrolledPagination } from './UncontrolledPagination';
export * from './constants';
export * from './usePaginationStyleProps';
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';

import { UncontrolledPagination } from '..';

const meta: Meta<typeof UncontrolledPagination> = {
title: 'Components/Pagination',
component: UncontrolledPagination,
parameters: {
layout: 'centered',
},
argTypes: {
defaultPage: {
control: 'number',
description: 'Default page for the first render; please reload the page to apply this setting.',
table: {
defaultValue: { summary: 1 },
},
},
totalPages: {
control: 'number',
},
visiblePages: {
control: 'number',
table: {
defaultValue: { summary: 5 },
},
},
},
args: {
defaultPage: 3,
totalPages: 10,
visiblePages: 5,
},
};

export default meta;
type Story = StoryObj<typeof UncontrolledPagination>;

export const UncontrolledPaginationPlayground: Story = {
name: 'UncontrolledPagination',
render: (args) => <UncontrolledPagination {...args} />,
};
48 changes: 48 additions & 0 deletions packages/web-react/src/components/Pagination/usePagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useCallback, useMemo, useState } from 'react';
import { UsePaginationProps } from '../../types/pagination';

export const usePagination = ({ totalPages, onChange, defaultPage, visiblePages }: UsePaginationProps) => {
const [currentPage, setCurrentPage] = useState(defaultPage <= 0 || defaultPage > totalPages ? 1 : defaultPage ?? 1);
const [pages, setPagesArray] = useState([visiblePages] ?? [5]);

useMemo(() => {
const currentVisiblePages = visiblePages > totalPages ? totalPages : visiblePages;
const firstPageChapter =
totalPages - currentPage < currentVisiblePages ? totalPages - (currentVisiblePages - 1) : currentPage;

setPagesArray(Array.from(Array(currentVisiblePages), (_, index) => index + firstPageChapter));
}, [visiblePages, currentPage, totalPages]);

const handlePageChange = useCallback(
(pageNumber: number) => {
setCurrentPage(pageNumber);
onChange && onChange(pageNumber);
},
[onChange],
);

const getPagination = () => {
const halfChap = Math.floor(visiblePages / 2);
let startPage = Math.max(1, currentPage - halfChap);
const endPage = Math.min(startPage + visiblePages - 1, totalPages);

if (totalPages - visiblePages < startPage - 1) {
const tmpStartPage = totalPages - visiblePages + 1;
startPage = tmpStartPage < 1 ? 1 : tmpStartPage;
}

return Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i);
};

useMemo(() => {
setPagesArray(getPagination());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return {
totalPages,
currentPage,
pages,
handlePageChange,
};
};
43 changes: 33 additions & 10 deletions packages/web-react/src/types/pagination.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ElementType } from 'react';
import { SpiritButtonProps } from './button';
import {
ChildrenProps,
SpiritElementProps,
SpiritLItemElementProps,
SpiritPolymorphicElementPropsWithRef,
SpiritUListElementProps,
} from './shared';
import { SpiritButtonProps } from './button';

export type PaginationLinkDirectionType = 'previous' | 'next';

Expand All @@ -15,24 +16,27 @@ export interface PaginationProps extends SpiritElementProps {

export interface PaginationItemProps extends SpiritLItemElementProps {}

export interface AriaPaginationProps {
accessibilityLabel?: string;
}

export interface PaginationLinkBaseProps<E extends ElementType = 'a'> {
elementType?: E;
}

export interface PaginationLinkProps<E extends ElementType = 'a'> extends PaginationLinkBaseProps<E> {
export interface PaginationLinkProps<E extends ElementType = 'a'>
extends PaginationLinkBaseProps<E>,
AriaPaginationProps {
isCurrent?: boolean;
accessibilityLabel: string;
pageNumber: number;
}

export type PaginationButtonLinkProps<E extends ElementType = 'a'> = SpiritButtonProps<E> & {
direction: PaginationLinkDirectionType;
accessibilityLabel: string;
};
export type PaginationButtonLinkProps<E extends ElementType = 'a'> = SpiritButtonProps<E> &
AriaPaginationProps & {
direction: PaginationLinkDirectionType;
};

export type PaginationLinkPreviousNextProps<E extends ElementType = 'a'> = SpiritButtonProps<E> & {
accessibilityLabel?: string;
};
export type PaginationLinkPreviousNextProps<E extends ElementType = 'a'> = SpiritButtonProps<E> & AriaPaginationProps;

export interface SpiritPaginationProps extends PaginationProps {}

Expand All @@ -44,3 +48,22 @@ export type SpiritPaginationLinkProps<E extends ElementType = 'a'> = PaginationL
export type SpiritPaginationButtonLinkProps<E extends ElementType = 'a'> = PaginationButtonLinkProps<E>;

export type SpiritPaginationLinkPreviousNextProps<E extends ElementType = 'a'> = PaginationLinkPreviousNextProps<E>;

export interface UncontrolledPaginationProps {
accessibilityLabelNext?: string;
accessibilityLabelPrevious?: string;
defaultPage?: number;
visiblePages?: number;
onChange?: (pageNumber: number) => void;
totalPages: number;
}

export interface SpiritUncontrolledPaginationProps
extends AriaPaginationProps,
UncontrolledPaginationProps,
ChildrenProps {}

export interface UsePaginationProps extends UncontrolledPaginationProps {
defaultPage: number;
visiblePages: number;
}

0 comments on commit d8368d1

Please sign in to comment.