Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pagination component #674

Merged
merged 20 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/css/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

/* Append here */
@import "./pagination/pagination";
@import "./highlight/highlight";
@import "./accordion/accordion";
@import "./alert/alert";
Expand Down
4 changes: 2 additions & 2 deletions packages/css/src/link/link.scss
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@
// Override for icon size
.amsterdam-link--in-list__chevron {
span.amsterdam-icon svg {
height: 16px;
width: 16px;
height: 1rem;
width: 1rem;
}
}

Expand Down
18 changes: 18 additions & 0 deletions packages/css/src/pagination/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Pagination

Pagination (in het Nederlands: paginering) is een navigatie-element onder een zoekresultatenlijst. Bij grote hoeveelheden zoekresultaten kan het duidelijker of functioneler zijn om de inhoud over meerdere pagina´s te verdelen. Paginering toont op welke zoekresultatenlijst de gebruiker zich bevindt en kan hiermee naar een andere zoekresultatenlijst navigeren.
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved

## Richtlijnen

- Gebruik paginering alleen op een zoekresultatenpagina.
- Voeg de paginering toe na de lijst met zoekresultaten.
- Start een zoekresultatenpagina bovenaan de pagina na het veranderen van pagina.
- De paginering kan gecombineerd worden met een teller bovenaan de pagina die “Pagina # van ##” aanduidt.
- De paginering wordt niet getoond als er maar 1 pagina is.
- Verwijs de gebruikers door naar de eerste pagina als ze een URL opgeven van een paginanummer dat niet (meer) bestaat.

## Relevante WCAG regels

