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

feat(Pagination)!: Use links instead of buttons #1821

Merged
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2f5a973
Remove onClick handlers and onPageChange
alimpens Jan 15, 2025
2cf1716
Use links instead of buttons
alimpens Jan 15, 2025
f367e7c
Update styling, add rel attrs
alimpens Jan 15, 2025
b4bc610
Add link template
alimpens Jan 15, 2025
9c52517
Prev and next should be outside of list
alimpens Jan 15, 2025
6ecadb6
Don't show previous or next on first or last page
alimpens Jan 15, 2025
46c8085
Move getRange function to separate file
alimpens Jan 15, 2025
dda74ad
Add tests
alimpens Jan 17, 2025
af77843
Add linkComponent prop
alimpens Jan 17, 2025
ca2886b
Add docs
alimpens Jan 17, 2025
4dd3602
Sort props
alimpens Jan 17, 2025
cb7221d
Merge branch 'develop' into feat/DES-1024-pagination-links-instead-of…
alimpens Jan 17, 2025
acdd492
Use inline-flex instead of flex
alimpens Jan 22, 2025
4d0eeb3
Rephrase comments
alimpens Jan 22, 2025
c2f97f0
Return earlier
alimpens Jan 22, 2025
80f56ab
Update docs
alimpens Jan 22, 2025
91b6a06
Merge branch 'feat/DES-1024-pagination-links-instead-of-buttons' of h…
alimpens Jan 22, 2025
3459b87
Update docs
alimpens Jan 22, 2025
9578379
Update docs
alimpens Jan 22, 2025
014f1a6
Extract LinkItem and Spacer into separate components
alimpens Jan 24, 2025
f43a50a
Get rid of test id's
alimpens Jan 24, 2025
bd5bd17
Update tests
alimpens Jan 24, 2025
546974d
Merge branch 'feat/DES-1024-pagination-links-instead-of-buttons' of h…
alimpens Jan 24, 2025
6604d46
Rename visuallyHiddenLabelId
alimpens Jan 24, 2025
3721c0c
Merge branch 'develop' of https://github.com/Amsterdam/design-system …
alimpens Jan 24, 2025
d00c264
Fix lint issue
alimpens Jan 24, 2025
09e7f1a
Fix tests
alimpens Jan 24, 2025
3365459
Extract Spacer and LinkItem into separate files
alimpens Jan 24, 2025
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
55 changes: 22 additions & 33 deletions packages/css/src/components/pagination/pagination.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,45 @@

@use "../../common/text-rendering" as *;

.ams-pagination {
display: flex;
flex-wrap: wrap;
font-family: var(--ams-pagination-font-family);
font-size: var(--ams-pagination-font-size);
font-weight: var(--ams-pagination-font-weight);
justify-content: center;
line-height: var(--ams-pagination-line-height);
}

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

.ams-pagination__list {
color: var(--ams-pagination-color);
display: flex;
flex-wrap: wrap;
font-family: var(--ams-pagination-font-family);
font-size: var(--ams-pagination-font-size);
font-weight: var(--ams-pagination-font-weight);
justify-content: center;
line-height: var(--ams-pagination-line-height);

@include reset-ol;
}

@mixin reset-button {
all: unset;
outline: revert;
}

.ams-pagination__button {
/* The reset is included at the top of the block here, if you set it
at the bottom `all: unset` overrides the `gap` property. */
@include reset-button;

box-sizing: border-box;
cursor: pointer;
.ams-pagination__link {
color: var(--ams-pagination-link-color);
display: flex;
gap: var(--ams-pagination-button-gap);
outline-offset: var(--ams-pagination-button-outline-offset);
padding-inline: var(--ams-pagination-button-padding-inline);
text-decoration-line: var(--ams-pagination-button-text-decoration-line);
text-decoration-thickness: var(--ams-pagination-button-text-decoration-thickness);
text-underline-offset: var(--ams-pagination-button-text-underline-offset);
gap: var(--ams-pagination-link-gap);
outline-offset: var(--ams-pagination-link-outline-offset);
padding-inline: var(--ams-pagination-link-padding-inline);
text-decoration-line: var(--ams-pagination-link-text-decoration-line);
text-decoration-thickness: var(--ams-pagination-link-text-decoration-thickness);
text-underline-offset: var(--ams-pagination-link-text-underline-offset);
touch-action: manipulation;

@include text-rendering;

&:disabled {
display: none;
}

&:hover {
color: var(--ams-pagination-button-hover-color);
text-decoration-line: var(--ams-pagination-button-hover-text-decoration-line);
color: var(--ams-pagination-link-hover-color);
text-decoration-line: var(--ams-pagination-link-hover-text-decoration-line);
}

// Override for icon size
Expand All @@ -63,9 +53,8 @@
}
}

