From d8368d1e37dd4cedb49a57722cd1e68cb8599f1b Mon Sep 17 00:00:00 2001 From: literat Date: Sun, 22 Oct 2023 22:01:44 +0200 Subject: [PATCH] Feat(web-react): Introduce UncontrolledPagination component --- .../src/components/Pagination/README.md | 24 +++++++ .../Pagination/UncontrolledPagination.tsx | 66 +++++++++++++++++++ .../__tests__/UncontrolledPagination.test.tsx | 53 +++++++++++++++ .../src/components/Pagination/index.ts | 8 ++- .../UncontrolledPagination.stories.tsx | 43 ++++++++++++ .../components/Pagination/usePagination.tsx | 48 ++++++++++++++ packages/web-react/src/types/pagination.ts | 43 +++++++++--- 7 files changed, 272 insertions(+), 13 deletions(-) create mode 100644 packages/web-react/src/components/Pagination/UncontrolledPagination.tsx create mode 100644 packages/web-react/src/components/Pagination/__tests__/UncontrolledPagination.test.tsx create mode 100644 packages/web-react/src/components/Pagination/stories/UncontrolledPagination.stories.tsx create mode 100644 packages/web-react/src/components/Pagination/usePagination.tsx diff --git a/packages/web-react/src/components/Pagination/README.md b/packages/web-react/src/components/Pagination/README.md index 7de44e7d28..57b649507e 100644 --- a/packages/web-react/src/components/Pagination/README.md +++ b/packages/web-react/src/components/Pagination/README.md @@ -75,6 +75,18 @@ ``` +## Uncontrolled Pagination + +```jsx + { + console.log(pageNumber); + }} +/> +``` + ## Pagination props | Name | Type | Default | Required | Description | @@ -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. diff --git a/packages/web-react/src/components/Pagination/UncontrolledPagination.tsx b/packages/web-react/src/components/Pagination/UncontrolledPagination.tsx new file mode 100644 index 0000000000..84d55f6311 --- /dev/null +++ b/packages/web-react/src/components/Pagination/UncontrolledPagination.tsx @@ -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 ( + + {currentPage !== 1 && ( + { + event.preventDefault(); + handlePageChange(currentPage - 1); + }} + /> + )} + {pages?.map((pageNumber: number) => ( + + { + event.preventDefault(); + handlePageChange(pageNumber); + }} + /> + + ))} + {currentPage !== totalPages && ( + { + event.preventDefault(); + handlePageChange(currentPage + 1); + }} + /> + )} + + ); +}; + +export default UncontrolledPagination; diff --git a/packages/web-react/src/components/Pagination/__tests__/UncontrolledPagination.test.tsx b/packages/web-react/src/components/Pagination/__tests__/UncontrolledPagination.test.tsx new file mode 100644 index 0000000000..a4ca2bd2a3 --- /dev/null +++ b/packages/web-react/src/components/Pagination/__tests__/UncontrolledPagination.test.tsx @@ -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( + , + ); + + 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( + , + ); + + 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( + , + ); + + const items = screen.getAllByRole('button'); + expect(items).toHaveLength(2); + + const nextPageItem = screen.getByText('test page 6'); + + fireEvent.click(nextPageItem); + + expect(onPageChange).toHaveBeenCalledWith(6); + }); +}); diff --git a/packages/web-react/src/components/Pagination/index.ts b/packages/web-react/src/components/Pagination/index.ts index 7bfb01d085..4bcb66b813 100644 --- a/packages/web-react/src/components/Pagination/index.ts +++ b/packages/web-react/src/components/Pagination/index.ts @@ -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'; diff --git a/packages/web-react/src/components/Pagination/stories/UncontrolledPagination.stories.tsx b/packages/web-react/src/components/Pagination/stories/UncontrolledPagination.stories.tsx new file mode 100644 index 0000000000..3157985e9e --- /dev/null +++ b/packages/web-react/src/components/Pagination/stories/UncontrolledPagination.stories.tsx @@ -0,0 +1,43 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; + +import { UncontrolledPagination } from '..'; + +const meta: Meta = { + 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; + +export const UncontrolledPaginationPlayground: Story = { + name: 'UncontrolledPagination', + render: (args) => , +}; diff --git a/packages/web-react/src/components/Pagination/usePagination.tsx b/packages/web-react/src/components/Pagination/usePagination.tsx new file mode 100644 index 0000000000..3350c209dc --- /dev/null +++ b/packages/web-react/src/components/Pagination/usePagination.tsx @@ -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, + }; +}; diff --git a/packages/web-react/src/types/pagination.ts b/packages/web-react/src/types/pagination.ts index 6cb76f38d1..7327c1ea6d 100644 --- a/packages/web-react/src/types/pagination.ts +++ b/packages/web-react/src/types/pagination.ts @@ -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'; @@ -15,24 +16,27 @@ export interface PaginationProps extends SpiritElementProps { export interface PaginationItemProps extends SpiritLItemElementProps {} +export interface AriaPaginationProps { + accessibilityLabel?: string; +} + export interface PaginationLinkBaseProps { elementType?: E; } -export interface PaginationLinkProps extends PaginationLinkBaseProps { +export interface PaginationLinkProps + extends PaginationLinkBaseProps, + AriaPaginationProps { isCurrent?: boolean; - accessibilityLabel: string; pageNumber: number; } -export type PaginationButtonLinkProps = SpiritButtonProps & { - direction: PaginationLinkDirectionType; - accessibilityLabel: string; -}; +export type PaginationButtonLinkProps = SpiritButtonProps & + AriaPaginationProps & { + direction: PaginationLinkDirectionType; + }; -export type PaginationLinkPreviousNextProps = SpiritButtonProps & { - accessibilityLabel?: string; -}; +export type PaginationLinkPreviousNextProps = SpiritButtonProps & AriaPaginationProps; export interface SpiritPaginationProps extends PaginationProps {} @@ -44,3 +48,22 @@ export type SpiritPaginationLinkProps = PaginationL export type SpiritPaginationButtonLinkProps = PaginationButtonLinkProps; export type SpiritPaginationLinkPreviousNextProps = PaginationLinkPreviousNextProps; + +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; +}