- [WCAG 2.4.8](https://www.w3.org/TR/WCAG22/#location): geef aan waar de gebruiker is in een verzameling van pagina's (AAA).

Pagination is een interactief element, hier gelden [de algemene eisen en richtlijnen voor interactieve elementen](https://amsterdam.github.io/design-system/?path=/docs/docs-designrichtlijnen-interactieve-elementen--docs) voor.
73 changes: 73 additions & 0 deletions packages/css/src/pagination/pagination.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* @license EUPL-1.2+
* Copyright (c) 2023 Gemeente Amsterdam
*/

@import "../../utils/breakpoint";

@mixin list-reset {
list-style-type: none;
margin-block: 0;
padding-inline-start: 0;
}

.amsterdam-pagination__list {
color: var(--amsterdam-pagination-color);
display: flex;
flex-wrap: wrap;
font-family: var(--amsterdam-pagination-font-family);
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved
font-size: var(--amsterdam-pagination-narrow-font-size);
font-weight: var(--amsterdam-pagination-font-weight);
justify-content: center;
line-height: var(--amsterdam-pagination-line-height);

@include list-reset;

@media screen and (width > $amsterdam-breakpoint) {
font-size: var(--amsterdam-pagination-wide-font-size);
}
}

@mixin button-reset {
all: unset;
box-sizing: border-box;
outline: revert;
-webkit-text-size-adjust: 100%;
}

.amsterdam-pagination__button {
@include button-reset;
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved

cursor: pointer;
display: flex;
gap: 0.5rem;
outline-offset: var(--amsterdam-pagination-button-outline-offset);
padding-inline: 0.75rem;
text-decoration-thickness: 2px;
text-underline-offset: 3px;
touch-action: manipulation;

&:hover {
color: var(--amsterdam-pagination-button-hover-color);
text-decoration: underline;
}

&:disabled {
visibility: hidden;
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved
}

// Override for icon size
span.amsterdam-icon svg {
height: 1rem;
width: 1rem;
}
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved
}

.amsterdam-pagination__button--current {
cursor: default;
font-weight: var(--amsterdam-pagination-button-current-font-weight);

&:hover {
text-decoration: none;
}
}
105 changes: 105 additions & 0 deletions packages/react/src/Pagination/Pagination.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { createRef, useState } from 'react'
import { Pagination } from './Pagination'
import '@testing-library/jest-dom'

describe('Pagination', () => {
it('renders', () => {
const { container } = render(<Pagination collectionSize={60} />)
const component = container.querySelector(':only-child')
expect(component).toBeInTheDocument()
expect(component).toBeVisible()
})

it('renders a design system BEM class name', () => {
const { container } = render(<Pagination collectionSize={60} />)
const component = container.querySelector(':only-child')
expect(component).toHaveClass('amsterdam-pagination')
})

it('can have a additional class name', () => {
const { container } = render(<Pagination collectionSize={60} className="extra" />)
const component = container.querySelector(':only-child')
expect(component).toHaveClass('extra')
expect(component).toHaveClass('amsterdam-pagination')
})

it('should render all the pages when the pages < maxVisiblePages', () => {
render(<Pagination pageSize={10} collectionSize={60} maxVisiblePages={7} />)
expect(screen.getAllByRole('listitem').length).toBe(8) // 6 + 2 buttons
})

it('should render the pages including one (last) spacer when the pages > maxVisiblePages', () => {
render(<Pagination page={1} pageSize={10} collectionSize={80} maxVisiblePages={7} />)
expect(screen.getAllByRole('listitem').length).toBe(8) // 6 + 2 buttons
expect(screen.getByTestId('lastSpacer')).toBeInTheDocument()
expect(screen.queryByTestId('firstSpacer')).not.toBeInTheDocument()
})

it('should render the pages including the two spacer when the pages > maxVisiblePages and current page > 4', () => {
render(<Pagination page={6} pageSize={10} collectionSize={100} maxVisiblePages={7} />)
expect(screen.getAllByRole('listitem').length).toBe(7) // 5 + 2 buttons
expect(screen.getByTestId('lastSpacer')).toBeInTheDocument()
expect(screen.getByTestId('firstSpacer')).toBeInTheDocument()
})

it('should navigate to the next page when clicking on the "next" button', () => {
const onPageChangeMock = jest.fn()
render(<Pagination page={6} pageSize={10} collectionSize={100} onPageChange={onPageChangeMock} />)

expect(onPageChangeMock).not.toHaveBeenCalled()
expect(screen.getByText('6')).toHaveAttribute('aria-current', 'true')
expect(screen.getByText('7')).not.toHaveAttribute('aria-current', 'true')

fireEvent.click(screen.getByText('volgende'))

expect(onPageChangeMock).toHaveBeenCalled()
expect(screen.getByText('6')).not.toHaveAttribute('aria-current', 'true')
expect(screen.getByText('7')).toHaveAttribute('aria-current', 'true')
})

it('should navigate to the previous page when clicking on the "previous" button', () => {
const onPageChangeMock = jest.fn()
render(<Pagination page={6} pageSize={10} collectionSize={100} onPageChange={onPageChangeMock} />)

expect(onPageChangeMock).not.toHaveBeenCalled()
expect(screen.getByText('6')).toHaveAttribute('aria-current', 'true')
expect(screen.getByText('5')).not.toHaveAttribute('aria-current', 'true')

fireEvent.click(screen.getByText('vorige'))

expect(onPageChangeMock).toHaveBeenCalled()
expect(screen.getByText('6')).not.toHaveAttribute('aria-current', 'true')
expect(screen.getByText('5')).toHaveAttribute('aria-current', 'true')
})

it('should be working in a controlled state', () => {
function ControlledComponent() {
const [page, setPage] = useState(6)

return <Pagination page={page} pageSize={10} collectionSize={100} onPageChange={setPage} />
}

render(<ControlledComponent />)

expect(screen.getByText('6')).toHaveAttribute('aria-current', 'true')
expect(screen.getByText('5')).not.toHaveAttribute('aria-current', 'true')

fireEvent.click(screen.getByText('vorige'))

expect(screen.getByText('6')).not.toHaveAttribute('aria-current', 'true')
expect(screen.getByText('5')).toHaveAttribute('aria-current', 'true')

fireEvent.click(screen.getByText('volgende'))

expect(screen.getByText('6')).toHaveAttribute('aria-current', 'true')
expect(screen.getByText('5')).not.toHaveAttribute('aria-current', 'true')
})

it('supports ForwardRef in React', () => {
const ref = createRef<HTMLElement>()
const { container } = render(<Pagination collectionSize={60} ref={ref} />)
const component = container.querySelector(':only-child')
expect(ref.current).toBe(component)
})
})
Loading