.ams-pagination__button--current {
cursor: default;
font-weight: var(--ams-pagination-button-current-font-weight);
.ams-pagination__link[aria-current="page"] {
font-weight: var(--ams-pagination-link-current-font-weight);

&:hover {
text-decoration: none;
Expand Down
155 changes: 84 additions & 71 deletions packages/react/src/Pagination/Pagination.test.tsx
Original file line number Diff line number Diff line change
@@ -1,150 +1,163 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { createRef, useState } from 'react'
import { render, screen } from '@testing-library/react'
import { createRef } from 'react'
import type { AnchorHTMLAttributes } from 'react'
import { Pagination } from './Pagination'
import '@testing-library/jest-dom'

describe('Pagination', () => {
const linkTemplate = (page: number) => `#?pagina=${page}`

it('renders', () => {
const { container } = render(<Pagination totalPages={10} />)
render(<Pagination linkTemplate={linkTemplate} totalPages={10} />)

const component = container.querySelector(':only-child')
const component = screen.getByRole('navigation', { name: 'Paginering' })

expect(component).toBeInTheDocument()
expect(component).toBeVisible()
})

it('renders a design system BEM class name', () => {
const { container } = render(<Pagination totalPages={10} />)
render(<Pagination linkTemplate={linkTemplate} totalPages={10} />)

const component = container.querySelector(':only-child')
const component = screen.getByRole('navigation', { name: 'Paginering' })

expect(component).toHaveClass('ams-pagination')
})

it('can have a additional class name', () => {
const { container } = render(<Pagination totalPages={10} className="extra" />)
render(<Pagination className="extra" linkTemplate={linkTemplate} totalPages={10} />)

const component = container.querySelector(':only-child')
const component = screen.getByRole('navigation', { name: 'Paginering' })

expect(component).toHaveClass('extra')
expect(component).toHaveClass('ams-pagination')
expect(component).toHaveClass('ams-pagination extra')
})

it('should render all the pages when totalPages < maxVisiblePages', () => {
render(<Pagination totalPages={6} maxVisiblePages={7} />)
render(<Pagination linkTemplate={linkTemplate} maxVisiblePages={7} totalPages={6} />)

expect(screen.getAllByRole('listitem').length).toBe(8) // 6 + 2 buttons
expect(screen.getAllByRole('listitem').length).toBe(6)
expect(screen.queryByTestId('lastSpacer')).not.toBeInTheDocument()
expect(screen.queryByTestId('firstSpacer')).not.toBeInTheDocument()
})

it('should render the pages including one (last) spacer when totalPages > maxVisiblePages', () => {
render(<Pagination page={1} totalPages={10} maxVisiblePages={7} />)
render(<Pagination linkTemplate={linkTemplate} maxVisiblePages={7} page={1} totalPages={10} />)

expect(screen.getAllByRole('listitem').length).toBe(8) // 6 + 2 buttons
expect(screen.getAllByRole('listitem').length).toBe(6)
expect(screen.getByTestId('lastSpacer')).toBeInTheDocument()
expect(screen.queryByTestId('firstSpacer')).not.toBeInTheDocument()
})

it('should render the pages including the two spacers when totalPages > maxVisiblePages and current page > 4', () => {
render(<Pagination page={6} totalPages={10} maxVisiblePages={7} />)
render(<Pagination linkTemplate={linkTemplate} maxVisiblePages={7} page={6} totalPages={10} />)

expect(screen.getAllByRole('listitem').length).toBe(7) // 5 + 2 buttons
expect(screen.getAllByRole('listitem').length).toBe(5)
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} totalPages={10} onPageChange={onPageChangeMock} />)
it('should set aria-current to true on the current page', () => {
render(<Pagination linkTemplate={linkTemplate} page={4} totalPages={10} />)

expect(onPageChangeMock).not.toHaveBeenCalled()
expect(screen.getByRole('button', { name: 'Pagina 6' })).toHaveAttribute('aria-current', 'true')
expect(screen.getByRole('button', { name: 'Ga naar pagina 7' })).not.toHaveAttribute('aria-current', 'true')
const currentPageLink = screen.getByRole('link', { name: 'Pagina 4' })

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

expect(onPageChangeMock).toHaveBeenCalled()
expect(screen.getByRole('button', { name: 'Ga naar pagina 6' })).not.toHaveAttribute('aria-current', 'true')
expect(screen.getByRole('button', { name: 'Pagina 7' })).toHaveAttribute('aria-current', 'true')
expect(currentPageLink).toHaveAttribute('aria-current', 'page')
})

it('should navigate to the previous page when clicking on the ‘previous’ button', () => {
const onPageChangeMock = jest.fn()

render(<Pagination page={6} totalPages={10} onPageChange={onPageChangeMock} />)
it('should set the correct href on the links', () => {
render(<Pagination linkTemplate={linkTemplate} page={4} totalPages={10} />)

expect(onPageChangeMock).not.toHaveBeenCalled()
expect(screen.getByRole('button', { name: 'Pagina 6' })).toHaveAttribute('aria-current', 'true')
expect(screen.getByRole('button', { name: 'Ga naar pagina 5' })).not.toHaveAttribute('aria-current', 'true')
const previousPageLink = screen.getByRole('link', { name: 'Vorige pagina' })
const currentPageLink = screen.getByRole('link', { name: 'Pagina 4' })
const nextPageLink = screen.getByRole('link', { name: 'Volgende pagina' })

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

expect(onPageChangeMock).toHaveBeenCalled()
expect(screen.getByRole('button', { name: 'Ga naar pagina 6' })).not.toHaveAttribute('aria-current', 'true')
expect(screen.getByRole('button', { name: 'Pagina 5' })).toHaveAttribute('aria-current', 'true')
expect(previousPageLink).toHaveAttribute('href', '#?pagina=3')
expect(currentPageLink).toHaveAttribute('href', '#?pagina=4')
expect(nextPageLink).toHaveAttribute('href', '#?pagina=5')
})

it('should be working in a controlled state', () => {
function ControlledComponent() {
const [page, setPage] = useState(6)
it('sets a custom id for the accessible label', () => {
render(<Pagination id="custom-id" linkTemplate={linkTemplate} totalPages={10} />)

const component = screen.getByRole('navigation', { name: 'Paginering' })

return <Pagination page={page} totalPages={10} onPageChange={setPage} />
}
expect(component).toHaveAttribute('aria-labelledby', 'custom-id')
})

render(<ControlledComponent />)
it('should not render when totalPages is 1 or less', () => {
render(<Pagination linkTemplate={linkTemplate} totalPages={1} />)

expect(screen.getByRole('button', { name: 'Pagina 6' })).toHaveAttribute('aria-current', 'true')
expect(screen.getByRole('button', { name: 'Ga naar pagina 5' })).not.toHaveAttribute('aria-current', 'true')
expect(screen.queryByRole('navigation')).not.toBeInTheDocument()
})

fireEvent.click(screen.getByText('vorige'))
it('should not show the previous link on the first page', () => {
render(<Pagination linkTemplate={linkTemplate} page={1} totalPages={10} />)

expect(screen.getByRole('button', { name: 'Ga naar pagina 6' })).not.toHaveAttribute('aria-current', 'true')
expect(screen.getByRole('button', { name: 'Pagina 5' })).toHaveAttribute('aria-current', 'true')
expect(screen.queryByRole('link', { name: 'Vorige pagina' })).not.toBeInTheDocument()
})

fireEvent.click(screen.getByText('volgende'))
it('should not show the next link on the last page', () => {
render(<Pagination linkTemplate={linkTemplate} page={10} totalPages={10} />)

expect(screen.getByRole('button', { name: 'Pagina 6' })).toHaveAttribute('aria-current', 'true')
expect(screen.getByRole('button', { name: 'Ga naar pagina 5' })).not.toHaveAttribute('aria-current', 'true')
expect(screen.queryByRole('link', { name: 'Volgende pagina' })).not.toBeInTheDocument()
})

it('renders custom labels for the ‘previous’ and ‘next’ buttons', () => {
render(<Pagination totalPages={10} previousLabel="previous" nextLabel="next" />)
it('renders custom labels for the ‘previous’ and ‘next’ links', () => {
render(
<Pagination linkTemplate={linkTemplate} nextLabel="next" page={4} previousLabel="previous" totalPages={10} />,
)

const previousButton = screen.getByRole('button', { name: 'Vorige pagina' })
const nextButton = screen.getByRole('button', { name: 'Volgende pagina' })
const previousLink = screen.getByRole('link', { name: 'Vorige pagina' })
const nextLink = screen.getByRole('link', { name: 'Volgende pagina' })

expect(previousButton).toHaveTextContent('previous')
expect(nextButton).toHaveTextContent('next')
expect(previousLink).toHaveTextContent('previous')
expect(previousLink).toHaveAttribute('rel', 'prev')
expect(nextLink).toHaveTextContent('next')
expect(nextLink).toHaveAttribute('rel', 'next')
})

it('renders an accessible label for the navigation', () => {
render(<Pagination totalPages={10} visuallyHiddenLabel="Pagination" />)
render(<Pagination linkTemplate={linkTemplate} totalPages={10} visuallyHiddenLabel="Pagination" />)

const navElement = screen.getByRole('navigation')
const component = screen.getByRole('navigation', { name: 'Pagination' })

expect(navElement).toHaveAccessibleName('Pagination')
expect(component).toBeInTheDocument()
})

it('renders accessible labels for the ‘previous’ and ‘next’ buttons', () => {
it('renders accessible labels for the ‘previous’ and ‘next’ links', () => {
render(
<Pagination totalPages={10} previousVisuallyHiddenLabel="Previous page" nextVisuallyHiddenLabel="Next page" />,
<Pagination
linkTemplate={linkTemplate}
nextVisuallyHiddenLabel="Next page"
page={4}
previousVisuallyHiddenLabel="Previous page"
totalPages={10}
/>,
)

const previousButton = screen.getByRole('button', { name: 'Previous page' })
const nextButton = screen.getByRole('button', { name: 'Next page' })
const previousLink = screen.getByRole('link', { name: 'Previous page' })
const nextLink = screen.getByRole('link', { name: 'Next page' })

expect(previousLink).toBeInTheDocument()
expect(nextLink).toBeInTheDocument()
})

it('renders a custom link component', () => {
const CustomLink = (props: AnchorHTMLAttributes<HTMLAnchorElement>) => <a data-test {...props} />

render(<Pagination linkComponent={CustomLink} linkTemplate={linkTemplate} totalPages={10} />)

const customLink = screen.getByRole('link', { name: 'Pagina 1' })

expect(previousButton).toBeInTheDocument()
expect(nextButton).toBeInTheDocument()
expect(customLink).toHaveAttribute('data-test')
})

it('supports ForwardRef in React', () => {
const ref = createRef<HTMLElement>()

const { container } = render(<Pagination totalPages={10} ref={ref} />)
render(<Pagination linkTemplate={linkTemplate} ref={ref} totalPages={10} />)

const component = container.querySelector(':only-child')
const component = screen.getByRole('navigation', { name: 'Paginering' })

expect(ref.current).toBe(component)
})
Expand Down
Loading
Loading